Compare commits

...

2 Commits

Author SHA1 Message Date
Aditya Singh
05cdaff264 chore: handle save and discard 2025-05-10 20:26:20 +05:30
Aditya Singh
062fa4bad0 chore: added filters init 2025-05-08 15:21:56 +05:30
15 changed files with 664 additions and 71 deletions

View File

@@ -32,9 +32,10 @@
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/sortable": "10.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",

View File

@@ -1,7 +1,17 @@
.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);
@@ -52,7 +62,7 @@
.right-actions {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
width: 100%;
justify-content: flex-end;
@@ -67,6 +77,22 @@
border: 0;
box-shadow: none;
}
.right-action-icon-container {
display: flex;
padding: 2px;
.settings-icon {
height: 14px;
width: 14px;
cursor: pointer;
}
&:hover {
background: var(--bg-slate-500);
}
&.active {
background: var(--bg-slate-500);
}
}
}
}
}

View File

@@ -6,17 +6,36 @@ import {
VerticalAlignTopOutlined,
} from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import classNames from 'classnames';
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction } from 'lodash-es';
import { Settings2 as SettingsIcon } from 'lucide-react';
import { 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 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 {
config,
handleFilterVisibilityChange,
source,
onFilterChange,
signal,
} = props;
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const {
filterConfig,
isDynamicFilters,
customFilters,
setIsStale,
} = useFilterConfig({ signal, config });
const {
currentQuery,
@@ -63,64 +82,99 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
return (
<div className="quick-filters">
{source !== QuickFiltersSource.INFRA_MONITORING &&
source !== QuickFiltersSource.API_MONITORING && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">
{lastQueryName ? 'Filters for' : 'Filters'}
</Typography.Text>
{lastQueryName && (
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
<div className="quick-filters-container">
<div className="quick-filters">
{source !== QuickFiltersSource.INFRA_MONITORING &&
source !== QuickFiltersSource.API_MONITORING && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">
{lastQueryName ? 'Filters for' : 'Filters'}
</Typography.Text>
{lastQueryName && (
<Tooltip
title={`Filter currently in sync with query ${lastQueryName}`}
>
<Typography.Text className="sync-tag">
{lastQueryName}
</Typography.Text>
</Tooltip>
)}
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<div className="right-action-icon-container">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</div>
</Tooltip>
)}
<Tooltip title="Collapse Filters">
<div className="right-action-icon-container">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</div>
</Tooltip>
{isDynamicFilters && (
<Tooltip title="Settings">
<div
className={classNames('right-action-icon-container', {
active: isSettingsOpen,
})}
>
<SettingsIcon
className="settings-icon"
width={14}
height={14}
onClick={(): void => setIsSettingsOpen(true)}
/>
</div>
</Tooltip>
)}
</section>
</section>
)}
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
<TypicalOverlayScrollbar>
<section className="filters">
{filterConfig.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.SLIDER:
return <Slider filter={filter} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
}
})}
</section>
</TypicalOverlayScrollbar>
</div>
<div className="quick-filters-settings-container">
{isSettingsOpen && (
<QuickFiltersSettings
signal={signal}
setIsSettingsOpen={setIsSettingsOpen}
customFilters={customFilters}
setIsStale={setIsStale}
/>
)}
<TypicalOverlayScrollbar>
<section className="filters">
{config.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.SLIDER:
return <Slider filter={filter} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
}
})}
</section>
</TypicalOverlayScrollbar>
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
.qf-filter-item {
padding: 8px;
border-radius: 4px;
// cursor: grab;
user-select: none;
.qf-filter-content{
display: flex;
align-items: center;
gap: 8px;
}
// &:active {
// cursor: grabbing;
// }
&.drag-enabled {
cursor: grab;
&:active {
cursor: grabbing;
}
}
}

View File

@@ -0,0 +1,97 @@
/* eslint-disable react/jsx-props-no-spreading */
import './AddedFilters.styles.scss';
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 { GripVertical } from 'lucide-react';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
function SortableFilter({ filter }: { filter: FilterType }): JSX.Element {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: filter.key });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="qf-filter-item drag-enabled" // TODO: handle drag disabled when searching
>
<div className="qf-filter-content">
<GripVertical size={16} />
{filter.key}
</div>
</div>
);
}
function AddedFilters({
addedFilters,
setAddedFilters,
}: {
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
}): 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);
});
}
};
return (
<div className="qf-filters added-filters">
<div className="qf-filters-header">ADDED FILTERS</div>
<div className="qf-added-filters-list">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={addedFilters.map((f) => f.key)}
strategy={verticalListSortingStrategy}
>
{addedFilters.map((filter) => (
<SortableFilter key={filter.key} filter={filter} />
))}
</SortableContext>
</DndContext>
</div>
</div>
);
}
export default AddedFilters;

View File

@@ -0,0 +1,10 @@
function OtherFilters(): JSX.Element {
return (
<div className="qf-filters other-filters">
<div className="qf-filters-header">OTHER FILTERS</div>
<div className="qf-other-filters-list" />
</div>
);
}
export default OtherFilters;

