diff --git a/frontend/package.json b/frontend/package.json index 3ddca62541..72b1493138 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", + "@dnd-kit/utilities": "3.2.2", "@grafana/data": "^11.2.3", "@mdx-js/loader": "2.3.0", "@mdx-js/react": "2.3.0", diff --git a/frontend/src/api/quickFilters/getCustomFilters.ts b/frontend/src/api/quickFilters/getCustomFilters.ts new file mode 100644 index 0000000000..b5bd6308e8 --- /dev/null +++ b/frontend/src/api/quickFilters/getCustomFilters.ts @@ -0,0 +1,25 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/quickFilters/getCustomFilters'; + +const getCustomFilters = async ( + props: Props, +): Promise | ErrorResponse> => { + const { signal } = props; + try { + const response = await ApiBaseInstance.get(`orgs/me/filters/${signal}`); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getCustomFilters; diff --git a/frontend/src/api/quickFilters/updateCustomFilters.ts b/frontend/src/api/quickFilters/updateCustomFilters.ts new file mode 100644 index 0000000000..46285a35a9 --- /dev/null +++ b/frontend/src/api/quickFilters/updateCustomFilters.ts @@ -0,0 +1,13 @@ +import { ApiBaseInstance } from 'api'; +import { AxiosError } from 'axios'; +import { SuccessResponse } from 'types/api'; +import { UpdateCustomFiltersProps } from 'types/api/quickFilters/updateCustomFilters'; + +const updateCustomFiltersAPI = async ( + props: UpdateCustomFiltersProps, +): Promise | AxiosError> => + ApiBaseInstance.put(`orgs/me/filters`, { + ...props.data, + }); + +export default updateCustomFiltersAPI; diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts index 6116d1b59b..179eac8e21 100644 --- a/frontend/src/api/utils.ts +++ b/frontend/src/api/utils.ts @@ -12,7 +12,7 @@ export const Logout = (): void => { deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_NAME); deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT); deleteLocalStorageKey(LOCALSTORAGE.USER_ID); - + deleteLocalStorageKey(LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT); window.dispatchEvent(new CustomEvent('LOGOUT')); // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx index 6f00073cec..a9d96adf32 100644 --- a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx +++ b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx @@ -19,7 +19,7 @@ import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSea import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useDebouncedFn from 'hooks/useDebouncedFunction'; -import { cloneDeep, isArray, isEmpty, isEqual, isFunction } from 'lodash-es'; +import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { useMemo, useState } from 'react'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; @@ -82,7 +82,9 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { const attributeValues: string[] = useMemo(() => { const dataType = filter.attributeKey.dataType || DataTypes.String; const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType]; - return (data?.payload?.[key] || []).filter((val) => !isEmpty(val)); + return (data?.payload?.[key] || []).filter( + (val) => val !== undefined && val !== null, + ); }, [data?.payload, filter.attributeKey.dataType]); const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount); diff --git a/frontend/src/components/QuickFilters/QuickFilters.styles.scss b/frontend/src/components/QuickFilters/QuickFilters.styles.scss index 5e64885c10..3c80c2bac9 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.styles.scss +++ b/frontend/src/components/QuickFilters/QuickFilters.styles.scss @@ -1,7 +1,16 @@ +.quick-filters-container { + display: flex; + height: 100%; + .quick-filters-settings-container { + position: relative; + } +} + .quick-filters { display: flex; flex-direction: column; height: 100%; + width: 100%; border-right: 1px solid var(--bg-slate-400); color: var(--bg-vanilla-100); @@ -44,7 +53,7 @@ font-size: 14px; font-style: normal; font-weight: 400; - line-height: 18px; /* 128.571% */ + line-height: 18px; text-transform: uppercase; } } @@ -52,7 +61,7 @@ .right-actions { display: flex; align-items: center; - gap: 12px; + gap: 10px; width: 100%; justify-content: flex-end; @@ -63,10 +72,34 @@ } .sync-icon { - background-color: var(--bg-ink-500); border: 0; box-shadow: none; } + + .right-action-icon-container { + position: relative; + display: flex; + padding: 2px; + background-color: var(--bg-ink-500); + + .settings-icon { + height: 14px; + width: 14px; + cursor: pointer; + } + + &.active, + &:hover { + background: var(--bg-slate-500); + } + } + } + } + + .quick-filters-skeleton { + .ant-skeleton-input { + width: 236px; + margin: 8px 12px; } } } @@ -90,8 +123,12 @@ } } .right-actions { - .sync-icon { + .right-action-icon-container { background-color: var(--bg-vanilla-100); + &.active, + &:hover { + background-color: var(--bg-vanilla-200); + } } } } diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx index 21861e77d4..ed443e8e4f 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -5,18 +5,43 @@ import { SyncOutlined, VerticalAlignTopOutlined, } from '@ant-design/icons'; -import { Tooltip, Typography } from 'antd'; -import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar'; +import { Skeleton, Tooltip, Typography } from 'antd'; +import getLocalStorageKey from 'api/browser/localstorage/get'; +import setLocalStorageKey from 'api/browser/localstorage/set'; +import classNames from 'classnames'; +import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { LOCALSTORAGE } from 'constants/localStorage'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { cloneDeep, isFunction } from 'lodash-es'; +import { cloneDeep, isFunction, isNull } from 'lodash-es'; +import { Settings2 as SettingsIcon } from 'lucide-react'; +import { useMemo, useState } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import Checkbox from './FilterRenderers/Checkbox/Checkbox'; import Slider from './FilterRenderers/Slider/Slider'; +import useFilterConfig from './hooks/useFilterConfig'; +import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip'; +import QuickFiltersSettings from './QuickFiltersSettings/QuickFiltersSettings'; import { FiltersType, IQuickFiltersProps, QuickFiltersSource } from './types'; export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { - const { config, handleFilterVisibilityChange, source, onFilterChange } = props; + const { + className, + config, + handleFilterVisibilityChange, + source, + onFilterChange, + signal, + } = props; + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + + const { + filterConfig, + isDynamicFilters, + customFilters, + setIsStale, + isCustomFiltersLoading, + } = useFilterConfig({ signal, config }); const { currentQuery, @@ -24,6 +49,16 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { redirectWithQueryBuilderData, } = useQueryBuilder(); + const showAnnouncementTooltip = useMemo(() => { + const localStorageValue = getLocalStorageKey( + LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT, + ); + if (!isNull(localStorageValue)) { + return !(localStorageValue === 'false'); + } + return true; + }, []); + // clear all the filters for the query which is in sync with filters const handleReset = (): void => { const updatedQuery = cloneDeep( @@ -63,68 +98,141 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName; return ( -
- {source !== QuickFiltersSource.INFRA_MONITORING && - source !== QuickFiltersSource.API_MONITORING && ( -
-
- - - {lastQueryName ? 'Filters for' : 'Filters'} - - {lastQueryName && ( - - {lastQueryName} +
+
+ {source !== QuickFiltersSource.INFRA_MONITORING && + source !== QuickFiltersSource.API_MONITORING && ( +
+
+ + + {lastQueryName ? 'Filters for' : 'Filters'} + + {lastQueryName && ( + + + {lastQueryName} + + + )} +
+ +
+ +
+ +
- )} + +
+ +
+
+ {isDynamicFilters && ( + +
+ setIsSettingsOpen(true)} + /> + { + setLocalStorageKey( + LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT, + 'false', + ); + }} + /> +
+
+ )} +
+ )} -
- - - -
- - - + {isCustomFiltersLoading ? ( +
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+ ) : ( + +
+ {filterConfig.map((filter) => { + switch (filter.type) { + case FiltersType.CHECKBOX: + return ( + + ); + case FiltersType.SLIDER: + return ; + // eslint-disable-next-line sonarjs/no-duplicated-branches + default: + return ( + + ); + } + })}
-
+ )} - - -
- {config.map((filter) => { - switch (filter.type) { - case FiltersType.CHECKBOX: - return ( - - ); - case FiltersType.SLIDER: - return ; - // eslint-disable-next-line sonarjs/no-duplicated-branches - default: - return ( - - ); - } - })} -
-
+
+
+
+ {isSettingsOpen && ( + + )} +
+
); } QuickFilters.defaultProps = { onFilterChange: null, + signal: '', + config: [], }; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/AddedFilters.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/AddedFilters.tsx new file mode 100644 index 0000000000..e2dbd68f6b --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/AddedFilters.tsx @@ -0,0 +1,147 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import { + closestCenter, + DndContext, + DragEndEvent, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Button } from 'antd'; +import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { GripVertical } from 'lucide-react'; +import { useMemo } from 'react'; +import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; + +function SortableFilter({ + filter, + onRemove, + allowDrag, + allowRemove, +}: { + filter: FilterType; + onRemove: (filter: FilterType) => void; + allowDrag: boolean; + allowRemove: boolean; +}): JSX.Element { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: filter.key }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+
+ {allowDrag && } + {filter.key} +
+ {allowRemove && ( + + )} +
+ ); +} + +function AddedFilters({ + inputValue, + addedFilters, + setAddedFilters, +}: { + inputValue: string; + addedFilters: FilterType[]; + setAddedFilters: React.Dispatch>; +}): JSX.Element { + const sensors = useSensors(useSensor(PointerSensor)); + + const handleDragEnd = (event: DragEndEvent): void => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setAddedFilters((items) => { + const oldIndex = items.findIndex((item) => item.key === active.id); + const newIndex = items.findIndex((item) => item.key === over.id); + + return arrayMove(items, oldIndex, newIndex); + }); + } + }; + + const filteredAddedFilters = useMemo( + () => + addedFilters.filter((filter) => + filter.key.toLowerCase().includes(inputValue.toLowerCase()), + ), + [addedFilters, inputValue], + ); + + const handleRemoveFilter = (filter: FilterType): void => { + setAddedFilters((prev) => prev.filter((f) => f.key !== filter.key)); + }; + + const allowDrag = inputValue.length === 0; + const allowRemove = addedFilters.length > 1; + + return ( +
+
ADDED FILTERS
+
+ + + {filteredAddedFilters.length === 0 ? ( +
No values found
+ ) : ( + f.key)} + strategy={verticalListSortingStrategy} + disabled={!allowDrag} + > + {filteredAddedFilters.map((filter) => ( + + ))} + + )} +
+
+
+
+ ); +} + +export default AddedFilters; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/AnnouncementTooltip/AnnouncementTooltip.styles.scss b/frontend/src/components/QuickFilters/QuickFiltersSettings/AnnouncementTooltip/AnnouncementTooltip.styles.scss new file mode 100644 index 0000000000..c615e02778 --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/AnnouncementTooltip/AnnouncementTooltip.styles.scss @@ -0,0 +1,56 @@ +.announcement-tooltip { + &__dot { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--bg-robin-500); + z-index: 1000; + } + + &__title { + font-weight: 500; + font-size: 15px; + } + + &__container { + position: absolute; + width: 320px; + background-color: var(--bg-robin-500); + color: var(--text-white); + border-radius: 8px; + padding: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__close-icon { + cursor: pointer; + color: var(--text-white); + } + + &__message { + margin: 12px 0; + line-height: 20px; + } + + &__footer { + display: flex; + justify-content: flex-end; + } + + &__button { + background: var(--bg-vanilla-100); + color: var(--bg-robin-500); + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + } +} diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/AnnouncementTooltip/index.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/AnnouncementTooltip/index.tsx new file mode 100644 index 0000000000..18f965eb3f --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/AnnouncementTooltip/index.tsx @@ -0,0 +1,79 @@ +import './AnnouncementTooltip.styles.scss'; + +import { Button, Typography } from 'antd'; +import classNames from 'classnames'; +import { X } from 'lucide-react'; +import { useState } from 'react'; + +type AnnouncementTooltipProps = { + position: { top: number; left: number }; + title: string; + message: string; + show?: boolean; + className?: string; + onClose?: () => void; +}; + +// TEMPORARY HACK FOR ANNOUNCEMENTS: To be removed once proper system in place. +function AnnouncementTooltip({ + position, + show, + title, + message, + className, + onClose, +}: AnnouncementTooltipProps): JSX.Element | null { + const [visible, setVisible] = useState(show); + + const closeTooltip = (): void => { + setVisible(false); + onClose?.(); + }; + + return visible ? ( + <> + {/* Dot */} +
+ + {/* Tooltip box */} +
+
+ + {title} + + +
+

