Compare commits

..

2 Commits

Author SHA1 Message Date
Ashwin Bhatkal
23168dc322 fix(alerts): match the rule state filter to the created-by filter 2026-11-07 10:48:03 +05:30
Ashwin Bhatkal
819d66d94d refactor(list-filter): move the created-by select into a reusable multi-select filter 2026-11-07 00:50:14 +05:30
10 changed files with 145 additions and 73 deletions

View File

@@ -11,8 +11,6 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
// generated API Instance
const generatedAPIAxiosInstance = axios.create({
baseURL: ENVIRONMENT.baseURL,
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
paramsSerializer: { indexes: null },
});
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;

View File

@@ -1,18 +1,13 @@
import { ConfigProvider, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Select } from 'antd';
import cx from 'classnames';
import type { UpdatedWindow } from '../dsl/types';
import type { CreatorOption } from '../types';
import { UPDATED_OPTIONS } from './constants';
import MultiSelectFilter from './MultiSelectFilter';
import styles from './FilterZone.module.scss';
// The option checkbox already marks selection, so selected rows stay regular weight.
const CREATOR_SELECT_THEME = {
components: { Select: { optionSelectedFontWeight: 400 } },
};
interface Props {
createdBy: string[];
updated: UpdatedWindow;
@@ -52,36 +47,16 @@ function FilterChips({
return (
<div className={styles.chips}>
<ConfigProvider theme={CREATOR_SELECT_THEME}>
<Select
mode="multiple"
showSearch
allowClear
className={cx(styles.select, styles.selectWide)}
placeholder="Created by"
value={createdBy}
options={creatorOptionsData}
optionFilterProp="label"
maxTagCount={1}
menuItemSelectedIcon={null}
data-testid={`${testIdPrefix}-filter-created-by`}
disabled={disabled}
onClear={onClearCreatedBy}
optionRender={(option): JSX.Element => (
<div className={styles.creatorOption}>
<div className={styles.creatorCheck}>
<Checkbox
color="primary"
value={createdBy.includes(option.value as string)}
/>
</div>
<span className={styles.creatorLabel}>{option.label}</span>
</div>
)}
onChange={(value): void => onCreatedByChange(value as string[])}
onDropdownVisibleChange={runOnClose}
/>
</ConfigProvider>
<MultiSelectFilter
value={createdBy}
options={creatorOptionsData}
placeholder="Created by"
testId={`${testIdPrefix}-filter-created-by`}
disabled={disabled}
onClear={onClearCreatedBy}
onChange={onCreatedByChange}
onDropdownVisibleChange={runOnClose}
/>
<Select
showSearch
className={cx(styles.select, styles.selectNarrow)}

View File

@@ -86,21 +86,21 @@
}
}
// Created-by option: a presentational checkbox + label (antd handles the toggle).
.creatorOption {
// Multi-select option: a presentational checkbox + label (antd handles the toggle).
.checkOption {
display: flex;
align-items: center;
gap: 8px;
}
.creatorCheck {
.checkOptionBox {
// Flex, not block: a line box would shift with the check mark's baseline.
display: flex;
flex-shrink: 0;
pointer-events: none;
}
.creatorLabel {
.checkOptionLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -0,0 +1,72 @@
import { ConfigProvider, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import cx from 'classnames';
import styles from './FilterZone.module.scss';
// The option checkbox already marks selection, so selected rows stay regular weight.
const CHECKBOX_SELECT_THEME = {
components: { Select: { optionSelectedFontWeight: 400 } },
};
export interface MultiSelectFilterOption {
value: string;
label: string;
}
interface Props {
value: string[];
options: MultiSelectFilterOption[];
placeholder: string;
testId: string;
onChange: (values: string[]) => void;
onClear?: () => void;
onDropdownVisibleChange?: (open: boolean) => void;
disabled?: boolean;
}
function MultiSelectFilter({
value,
options,
placeholder,
testId,
onChange,
onClear,
onDropdownVisibleChange,
disabled = false,
}: Props): JSX.Element {
return (
<ConfigProvider theme={CHECKBOX_SELECT_THEME}>
<Select
mode="multiple"
showSearch
allowClear
className={cx(styles.select, styles.selectWide)}
placeholder={placeholder}
value={value}
options={options}
optionFilterProp="label"
maxTagCount={1}
menuItemSelectedIcon={null}
data-testid={testId}
disabled={disabled}
onClear={onClear}
optionRender={(option): JSX.Element => (
<div className={styles.checkOption}>
<div className={styles.checkOptionBox}>
<Checkbox
color="primary"
value={value.includes(option.value as string)}
/>
</div>
<span className={styles.checkOptionLabel}>{option.label}</span>
</div>
)}
onChange={(next): void => onChange(next as string[])}
onDropdownVisibleChange={onDropdownVisibleChange}
/>
</ConfigProvider>
);
}
export default MultiSelectFilter;

View File

@@ -135,9 +135,18 @@ describe('ListAlertRules — search', () => {
await screen.findByText('High CPU Alert');
await user.click(screen.getByTestId('alert-rules-filter-states'));
await user.click(await screen.findByRole('option', { name: 'Firing' }));
await user.click(await screen.findByRole('option', { name: 'Disabled' }));
await user.click(
within(screen.getByTestId('alert-rules-filter-states')).getByRole(
'combobox',
),
);
const dropdown = await waitFor(() => {
const element = document.querySelector<HTMLElement>('.ant-select-dropdown');
expect(element).not.toBeNull();
return element as HTMLElement;
});
await user.click(within(dropdown).getByText('Firing'));
await user.click(within(dropdown).getByText('Disabled'));
await waitFor(() => {
expect(lastRequest(requests).getAll('states')).toStrictEqual([

View File

@@ -1,4 +0,0 @@
.statesSelect {
width: 240px;
flex: none;
}

View File

@@ -1,10 +1,9 @@
import { useCallback } from 'react';
import { ComboboxSimple, type ComboboxSimpleItem } from '@signozhq/ui/combobox';
import { RuletypesAlertStateDTO } from 'api/generated/services/sigNoz.schemas';
import MultiSelectFilter, {
type MultiSelectFilterOption,
} from 'components/ListFilter/FilterZone/MultiSelectFilter';
import styles from './RuleStatesFilter.module.scss';
const STATE_ITEMS: ComboboxSimpleItem[] = [
const STATE_OPTIONS: MultiSelectFilterOption[] = [
{ value: RuletypesAlertStateDTO.firing, label: 'Firing' },
{ value: RuletypesAlertStateDTO.pending, label: 'Pending' },
{ value: RuletypesAlertStateDTO.recovering, label: 'Recovering' },
@@ -22,26 +21,13 @@ function RuleStatesFilter({
states,
onChange,
}: RuleStatesFilterProps): JSX.Element {
const handleChange = useCallback(
(values: unknown): void => {
if (Array.isArray(values)) {
onChange(values as string[]);
}
},
[onChange],
);
return (
<ComboboxSimple
className={styles.statesSelect}
multiple
<MultiSelectFilter
value={states}
onChange={handleChange}
placeholder="Filter by state"
inputPlaceholder="Select one or more"
items={STATE_ITEMS}
maxDisplayedPills={2}
options={STATE_OPTIONS}
placeholder="State"
testId="alert-rules-filter-states"
onChange={onChange}
/>
);
}

View File

@@ -3,11 +3,13 @@ import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRulesV3 } from 'api/generated/services/rules';
import type {
ListRulesV3200,
ListRulesV3Params,
RenderErrorResponseDTO,
RuletypesLabelPairDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { AxiosError } from 'axios';
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
import { isUndefined } from 'lodash-es';
import type { AlertRule } from './types';
@@ -33,7 +35,18 @@ export function useAlertRulesData(
const hasLoggedEvent = useRef(false);
const rulesResponse = useListRulesV3(params, {
query: { keepPreviousData: true },
query: {
keepPreviousData: true,
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
queryFn: ({ signal }) =>
GeneratedAPIInstance<ListRulesV3200>({
url: '/api/v3/rules',
method: 'GET',
params,
paramsSerializer: { indexes: null },
signal,
}),
},
});
const rules = useMemo(

View File

@@ -15,6 +15,7 @@ import {
openRenameViewPopover,
openRowActions,
openSaveViewPopover,
openStatesFilter,
toggleAlert,
} from './__story_mockdata__/alertRules.play';
@@ -179,6 +180,11 @@ export const ColumnPicker: Story = {
play: openColumnPicker,
};
/** The State dropdown, open with one state picked. */
export const StateFilterOpen: Story = {
play: openStatesFilter,
};
/**
* Every label badge in the Labels column, held open: each one repeats its own
* `key: value` with a copy button. The rules carry two labels apiece, which fit

View File

@@ -111,6 +111,23 @@ export const openColumnPicker = async ({
await screen.findByText(/toggle columns/i);
};
export const openStatesFilter = async ({
canvasElement,
}: PlayContext): Promise<void> => {
const filter = await within(canvasElement).findByTestId(
'alert-rules-filter-states',
undefined,
untilLoaded,
);
await userEvent.click(filter.querySelector('.ant-select-selector') ?? filter);
const option = (await screen.findAllByText('Firing')).find((node) =>
node.closest('.ant-select-dropdown'),
);
await userEvent.click(option ?? filter);
await screen.findByRole('checkbox', { checked: true });
};
export const toggleAlert = async ({
canvasElement,
args,