View File

@@ -0,0 +1,75 @@
.quick-filters-settings {
position: absolute;
z-index: 999;
width: 342px;
height: 100%;
background: var(--bg-slate-500);
.qf-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10.5px;
border-bottom: 1px solid var(--bg-slate-400);
.qf-title {
display: flex;
align-items: center;
gap: 12px;
}
.qf-header-icon {
width: 16px;
height: 16px;
cursor: pointer;
}
}
.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;
}
}
}
}
// add light theme
// .light {
// .quick-filters-settings {
// background: var(--bg-slate-50);
// }
// }
//ADDED FILTERS AND OTHER FILTERS COMMON STYLES
.qf-filters {
display: flex;
flex-direction: column;
.qf-filters-header {
color: var(--bg-slate-50);
border-top: 1px solid var(--bg-slate-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
margin-top: 12px;
padding: 8px 12px;
}
}

View File

@@ -0,0 +1,92 @@
import './QuickFiltersSettings.styles.scss';
import Button from 'antd/es/button';
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,
} = 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 (
<div className="quick-filters-settings">
<div className="qf-header">
<div className="qf-title">
<TableColumnsSplit width={16} height={16} />
Edit quick filters
</div>
<XIcon
className="qf-header-icon"
width={16}
height={16}
onClick={handleSettingsClose}
/>
</div>
<AddedFilters
addedFilters={addedFilters}
setAddedFilters={setAddedFilters}
/>
<OtherFilters />
{hasUnsavedChanges && (
<div className="qf-footer">
<Button
type="default"
onClick={handleDiscardChanges}
icon={<XIcon width={16} height={16} />}
>
Discard
</Button>
<Button
type="primary"
onClick={handleSaveChanges}
icon={<CheckIcon width={16} height={16} />}
loading={isUpdatingCustomFilters}
>
Save changes
</Button>
</div>
)}
</div>
);
}
export default QuickFiltersSettings;

View File

@@ -0,0 +1,82 @@
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
import { AxiosError } from 'axios';
import { SignalType } from 'components/QuickFilters/types';
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<React.SetStateAction<FilterType[]>>;
handleSettingsClose: () => void;
handleDiscardChanges: () => void;
handleSaveChanges: () => void;
isUpdatingCustomFilters: boolean;
}
const useQuickFilterSettings = ({
customFilters,
setIsSettingsOpen,
setIsStale,
signal,
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
const [addedFilters, setAddedFilters] = useState<FilterType[]>(customFilters);
const {
mutate: updateCustomFilters,
isLoading: isUpdatingCustomFilters,
} = useMutation(updateCustomFiltersAPI, {
onSuccess: () => {
// set isStale to true
// close settings
setIsStale(true);
// display success toast
},
onError: (error: AxiosError) => {
console.error('>>>>error', error);
// display error toast
// close settings
},
});
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,
};
};
export default useQuickFilterSettings;

View File

@@ -0,0 +1,62 @@
import getCustomFilters from 'api/quickFilters/getCustomFilters';
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<React.SetStateAction<FilterType[]>>;
isCustomFiltersLoading: boolean;
isDynamicFilters: boolean;
setIsStale: React.Dispatch<React.SetStateAction<boolean>>;
}
const useFilterConfig = ({
signal,
config,
}: UseFilterConfigProps): UseFilterConfigReturn => {
const [customFilters, setCustomFilters] = useState<FilterType[]>([]);
const [isStale, setIsStale] = useState(true);
const isDynamicFilters = useMemo(() => customFilters.length > 0, [
customFilters,
]);
const { isLoading: isCustomFiltersLoading } = useQuery<
SuccessResponse<PayloadProps> | ErrorResponse,
Error
>(['addedFilters'], () => 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;

View File

@@ -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,7 @@ export interface IQuickFiltersProps {
handleFilterVisibilityChange: () => void;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
signal?: SignalType;
}
export enum QuickFiltersSource {

View File

@@ -0,0 +1,33 @@
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { FiltersType, IQuickFiltersConfig } from './types';
const getFilterName = (str: string): string =>
// replace . and _ with space
str.replace(/\./g, ' ').replace(/_/g, ' ');
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),
);
};

View File

@@ -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;
};

View File

@@ -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;
};
}

View File

@@ -2511,19 +2511,19 @@
resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz"
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==
"@dnd-kit/accessibility@^3.1.1":
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"
"@dnd-kit/core@6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.1.0.tgz#e81a3d10d9eca5d3b01cbf054171273a3fe01def"
integrity sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==
"@dnd-kit/core@6.3.1":
version "6.3.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003"
integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
dependencies:
"@dnd-kit/accessibility" "^3.1.0"
"@dnd-kit/accessibility" "^3.1.1"
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
@@ -2535,15 +2535,15 @@
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/sortable@8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-8.0.0.tgz#086b7ac6723d4618a4ccb6f0227406d8a8862a96"
integrity sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==
"@dnd-kit/sortable@10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8"
integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==
dependencies:
"@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==