{message}

+
+ +
+
+ + ) : null; +} + +AnnouncementTooltip.defaultProps = { + show: false, + className: '', + onClose: (): void => {}, +}; + +export default AnnouncementTooltip; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx new file mode 100644 index 0000000000..05c9954295 --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/OtherFilters.tsx @@ -0,0 +1,104 @@ +import { Button, Skeleton } from 'antd'; +import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants'; +import { SignalType } from 'components/QuickFilters/types'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions'; +import { useMemo } from 'react'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; + +function OtherFiltersSkeleton(): JSX.Element { + return ( + <> + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + + ); +} + +function OtherFilters({ + signal, + inputValue, + addedFilters, + setAddedFilters, +}: { + signal: SignalType | undefined; + inputValue: string; + addedFilters: FilterType[]; + setAddedFilters: React.Dispatch>; +}): JSX.Element { + const { + data: suggestionsData, + isFetching: isFetchingSuggestions, + } = useGetAttributeSuggestions( + { + searchText: inputValue, + dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType], + filters: {} as TagFilter, + }, + { + queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue], + enabled: !!signal, + }, + ); + + const otherFilters = useMemo( + () => + suggestionsData?.payload?.attributes?.filter( + (attr) => !addedFilters.some((filter) => filter.key === attr.key), + ), + [suggestionsData, addedFilters], + ); + + const handleAddFilter = (filter: FilterType): void => { + setAddedFilters((prev) => [ + ...prev, + { + key: filter.key, + dataType: filter.dataType, + isColumn: filter.isColumn, + isJSON: filter.isJSON, + type: filter.type, + }, + ]); + }; + + const renderFilters = (): React.ReactNode => { + if (isFetchingSuggestions) return ; + if (!otherFilters?.length) + return
No values found
; + + return otherFilters.map((filter) => ( +
+
{filter.key}
+ +
+ )); + }; + + return ( +
+
OTHER FILTERS
+
+ + <>{renderFilters()} + +
+
+ ); +} + +export default OtherFilters; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss new file mode 100644 index 0000000000..6fa7054344 --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.styles.scss @@ -0,0 +1,190 @@ +.quick-filters-settings { + display: flex; + flex-direction: column; + position: absolute; + z-index: 999; + width: 342px; + background: var(--bg-slate-500); + transition: width 0.05s ease-in-out; + overflow: hidden; + + &.qf-logs-explorer { + height: calc(100vh - 45px); + } + + &.qf-exceptions { + height: 100vh; + } + + &.hidden { + width: 0; + } + .qf-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10.5px; + + .qf-title { + display: flex; + align-items: center; + gap: 12px; + } + + .qf-header-icon { + width: 16px; + height: 16px; + cursor: pointer; + } + } + + .qf-filters { + &.added-filters { + max-height: 40%; + } + &.other-filters { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + } + } + + .search { + .ant-input { + background-color: var(--bg-slate-500); + height: 46px; + border-radius: 0; + } + } + + .qf-other-filters-list { + .ant-skeleton-input { + width: 300px; + margin: 8px 12px; + } + } + + .qf-footer { + display: flex; + gap: 12px; + padding: 12px; + border-top: 1px solid var(--bg-slate-400); + button { + display: flex; + align-items: center; + justify-content: center; + width: 50%; + + .ant-btn-icon { + margin: 3px !important; + } + } + } +} + +//ADDED FILTERS AND OTHER FILTERS COMMON STYLES +.qf-filters { + display: flex; + flex-direction: column; + .qf-filters-header { + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; + letter-spacing: 0.88px; + text-transform: uppercase; + padding: 8px 12px; + } +} + +.no-values-found { + padding: 8px; + text-align: center; +} + +.qf-added-filters-list { + margin-bottom: 12px; + overflow: hidden; +} + +.qf-other-filters-list { + margin-bottom: 12px; + overflow: hidden; +} + +.qf-filter-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px; + border-radius: 4px; + user-select: none; + transition: background-color 0.2s ease-in-out; + + .drag-handle { + display: flex; + align-items: center; + gap: 8px; + flex-grow: 1; + } + + .qf-filter-content { + display: flex; + align-items: center; + gap: 8px; + } + + &.other-filters-item { + padding: 8px 12px; + height: 32px; + } + + &.drag-enabled { + cursor: grab; + &:active { + cursor: grabbing; + } + } + + &.drag-disabled { + padding: 8px 12px; + } + + .remove-filter-btn, + .add-filter-btn { + padding: 6px 12px; + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + + &:hover { + background-color: var(--bg-slate-400); + .remove-filter-btn, + .add-filter-btn { + opacity: 1; + } + } +} + +.lightMode { + .quick-filters-settings { + background: var(--bg-vanilla-100); + .search { + .ant-input { + background-color: var(--bg-vanilla-100); + } + } + .qf-footer { + border-top: 1px solid var(--bg-vanilla-300); + } + } + + .qf-filter-item { + &:hover { + background-color: var(--bg-vanilla-200); + } + } +} diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx new file mode 100644 index 0000000000..aa78d26107 --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx @@ -0,0 +1,109 @@ +import './QuickFiltersSettings.styles.scss'; + +import { Button, Input } from 'antd'; +import { CheckIcon, TableColumnsSplit, XIcon } from 'lucide-react'; +import { useMemo } from 'react'; +import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; + +import { SignalType } from '../types'; +import AddedFilters from './AddedFilters'; +import useQuickFilterSettings from './hooks/useQuickFilterSettings'; +import OtherFilters from './OtherFilters'; + +function QuickFiltersSettings({ + signal, + setIsSettingsOpen, + customFilters, + setIsStale, +}: { + signal: SignalType | undefined; + setIsSettingsOpen: (isSettingsOpen: boolean) => void; + customFilters: FilterType[]; + setIsStale: (isStale: boolean) => void; +}): JSX.Element { + const { + handleSettingsClose, + handleDiscardChanges, + addedFilters, + setAddedFilters, + handleSaveChanges, + isUpdatingCustomFilters, + inputValue, + handleInputChange, + debouncedInputValue, + } = useQuickFilterSettings({ + setIsSettingsOpen, + customFilters, + setIsStale, + signal, + }); + + const hasUnsavedChanges = useMemo( + () => + // check if both arrays have the same length and same order of elements + !( + addedFilters.length === customFilters.length && + addedFilters.every( + (filter, index) => filter.key === customFilters[index].key, + ) + ), + [addedFilters, customFilters], + ); + + return ( + <> +
+
+ + Edit quick filters +
+ +
+
+ +
+ + + {hasUnsavedChanges && ( +
+ + +
+ )} + + ); +} + +export default QuickFiltersSettings; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts b/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts new file mode 100644 index 0000000000..24ba1454d7 --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts @@ -0,0 +1,9 @@ +import { SignalType } from 'components/QuickFilters/types'; +import { DataSource } from 'types/common/queryBuilder'; + +export const SIGNAL_DATA_SOURCE_MAP = { + [SignalType.LOGS]: DataSource.LOGS, + [SignalType.TRACES]: DataSource.TRACES, + [SignalType.EXCEPTIONS]: DataSource.TRACES, + [SignalType.API_MONITORING]: DataSource.TRACES, +}; diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx b/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx new file mode 100644 index 0000000000..f365ddba06 --- /dev/null +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/hooks/useQuickFilterSettings.tsx @@ -0,0 +1,111 @@ +import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters'; +import axios, { AxiosError } from 'axios'; +import { SignalType } from 'components/QuickFilters/types'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { useNotifications } from 'hooks/useNotifications'; +import { useCallback, useState } from 'react'; +import { useMutation } from 'react-query'; +import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; + +interface UseQuickFilterSettingsProps { + setIsSettingsOpen: (isSettingsOpen: boolean) => void; + customFilters: FilterType[]; + setIsStale: (isStale: boolean) => void; + signal?: SignalType; +} + +interface UseQuickFilterSettingsReturn { + addedFilters: FilterType[]; + setAddedFilters: React.Dispatch>; + handleSettingsClose: () => void; + handleDiscardChanges: () => void; + handleSaveChanges: () => void; + isUpdatingCustomFilters: boolean; + inputValue: string; + setInputValue: React.Dispatch>; + debouncedInputValue: string; + handleInputChange: (e: React.ChangeEvent) => void; +} + +const useQuickFilterSettings = ({ + customFilters, + setIsSettingsOpen, + setIsStale, + signal, +}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => { + const [inputValue, setInputValue] = useState(''); + const [debouncedInputValue, setDebouncedInputValue] = useState(''); + const [addedFilters, setAddedFilters] = useState(customFilters); + const { notifications } = useNotifications(); + + const { + mutate: updateCustomFilters, + isLoading: isUpdatingCustomFilters, + } = useMutation(updateCustomFiltersAPI, { + onSuccess: () => { + setIsSettingsOpen(false); + setIsStale(true); + notifications.success({ + message: 'Quick filters updated successfully', + placement: 'bottomRight', + }); + }, + onError: (error: AxiosError) => { + notifications.error({ + message: axios.isAxiosError(error) ? error.message : SOMETHING_WENT_WRONG, + placement: 'bottomRight', + }); + }, + }); + const debouncedUpdate = useDebouncedFn((value) => { + setDebouncedInputValue(value as string); + }, 400); + + const handleInputChange = useCallback( + (e: React.ChangeEvent): void => { + const value = e.target.value.trim().toLowerCase(); + setInputValue(value); + debouncedUpdate(value); + }, + [debouncedUpdate], + ); + + const handleSettingsClose = useCallback((): void => { + setIsSettingsOpen(false); + }, [setIsSettingsOpen]); + + const handleDiscardChanges = useCallback((): void => { + setAddedFilters(customFilters); + }, [customFilters, setAddedFilters]); + + const handleSaveChanges = useCallback((): void => { + if (signal) { + updateCustomFilters({ + data: { + filters: addedFilters.map((filter) => ({ + key: filter.key, + datatype: filter.dataType, + type: filter.type, + })), + signal, + }, + }); + } + }, [addedFilters, signal, updateCustomFilters]); + + return { + handleSettingsClose, + handleDiscardChanges, + addedFilters, + setAddedFilters, + handleSaveChanges, + isUpdatingCustomFilters, + inputValue, + setInputValue, + debouncedInputValue, + handleInputChange, + }; +}; + +export default useQuickFilterSettings; diff --git a/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx b/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx new file mode 100644 index 0000000000..2a095d6685 --- /dev/null +++ b/frontend/src/components/QuickFilters/hooks/useFilterConfig.tsx @@ -0,0 +1,67 @@ +import getCustomFilters from 'api/quickFilters/getCustomFilters'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useMemo, useState } from 'react'; +import { useQuery } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + Filter as FilterType, + PayloadProps, +} from 'types/api/quickFilters/getCustomFilters'; + +import { IQuickFiltersConfig, SignalType } from '../types'; +import { getFilterConfig } from '../utils'; + +interface UseFilterConfigProps { + signal?: SignalType; + config: IQuickFiltersConfig[]; +} +interface UseFilterConfigReturn { + filterConfig: IQuickFiltersConfig[]; + customFilters: FilterType[]; + setCustomFilters: React.Dispatch>; + isCustomFiltersLoading: boolean; + isDynamicFilters: boolean; + setIsStale: React.Dispatch>; +} + +const useFilterConfig = ({ + signal, + config, +}: UseFilterConfigProps): UseFilterConfigReturn => { + const [customFilters, setCustomFilters] = useState([]); + const [isStale, setIsStale] = useState(true); + const isDynamicFilters = useMemo(() => customFilters.length > 0, [ + customFilters, + ]); + const { isLoading: isCustomFiltersLoading } = useQuery< + SuccessResponse | ErrorResponse, + Error + >( + [REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal], + () => getCustomFilters({ signal: signal || '' }), + { + onSuccess: (data) => { + if ('payload' in data && data.payload?.filters) { + setCustomFilters(data.payload.filters || ([] as FilterType[])); + } + setIsStale(false); + }, + enabled: !!signal && isStale, + }, + ); + const filterConfig = useMemo(() => getFilterConfig(customFilters, config), [ + config, + customFilters, + ]); + + return { + filterConfig, + customFilters, + setCustomFilters, + isCustomFiltersLoading, + isDynamicFilters, + setIsStale, + }; +}; + +export default useFilterConfig; diff --git a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx index 4aa026f60c..069d52d6eb 100644 --- a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx +++ b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx @@ -1,111 +1,288 @@ import '@testing-library/jest-dom'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { ENVIRONMENT } from 'constants/env'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { + otherFiltersResponse, + quickFiltersAttributeValuesResponse, + quickFiltersListResponse, +} from 'mocks-server/__mockdata__/customQuickFilters'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import QuickFilters from '../QuickFilters'; -import { QuickFiltersSource } from '../types'; +import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types'; import { QuickFiltersConfig } from './constants'; -// Mock the useQueryBuilder hook jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({ useQueryBuilder: jest.fn(), })); -// Mock the useGetAggregateValues hook -jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({ - useGetAggregateValues: jest.fn(), -})); const handleFilterVisibilityChange = jest.fn(); const redirectWithQueryBuilderData = jest.fn(); +const putHandler = jest.fn(); -function TestQuickFilters(): JSX.Element { +const BASE_URL = ENVIRONMENT.baseURL; +const SIGNAL = SignalType.LOGS; +const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`; +const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`; +const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`; +const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`; + +const FILTER_OS_DESCRIPTION = 'os.description'; +const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name'; +const ADDED_FILTERS_LABEL = /ADDED FILTERS/i; +const OTHER_FILTERS_LABEL = /OTHER FILTERS/i; +const SAVE_CHANGES_TEXT = 'Save changes'; +const DISCARD_TEXT = 'Discard'; +const FILTER_SERVICE_NAME = 'Service Name'; +const SETTINGS_ICON_TEST_ID = 'settings-icon'; +const QUERY_NAME = 'Test Query'; + +const setupServer = (): void => { + server.use( + rest.get(quickFiltersListURL, (_, res, ctx) => + res(ctx.status(200), ctx.json(quickFiltersListResponse)), + ), + rest.get(quickFiltersSuggestionsURL, (_, res, ctx) => + res(ctx.status(200), ctx.json(otherFiltersResponse)), + ), + rest.put(saveQuickFiltersURL, async (req, res, ctx) => { + putHandler(await req.json()); + return res(ctx.status(200), ctx.json({})); + }), + rest.get(quickFiltersAttributeValuesURL, (_, res, ctx) => + res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)), + ), + ); +}; + +function TestQuickFilters({ + signal = SignalType.LOGS, + config = QuickFiltersConfig, +}: { + signal?: SignalType; + config?: IQuickFiltersConfig[]; +}): JSX.Element { return ( ); } -describe('Quick Filters', () => { - beforeEach(() => { - // Provide a mock implementation for useQueryBuilder - (useQueryBuilder as jest.Mock).mockReturnValue({ - currentQuery: { - builder: { - queryData: [ - { - queryName: 'Test Query', - filters: { items: [{ key: 'test', value: 'value' }] }, - }, - ], - }, +TestQuickFilters.defaultProps = { + signal: '', + config: QuickFiltersConfig, +}; + +beforeAll(() => { + server.listen(); +}); + +afterEach(() => { + server.resetHandlers(); +}); + +afterAll(() => { + server.close(); + cleanup(); +}); + +beforeEach(() => { + (useQueryBuilder as jest.Mock).mockReturnValue({ + currentQuery: { + builder: { + queryData: [ + { + queryName: QUERY_NAME, + filters: { items: [{ key: 'test', value: 'value' }] }, + }, + ], }, - lastUsedQuery: 0, - redirectWithQueryBuilderData, - }); - - // Provide a mock implementation for useGetAggregateValues - (useGetAggregateValues as jest.Mock).mockReturnValue({ - data: { - statusCode: 200, - error: null, - message: 'success', - payload: { - stringAttributeValues: [ - 'mq-kafka', - 'otel-demo', - 'otlp-python', - 'sample-flask', - ], - numberAttributeValues: null, - boolAttributeValues: null, - }, - }, // Mocked API response - isLoading: false, - }); - }); - - it('renders correctly with default props', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + }, + lastUsedQuery: 0, + redirectWithQueryBuilderData, }); + setupServer(); +}); +describe('Quick Filters', () => { it('displays the correct query name in the header', () => { render(); expect(screen.getByText('Filters for')).toBeInTheDocument(); - expect(screen.getByText('Test Query')).toBeInTheDocument(); + expect(screen.getByText(QUERY_NAME)).toBeInTheDocument(); }); - it('should add filter data to query when checkbox is clicked', () => { + it('should add filter data to query when checkbox is clicked', async () => { render(); const checkbox = screen.getByText('mq-kafka'); fireEvent.click(checkbox); - expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( - expect.objectContaining({ - builder: { - queryData: expect.arrayContaining([ - expect.objectContaining({ - filters: expect.objectContaining({ - items: expect.arrayContaining([ - expect.objectContaining({ - key: expect.objectContaining({ - key: 'deployment.environment', + + await waitFor(() => { + expect(redirectWithQueryBuilderData).toHaveBeenCalledWith( + expect.objectContaining({ + builder: { + queryData: expect.arrayContaining([ + expect.objectContaining({ + filters: expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + key: expect.objectContaining({ + key: 'deployment.environment', + }), + value: 'mq-kafka', }), - value: 'mq-kafka', - }), - ]), + ]), + }), }), - }), - ]), - }, - }), - ); // sets composite query param + ]), + }, + }), + ); + }); + }); +}); + +describe('Quick Filters with custom filters', () => { + it('loads the custom filters correctly', async () => { + render(); + expect(screen.getByText('Filters for')).toBeInTheDocument(); + expect(screen.getByText(QUERY_NAME)).toBeInTheDocument(); + await screen.findByText(FILTER_SERVICE_NAME); + await screen.findByText('otel-demo'); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + fireEvent.click(icon); + + expect(await screen.findByText('Edit quick filters')).toBeInTheDocument(); + + const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; + expect(addedSection).toContainElement( + await screen.findByText(FILTER_OS_DESCRIPTION), + ); + + const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!; + expect(otherSection).toContainElement( + await screen.findByText(FILTER_K8S_DEPLOYMENT_NAME), + ); + }); + + it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => { + render(); + await screen.findByText(FILTER_SERVICE_NAME); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + fireEvent.click(icon); + + const otherFilterItem = await screen.findByText(FILTER_K8S_DEPLOYMENT_NAME); + const addButton = otherFilterItem.parentElement?.querySelector('button'); + expect(addButton).not.toBeNull(); + fireEvent.click(addButton as HTMLButtonElement); + + const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; + await waitFor(() => { + expect(addedSection).toHaveTextContent(FILTER_K8S_DEPLOYMENT_NAME); + }); + }); + + it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => { + render(); + await screen.findByText(FILTER_SERVICE_NAME); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + fireEvent.click(icon); + + const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; + const target = await screen.findByText(FILTER_OS_DESCRIPTION); + const removeBtn = target.parentElement?.querySelector('button'); + expect(removeBtn).not.toBeNull(); + fireEvent.click(removeBtn as HTMLButtonElement); + + await waitFor(() => { + expect(addedSection).not.toContainElement( + screen.getByText(FILTER_OS_DESCRIPTION), + ); + }); + + const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!; + expect(otherSection).toContainElement( + screen.getByText(FILTER_OS_DESCRIPTION), + ); + }); + + it('restores original filter state on Discard', async () => { + render(); + await screen.findByText(FILTER_SERVICE_NAME); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + fireEvent.click(icon); + + const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!; + const target = await screen.findByText(FILTER_OS_DESCRIPTION); + const removeBtn = target.parentElement?.querySelector('button'); + expect(removeBtn).not.toBeNull(); + fireEvent.click(removeBtn as HTMLButtonElement); + + const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!; + await waitFor(() => { + expect(addedSection).not.toContainElement( + screen.getByText(FILTER_OS_DESCRIPTION), + ); + expect(otherSection).toContainElement( + screen.getByText(FILTER_OS_DESCRIPTION), + ); + }); + + fireEvent.click(screen.getByText(DISCARD_TEXT)); + + await waitFor(() => { + expect(addedSection).toContainElement( + screen.getByText(FILTER_OS_DESCRIPTION), + ); + expect(otherSection).not.toContainElement( + screen.getByText(FILTER_OS_DESCRIPTION), + ); + }); + }); + + it('saves the updated filters by calling PUT with correct payload', async () => { + render(); + await screen.findByText(FILTER_SERVICE_NAME); + + const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID); + fireEvent.click(icon); + + const target = await screen.findByText(FILTER_OS_DESCRIPTION); + const removeBtn = target.parentElement?.querySelector('button'); + expect(removeBtn).not.toBeNull(); + fireEvent.click(removeBtn as HTMLButtonElement); + + fireEvent.click(screen.getByText(SAVE_CHANGES_TEXT)); + + await waitFor(() => { + expect(putHandler).toHaveBeenCalled(); + }); + + const requestBody = putHandler.mock.calls[0][0]; + expect(requestBody.filters).toEqual( + expect.arrayContaining([ + expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }), + ]), + ); + expect(requestBody.signal).toBe(SIGNAL); }); }); diff --git a/frontend/src/components/QuickFilters/tests/__snapshots__/QuickFilters.test.tsx.snap b/frontend/src/components/QuickFilters/tests/__snapshots__/QuickFilters.test.tsx.snap deleted file mode 100644 index 9ac623e325..0000000000 --- a/frontend/src/components/QuickFilters/tests/__snapshots__/QuickFilters.test.tsx.snap +++ /dev/null @@ -1,384 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Quick Filters renders correctly with default props 1`] = ` -
-
-
-
- - - - - Filters for - - - Test Query - -
-
- - - -
- - - -
-
-
-
-
-
-
-
- - - Environment - -
-
- - Clear All - -
-
- -
-
- -
- - mq-kafka - - - -
-
-
- -
- - otel-demo - - - -
-
-
- -
- - otlp-python - - - -
-
-
- -
- - sample-flask - - - -
-
-
-
-
-
-
- - - Service Name - -
-
-
-
-
-
-
-
-
-`; diff --git a/frontend/src/components/QuickFilters/types.ts b/frontend/src/components/QuickFilters/types.ts index fee95b10cb..e39daf232d 100644 --- a/frontend/src/components/QuickFilters/types.ts +++ b/frontend/src/components/QuickFilters/types.ts @@ -17,6 +17,13 @@ export enum SpecficFilterOperations { ONLY = 'ONLY', } +export enum SignalType { + TRACES = 'traces', + LOGS = 'logs', + API_MONITORING = 'api_monitoring', + EXCEPTIONS = 'exceptions', +} + export interface IQuickFiltersConfig { type: FiltersType; title: string; @@ -33,6 +40,8 @@ export interface IQuickFiltersProps { handleFilterVisibilityChange: () => void; source: QuickFiltersSource; onFilterChange?: (query: Query) => void; + signal?: SignalType; + className?: string; } export enum QuickFiltersSource { diff --git a/frontend/src/components/QuickFilters/utils.tsx b/frontend/src/components/QuickFilters/utils.tsx new file mode 100644 index 0000000000..4d1ed5ffa5 --- /dev/null +++ b/frontend/src/components/QuickFilters/utils.tsx @@ -0,0 +1,39 @@ +import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters'; + +import { FiltersType, IQuickFiltersConfig } from './types'; + +const getFilterName = (str: string): string => + // replace . and _ with space + // capitalize the first letter of each word + str + .replace(/\./g, ' ') + .replace(/_/g, ' ') + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + +export const getFilterConfig = ( + customFilters?: FilterType[], + config?: IQuickFiltersConfig[], +): IQuickFiltersConfig[] => { + if (!customFilters?.length) { + return config || []; + } + + return customFilters.map( + (att, index) => + ({ + type: FiltersType.CHECKBOX, + title: getFilterName(att.key), + attributeKey: { + id: att.key, + key: att.key, + dataType: att.dataType, + type: att.type, + isColumn: att.isColumn, + isJSON: att.isJSON, + }, + defaultOpen: index === 0, + } as IQuickFiltersConfig), + ); +}; diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 7cf51ccb11..eccdc175ba 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -28,4 +28,5 @@ export enum LOCALSTORAGE { DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING', METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS', SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS', + QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT', } diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index c8437c8cb5..51fba40c47 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -78,4 +78,8 @@ export const REACT_QUERY_KEY = { GET_FUNNEL_SLOW_TRACES: 'GET_FUNNEL_SLOW_TRACES', GET_FUNNEL_ERROR_TRACES: 'GET_FUNNEL_ERROR_TRACES', GET_FUNNEL_STEPS_GRAPH_DATA: 'GET_FUNNEL_STEPS_GRAPH_DATA', + + // Quick Filters Query Keys + GET_CUSTOM_FILTERS: 'GET_CUSTOM_FILTERS', + GET_OTHER_FILTERS: 'GET_OTHER_FILTERS', } as const; diff --git a/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts b/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts new file mode 100644 index 0000000000..3bc5a15e53 --- /dev/null +++ b/frontend/src/mocks-server/__mockdata__/customQuickFilters.ts @@ -0,0 +1,163 @@ +export const quickFiltersListResponse = { + status: 'success', + data: { + signal: 'logs', + filters: [ + { + key: 'os.description', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'service.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'quantity', + dataType: 'float64', + type: 'tag', + isColumn: false, + isJSON: false, + }, + { + key: 'body', + dataType: 'string', + type: '', + isColumn: false, + isJSON: false, + }, + { + key: 'deployment.environment', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'service.namespace', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'k8s.namespace.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'service.instance.id', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'k8s.pod.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'process.owner', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + ], + }, +}; + +export const otherFiltersResponse = { + status: 'success', + data: { + attributes: [ + { + key: 'service.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'k8s.deployment.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'deployment.environment', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'service.namespace', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'k8s.namespace.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'service.instance.id', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'k8s.pod.name', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'k8s.pod.uid', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + { + key: 'os.description', + dataType: 'string', + type: 'resource', + isColumn: false, + isJSON: false, + }, + ], + }, +}; + +export const quickFiltersAttributeValuesResponse = { + status: 'success', + data: { + stringAttributeValues: [ + 'mq-kafka', + 'otel-demo', + 'otlp-python', + 'sample-flask', + ], + numberAttributeValues: null, + boolAttributeValues: null, + }, +}; diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index b988a43b5f..e1ab02e369 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -6,7 +6,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set'; import cx from 'classnames'; import ExplorerCard from 'components/ExplorerCard/ExplorerCard'; import QuickFilters from 'components/QuickFilters/QuickFilters'; -import { QuickFiltersSource } from 'components/QuickFilters/types'; +import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types'; import { LOCALSTORAGE } from 'constants/localStorage'; import LogExplorerQuerySection from 'container/LogExplorerQuerySection'; import LogsExplorerViews from 'container/LogsExplorerViews'; @@ -28,7 +28,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe import { DataSource } from 'types/common/queryBuilder'; import { WrapperStyled } from './styles'; -import { LogsQuickFiltersConfig, SELECTED_VIEWS } from './utils'; +import { SELECTED_VIEWS } from './utils'; function LogsExplorer(): JSX.Element { const [showFrequencyChart, setShowFrequencyChart] = useState(true); @@ -215,8 +215,9 @@ function LogsExplorer(): JSX.Element { {showFilters && (
diff --git a/frontend/src/types/api/quickFilters/getCustomFilters.ts b/frontend/src/types/api/quickFilters/getCustomFilters.ts new file mode 100644 index 0000000000..3b9a9e68d2 --- /dev/null +++ b/frontend/src/types/api/quickFilters/getCustomFilters.ts @@ -0,0 +1,16 @@ +export interface Filter { + key: string; + dataType: string; + type: string; + isColumn: boolean; + isJSON: boolean; +} + +export interface Props { + signal: string; +} + +export type PayloadProps = { + filters: Filter[]; + signal: string; +}; diff --git a/frontend/src/types/api/quickFilters/updateCustomFilters.ts b/frontend/src/types/api/quickFilters/updateCustomFilters.ts new file mode 100644 index 0000000000..6960500fbe --- /dev/null +++ b/frontend/src/types/api/quickFilters/updateCustomFilters.ts @@ -0,0 +1,14 @@ +import { SignalType } from 'components/QuickFilters/types'; + +interface FilterType { + key: string; + datatype: string; + type: string; +} + +export interface UpdateCustomFiltersProps { + data: { + filters: FilterType[]; + signal: SignalType; + }; +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 37613f1a81..fa585d0cd2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2492,9 +2492,9 @@ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@dnd-kit/accessibility@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz#1054e19be276b5f1154ced7947fc0cb5d99192e0" - integrity sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ== + version "3.1.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af" + integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw== dependencies: tslib "^2.0.0" @@ -2523,7 +2523,7 @@ "@dnd-kit/utilities" "^3.2.2" tslib "^2.0.0" -"@dnd-kit/utilities@^3.2.2": +"@dnd-kit/utilities@3.2.2", "@dnd-kit/utilities@^3.2.2": version "3.2.2" resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b" integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==