mirror of
https://github.com/grafana/grafana.git
synced 2026-08-09 04:38:16 -05:00
Alerting: Show unusable time intervals as disabled in the mute timings selector
Filtering canUse:false intervals out of the policy form left the dropdown saying "No options found" for an interval the user can see configured elsewhere. List them disabled with a reason instead. isUsableTimeInterval now treats intervals without metadata (Alertmanager config API) as usable, since the annotation is k8s-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4bb3ededcb
commit
1bec8ee90e
+33
-31
@@ -17,19 +17,16 @@ import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import MuteTimingsSelector from './MuteTimingsSelector';
|
||||
|
||||
const renderWithProvider = (alertManagerSource = GRAFANA_RULES_SOURCE_NAME) => {
|
||||
const renderWithProvider = (alertManagerSource = GRAFANA_RULES_SOURCE_NAME, onChange: () => void = () => {}) => {
|
||||
return render(
|
||||
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertManagerSource}>
|
||||
<MuteTimingsSelector
|
||||
alertmanager={alertManagerSource}
|
||||
selectProps={{
|
||||
onChange: () => {},
|
||||
}}
|
||||
/>
|
||||
<MuteTimingsSelector alertmanager={alertManagerSource} selectProps={{ onChange }} />
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const NOT_USABLE_DESCRIPTION = /imported from an external alertmanager/i;
|
||||
|
||||
setupMswServer();
|
||||
|
||||
describe('MuteTimingsSelector', () => {
|
||||
@@ -60,15 +57,16 @@ describe('MuteTimingsSelector', () => {
|
||||
expect(screen.getByText('another-regular')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should filter out time intervals with canUse: false', async () => {
|
||||
it('should list time intervals with canUse: false as unselectable', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
setTimeIntervalsList([
|
||||
{ name: 'regular-interval', provenance: 'none' },
|
||||
{ name: 'imported-interval', canUse: false },
|
||||
{ name: 'file-provisioned', provenance: 'file' },
|
||||
]);
|
||||
|
||||
renderWithProvider();
|
||||
renderWithProvider(GRAFANA_RULES_SOURCE_NAME, onChange);
|
||||
|
||||
// Click to open the dropdown
|
||||
const selector = await screen.findByRole('combobox', { name: /time intervals/i });
|
||||
@@ -78,34 +76,33 @@ describe('MuteTimingsSelector', () => {
|
||||
expect(await screen.findByText('regular-interval')).toBeInTheDocument();
|
||||
expect(screen.getByText('file-provisioned')).toBeInTheDocument();
|
||||
|
||||
// Non-usable interval should NOT be in the list
|
||||
expect(screen.queryByText('imported-interval')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('imported-interval')).toBeInTheDocument();
|
||||
expect(screen.getByText(NOT_USABLE_DESCRIPTION)).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText('imported-interval'));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show only usable intervals when all types are present', async () => {
|
||||
it('should still allow selecting a usable interval alongside unselectable ones', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
setTimeIntervalsList([
|
||||
{ name: 'normal-1', provenance: 'none' },
|
||||
{ name: 'imported-1', canUse: false },
|
||||
{ name: 'normal-2', provenance: 'none' },
|
||||
{ name: 'imported-2', canUse: false },
|
||||
{ name: 'file-1', provenance: 'file' },
|
||||
]);
|
||||
|
||||
renderWithProvider();
|
||||
renderWithProvider(GRAFANA_RULES_SOURCE_NAME, onChange);
|
||||
|
||||
// Click to open the dropdown
|
||||
const selector = await screen.findByRole('combobox', { name: /time intervals/i });
|
||||
await user.click(selector);
|
||||
|
||||
// Usable intervals should be visible
|
||||
expect(await screen.findByText('normal-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('normal-2')).toBeInTheDocument();
|
||||
expect(screen.getByText('file-1')).toBeInTheDocument();
|
||||
await user.click(await screen.findByText('normal-1'));
|
||||
|
||||
// Non-usable intervals should NOT be visible
|
||||
expect(screen.queryByText('imported-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('imported-2')).not.toBeInTheDocument();
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([expect.objectContaining({ value: 'normal-1' })]),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty list', async () => {
|
||||
@@ -120,24 +117,28 @@ describe('MuteTimingsSelector', () => {
|
||||
|
||||
it('should handle list with only non-usable intervals', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
setTimeIntervalsList([
|
||||
{ name: 'imported-1', canUse: false },
|
||||
{ name: 'imported-2', canUse: false },
|
||||
]);
|
||||
|
||||
renderWithProvider();
|
||||
renderWithProvider(GRAFANA_RULES_SOURCE_NAME, onChange);
|
||||
|
||||
// Click to open the dropdown
|
||||
const selector = await screen.findByRole('combobox', { name: /time intervals/i });
|
||||
await user.click(selector);
|
||||
|
||||
// No intervals should be visible
|
||||
expect(screen.queryByText('imported-1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('imported-2')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('imported-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('imported-2')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText('imported-1'));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter out intervals with missing canUse annotation', async () => {
|
||||
it('should treat a k8s interval with no canUse annotation as unselectable', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = jest.fn();
|
||||
// Manually create intervals without canUse annotation
|
||||
const listMuteTimingsPath = listNamespacedTimeIntervalHandler().info.path;
|
||||
|
||||
@@ -175,15 +176,16 @@ describe('MuteTimingsSelector', () => {
|
||||
})
|
||||
);
|
||||
|
||||
renderWithProvider();
|
||||
renderWithProvider(GRAFANA_RULES_SOURCE_NAME, onChange);
|
||||
|
||||
// Click to open the dropdown
|
||||
const selector = await screen.findByRole('combobox', { name: /time intervals/i });
|
||||
await user.click(selector);
|
||||
|
||||
// Only interval with canUse: 'true' should be visible
|
||||
expect(await screen.findByText('interval-with-canuse')).toBeInTheDocument();
|
||||
expect(screen.queryByText('interval-without-canuse')).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText('interval-without-canuse'));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('external alertmanager', () => {
|
||||
|
||||
+25
-7
@@ -1,22 +1,40 @@
|
||||
import { type SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { MultiSelect, type MultiSelectCommonProps } from '@grafana/ui';
|
||||
import { type MuteTiming, useMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
|
||||
import {
|
||||
type MuteTiming,
|
||||
isUsableTimeInterval,
|
||||
useMuteTimings,
|
||||
} from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
|
||||
import { type BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
|
||||
import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager';
|
||||
|
||||
const mapTimeInterval = ({ name, time_intervals }: MuteTiming): SelectableValue<string> => ({
|
||||
value: name,
|
||||
label: name,
|
||||
description: time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '),
|
||||
});
|
||||
const mapTimeInterval = (timing: MuteTiming): SelectableValue<string> => {
|
||||
const { name, time_intervals } = timing;
|
||||
const schedule = time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND ');
|
||||
const isUsable = isUsableTimeInterval(timing);
|
||||
|
||||
return {
|
||||
value: name,
|
||||
label: name,
|
||||
// Shown but disabled rather than hidden: filtering imported intervals out leaves the user staring at
|
||||
// "No options found" for an interval they can see configured elsewhere.
|
||||
isDisabled: !isUsable,
|
||||
description: isUsable
|
||||
? schedule
|
||||
: t(
|
||||
'alerting.time-intervals-selector.imported-not-usable',
|
||||
'Imported from an external Alertmanager — promote it to use it here'
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/** Provides a MultiSelect with available time intervals for the given alertmanager */
|
||||
const TimeIntervalSelector = ({
|
||||
alertmanager,
|
||||
selectProps,
|
||||
}: BaseAlertmanagerArgs & { selectProps: MultiSelectCommonProps<string> }) => {
|
||||
const { data } = useMuteTimings({ alertmanager, skip: selectProps.disabled, filterUsable: true });
|
||||
const { data } = useMuteTimings({ alertmanager, skip: selectProps.disabled });
|
||||
|
||||
const timeIntervalOptions = (data || []).map((value) => mapTimeInterval(value));
|
||||
|
||||
|
||||
@@ -99,17 +99,17 @@ const useGrafanaAlertmanagerIntervals = () =>
|
||||
* Imported time intervals (provenance: converted_prometheus) are marked with canUse: false
|
||||
* by the backend because they belong to external Mimir-kind resources and should not be
|
||||
* referenced in Grafana-managed routes or rules.
|
||||
*
|
||||
* Only k8s entities carry the annotation; intervals parsed from the Alertmanager config API have no
|
||||
* metadata and are always usable.
|
||||
*/
|
||||
const isUsableTimeInterval = (timing: MuteTiming): boolean => {
|
||||
const canUse = timing.metadata?.annotations?.[K8sAnnotations.CanUse];
|
||||
return canUse === 'true';
|
||||
export const isUsableTimeInterval = (timing: MuteTiming): boolean => {
|
||||
if (!timing.metadata) {
|
||||
return true;
|
||||
}
|
||||
return timing.metadata.annotations?.[K8sAnnotations.CanUse] === 'true';
|
||||
};
|
||||
|
||||
interface UseMuteTimingsOptions extends BaseAlertmanagerArgs, Skippable {
|
||||
/** When true, filters out intervals marked as non-usable (canUse: false). Defaults to false. */
|
||||
filterUsable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Depending on alertmanager source, fetches mute timings.
|
||||
*
|
||||
@@ -117,8 +117,11 @@ interface UseMuteTimingsOptions extends BaseAlertmanagerArgs, Skippable {
|
||||
* fetches time intervals from k8s API.
|
||||
*
|
||||
* Otherwise, fetches and parses from the alertmanager config API
|
||||
*
|
||||
* Non-usable intervals are included — callers offering them for selection should disable them via
|
||||
* {@link isUsableTimeInterval} rather than drop them.
|
||||
*/
|
||||
export const useMuteTimings = ({ alertmanager, skip, filterUsable = false }: UseMuteTimingsOptions) => {
|
||||
export const useMuteTimings = ({ alertmanager, skip }: BaseAlertmanagerArgs & Skippable) => {
|
||||
const useK8sApi = shouldUseK8sApi(alertmanager);
|
||||
|
||||
const [getGrafanaTimeIntervals, intervalsResponse] = useGrafanaAlertmanagerIntervals();
|
||||
@@ -135,13 +138,7 @@ export const useMuteTimings = ({ alertmanager, skip, filterUsable = false }: Use
|
||||
}
|
||||
}, [alertmanager, getAlertmanagerTimeIntervals, getGrafanaTimeIntervals, skip, useK8sApi]);
|
||||
|
||||
const response = useK8sApi ? intervalsResponse : configApiResponse;
|
||||
|
||||
if (filterUsable && useK8sApi && response.data) {
|
||||
return { ...response, data: response.data.filter(isUsableTimeInterval) };
|
||||
}
|
||||
|
||||
return response;
|
||||
return useK8sApi ? intervalsResponse : configApiResponse;
|
||||
};
|
||||
|
||||
type CreateUpdateMuteTimingArgs = { interval: MuteTimeInterval };
|
||||
|
||||
@@ -3619,6 +3619,7 @@
|
||||
},
|
||||
"time-intervals-selector": {
|
||||
"aria-label-time-intervals": "Time intervals",
|
||||
"imported-not-usable": "Imported from an external Alertmanager — promote it to use it here",
|
||||
"placeholder-select-time-intervals": "Select time intervals..."
|
||||
},
|
||||
"time-intervals-table": {
|
||||
|
||||
Reference in New Issue
Block a user