Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/datasources/alarms/AlarmsDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('AlarmsDataSource', () => {

it('should initialize with ListAlarms as the default query', () => {
expect(datastore.defaultQuery).toEqual({
outputType: 'Properties',
filter: '',
properties: ['displayName', 'currentSeverityLevel', 'occurredAt', 'source', 'state', 'workspace'],
take: 1000,
Expand Down
6 changes: 3 additions & 3 deletions src/datasources/alarms/components/AlarmsQueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AlarmsCountQueryEditor } from './editors/alarms-count/AlarmsCountQueryE
import { AlarmsCountQuery } from '../types/AlarmsCount.types';
import { InlineField } from 'core/components/InlineField';
import { CONTROL_WIDTH, LABEL_WIDTH, labels, tooltips } from '../constants/AlarmsQueryEditor.constants';
import { Combobox, Stack } from '@grafana/ui';
import { Combobox } from '@grafana/ui';
import { DEFAULT_QUERY_TYPE, defaultAlarmsCountQuery, defaultAlarmsTrendQuery, defaultListAlarmsQuery } from '../constants/DefaultQueries.constants';
import { ListAlarmsQuery } from '../types/ListAlarms.types';
import { ListAlarmsQueryEditor } from './editors/list-alarms/ListAlarmsQueryEditor';
Expand Down Expand Up @@ -82,7 +82,7 @@ export function AlarmsQueryEditor({ datasource, query, onChange, onRunQuery }: P
}, []);

return (
<Stack direction='column'>
<>
Copy link
Collaborator Author

@Ahalya-ni Ahalya-ni Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is required to have less gap between the query type and output UI controls. This will affect AlarmsTrend editor gap between the query type and its query builder. Hence added Space from the grafana/ui to add appropriate space between the controls in Alarms Trend editor.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add the screenshot of alarm trend editor so we can see the difference?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alarm Trend editor without Space
image

Alarm Trend editor with Space
image

<InlineField
label={labels.queryType}
labelWidth={LABEL_WIDTH}
Expand Down Expand Up @@ -118,6 +118,6 @@ export function AlarmsQueryEditor({ datasource, query, onChange, onRunQuery }: P
datasource={datasource.alarmsTrendQueryHandler}
/>
)}
</Stack>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ERROR_SEVERITY_WARNING, LABEL_WIDTH, labels, SECONDARY_LABEL_WIDTH, too
import { AlarmsTrendQueryHandler } from 'datasources/alarms/query-type-handlers/alarms-trend/AlarmsTrendQueryHandler';
import { Workspace } from 'core/types';
import { FloatingError } from 'core/errors';
import { InlineSwitch, Stack } from '@grafana/ui';
import { InlineSwitch, Space, Stack } from '@grafana/ui';

type Props = {
query: AlarmsTrendQuery;
Expand Down Expand Up @@ -43,6 +43,7 @@ export function AlarmsTrendQueryEditor({ query, handleQueryChange, datasource }:

return (
<>
<Space v={1} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this vertical space? I don't see this space in Assets

Copy link
Collaborator Author

@Ahalya-ni Ahalya-ni Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the Assets datasource, there's no gap between the Query type and Group by UI elements in Calibration Forecast. We have the same pattern in Alarms since both datasources have only one query type with an output control (List Assets in Assets datasource, List Alarms in Alarms datasource).

As I understand it, related controls are grouped together, while different functional sections are separated by gaps. In the Results datasource, we added a gap after the Output control, which works because both query types have output controls, maintaining consistency.

I've added a Space component to create a clear separation between the Query Type and query builder sections in the alarms trend workflow, following the mockup UI design specified in the HLD.

image

Leaving this comment open, to get @kartheeswaran-ni thoughts.

<Stack>
<InlineField
label={labels.queryBy}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { QueryType, TransitionInclusionOption } from 'datasources/alarms/types/types';
import { ListAlarmsQueryHandler } from 'datasources/alarms/query-type-handlers/list-alarms/ListAlarmsQueryHandler';
import { AlarmsSpecificProperties, AlarmsTransitionProperties, ListAlarmsQuery } from 'datasources/alarms/types/ListAlarms.types';
import { AlarmsSpecificProperties, AlarmsTransitionProperties, ListAlarmsQuery, OutputType } from 'datasources/alarms/types/ListAlarms.types';
import { ListAlarmsQueryEditor } from './ListAlarmsQueryEditor';
import userEvent from '@testing-library/user-event';
import { AlarmsPropertiesOptions, takeErrorMessages, AlarmsTransitionInclusionOptions } from 'datasources/alarms/constants/AlarmsQueryEditor.constants';
Expand All @@ -25,14 +25,19 @@ const defaultProps = {
query: {
refId: 'A',
queryType: QueryType.ListAlarms,
outputType: OutputType.Properties,
},
handleQueryChange: mockHandleQueryChange,
datasource: mockDatasource,
};

async function renderElement(query: ListAlarmsQuery = { ...defaultProps.query }) {
return await act(async () => {
const reactNode = React.createElement(ListAlarmsQueryEditor, { ...defaultProps, query });
const reactNode = React.createElement(ListAlarmsQueryEditor, {
query: { ...defaultProps.query, ...query },
handleQueryChange: defaultProps.handleQueryChange,
datasource: defaultProps.datasource,
});

return render(reactNode);
});
Expand Down Expand Up @@ -62,6 +67,13 @@ describe('ListAlarmsQueryEditor', () => {
expect(screen.getAllByText('Value').length).toBe(1);
});

it('should render outputType control', async () => {
await renderElement();

expect(screen.getByText('Output')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: OutputType.Properties })).toBeInTheDocument();
});

it('should call handleQueryChange when filter changes', async () => {
const container = await renderElement();
const queryBuilder = container.getByRole('dialog');
Expand Down Expand Up @@ -115,6 +127,52 @@ describe('ListAlarmsQueryEditor', () => {
expect(screen.getByText('Test Error Description')).toBeInTheDocument();
});

describe('Output Type', () => {
it('should call handleQueryChange with total count as output type when switched to TotalCount', async () => {
const container = await renderElement();
const totalCountRadio = container.getByRole('radio', { name: OutputType.TotalCount });

await userEvent.click(totalCountRadio);

expect(mockHandleQueryChange).toHaveBeenCalledWith(
expect.objectContaining({
outputType: OutputType.TotalCount,
})
);
});

it('should call handleQueryChange with properties as output type when switched to Properties', async () => {
const container = await renderElement({ refId: 'A', outputType: OutputType.TotalCount });
const propertiesRadio = container.getByRole('radio', { name: OutputType.Properties });

await userEvent.click(propertiesRadio);

expect(mockHandleQueryChange).toHaveBeenCalledWith(
expect.objectContaining({
outputType: OutputType.Properties,
})
);
});

it('should show properties, include transition, descending and take controls when output type is Properties', async () => {
const container = await renderElement({ refId: 'A', outputType: OutputType.Properties });

expect(container.getAllByRole('combobox').length).toBe(2);
expect(container.getByPlaceholderText('Select the properties to query')).toBeInTheDocument();
expect(container.getByRole('combobox', { name: 'Include Transition' })).toBeInTheDocument();
expect(container.getByRole('switch', { name: 'Descending' })).toBeInTheDocument();
expect(container.getByRole('spinbutton', { name: 'Take' })).toBeInTheDocument();
});

it('should hide properties, include transition, descending and take controls when output type is TotalCount', async () => {
const container = await renderElement({ refId: 'A', outputType: OutputType.TotalCount });

expect(container.queryAllByRole('combobox').length).toBe(0);
expect(container.queryByRole('switch', { name: 'Descending' })).not.toBeInTheDocument();
expect(container.queryByRole('spinbutton', { name: 'Take' })).not.toBeInTheDocument();
});
});

describe('Properties', () => {
it('should render the selected alarm properties in the UI', async () => {
await renderElement({ refId: 'A', queryType: QueryType.ListAlarms, properties: [AlarmsSpecificProperties.acknowledged] });
Expand Down Expand Up @@ -172,10 +230,12 @@ describe('ListAlarmsQueryEditor', () => {
fireEvent.change(takeInput, { target: { value: '250' } });
fireEvent.blur(takeInput);

expect(mockHandleQueryChange).toHaveBeenCalledWith({
refId: 'A',
take: 250,
});
expect(mockHandleQueryChange).toHaveBeenCalledWith(
expect.objectContaining({
refId: 'A',
take: 250,
})
);
});

it('should preserve other query properties when take changes', async () => {
Expand All @@ -187,12 +247,14 @@ describe('ListAlarmsQueryEditor', () => {
fireEvent.change(takeInput, { target: { value: '300' } });
fireEvent.blur(takeInput);

expect(mockHandleQueryChange).toHaveBeenCalledWith({
refId: 'A',
filter: 'existing filter',
descending: true,
take: 300
});
expect(mockHandleQueryChange).toHaveBeenCalledWith(
expect.objectContaining({
refId: 'A',
filter: 'existing filter',
descending: true,
take: 300
})
);
});

it('should display minimum take error message when take value is below 1', async () => {
Expand Down Expand Up @@ -325,10 +387,12 @@ describe('ListAlarmsQueryEditor', () => {

fireEvent.click(descendingSwitch);

expect(mockHandleQueryChange).toHaveBeenCalledWith({
refId: 'A',
descending: true
});
expect(mockHandleQueryChange).toHaveBeenCalledWith(
expect.objectContaining({
refId: 'A',
descending: true
})
);
});

it('should preserve other query properties when descending changes', async () => {
Expand All @@ -338,12 +402,14 @@ describe('ListAlarmsQueryEditor', () => {
const descendingSwitch = screen.getByRole('switch');
fireEvent.click(descendingSwitch);

expect(mockHandleQueryChange).toHaveBeenCalledWith({
refId: 'A',
filter: 'test filter',
take: 500,
descending: true
});
expect(mockHandleQueryChange).toHaveBeenCalledWith(
expect.objectContaining({
refId: 'A',
filter: 'test filter',
take: 500,
descending: true
})
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
} from 'datasources/alarms/constants/AlarmsQueryEditor.constants';
import { Workspace } from 'core/types';
import { FloatingError } from 'core/errors';
import { AlarmsProperties, ListAlarmsQuery } from 'datasources/alarms/types/ListAlarms.types';
import { AlarmsProperties, ListAlarmsQuery, OutputType } from 'datasources/alarms/types/ListAlarms.types';
import { ListAlarmsQueryHandler } from 'datasources/alarms/query-type-handlers/list-alarms/ListAlarmsQueryHandler';
import { AutoSizeInput, Combobox, ComboboxOption, InlineSwitch, MultiCombobox, Stack } from '@grafana/ui';
import { AutoSizeInput, Combobox, ComboboxOption, InlineSwitch, MultiCombobox, RadioButtonGroup, Stack } from '@grafana/ui';
import { validateNumericInput } from 'core/utils';
import { TransitionInclusionOption } from 'datasources/alarms/types/types';

Expand Down Expand Up @@ -114,6 +114,10 @@ export function ListAlarmsQueryEditor({ query, handleQueryChange, datasource }:
});
};

const onOutputTypeChange = (value: OutputType) => {
handleQueryChange({ ...query, outputType: value });
};

const propertiesOptions = useMemo(() => {
const transitionInclusionOption = query.transitionInclusionOption;
const allOptions = Object.values(AlarmsPropertiesOptions);
Expand All @@ -130,22 +134,35 @@ export function ListAlarmsQueryEditor({ query, handleQueryChange, datasource }:
return (
<Stack direction='column'>
<InlineField
label={labels.properties}
label={labels.outputType}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.properties}
invalid={!isPropertiesControlValid}
error={PROPERTIES_ERROR_MESSAGE}
tooltip={tooltips.outputType}
>
<MultiCombobox
placeholder={placeholders.properties}
options={propertiesOptions}
onChange={onPropertiesChange}
value={query.properties}
width='auto'
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
<RadioButtonGroup
options={Object.values(OutputType).map(value => ({ label: value, value }))}
value={query.outputType}
onChange={onOutputTypeChange}
/>
</InlineField>
{query.outputType === OutputType.Properties && (
<InlineField
label={labels.properties}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.properties}
invalid={!isPropertiesControlValid}
error={PROPERTIES_ERROR_MESSAGE}
>
<MultiCombobox
placeholder={placeholders.properties}
options={propertiesOptions}
onChange={onPropertiesChange}
value={query.properties}
width='auto'
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
)}
<Stack>
<InlineField
label={labels.queryBy}
Expand All @@ -159,49 +176,51 @@ export function ListAlarmsQueryEditor({ query, handleQueryChange, datasource }:
onChange={onFilterChange}
/>
</InlineField>
<Stack direction='column'>
<InlineField
label={labels.transitionInclusion}
labelWidth={SECONDARY_LABEL_WIDTH}
tooltip={tooltips.transitionInclusion}
>
<Combobox
options={Object.values(AlarmsTransitionInclusionOptions)}
value={query.transitionInclusionOption}
width={SECONDARY_CONTROL_WIDTH}
onChange={onTransitionInclusionChange}
/>
</InlineField>
<InlineField
label={labels.descending}
labelWidth={SECONDARY_LABEL_WIDTH}
tooltip={tooltips.descending}
>
<InlineSwitch
onChange={event => onDescendingChange(event.currentTarget.checked)}
value={query.descending}
/>
</InlineField>
<InlineField
label={labels.take}
labelWidth={SECONDARY_LABEL_WIDTH}
tooltip={tooltips.take}
invalid={!!takeInvalidMessage}
error={takeInvalidMessage}
>
<AutoSizeInput
minWidth={SECONDARY_CONTROL_WIDTH}
maxWidth={SECONDARY_CONTROL_WIDTH}
type="number"
value={query.take}
onChange={onTakeChange}
placeholder={placeholders.take}
onKeyDown={event => {
validateNumericInput(event);
}}
/>
</InlineField>
</Stack>
{query.outputType === OutputType.Properties && (
<Stack direction='column'>
<InlineField
label={labels.transitionInclusion}
labelWidth={SECONDARY_LABEL_WIDTH}
tooltip={tooltips.transitionInclusion}
>
<Combobox
options={Object.values(AlarmsTransitionInclusionOptions)}
value={query.transitionInclusionOption}
width={SECONDARY_CONTROL_WIDTH}
onChange={onTransitionInclusionChange}
/>
</InlineField>
<InlineField
label={labels.descending}
labelWidth={SECONDARY_LABEL_WIDTH}
tooltip={tooltips.descending}
>
<InlineSwitch
onChange={event => onDescendingChange(event.currentTarget.checked)}
value={query.descending}
/>
</InlineField>
<InlineField
label={labels.take}
labelWidth={SECONDARY_LABEL_WIDTH}
tooltip={tooltips.take}
invalid={!!takeInvalidMessage}
error={takeInvalidMessage}
>
<AutoSizeInput
minWidth={SECONDARY_CONTROL_WIDTH}
maxWidth={SECONDARY_CONTROL_WIDTH}
type="number"
value={query.take}
onChange={onTakeChange}
placeholder={placeholders.take}
onKeyDown={event => {
validateNumericInput(event);
}}
/>
</InlineField>
</Stack>
)}
</Stack>
<FloatingError
message={datasource.errorTitle}
Expand Down
Loading