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:
rodrigopk
2026-08-06 22:55:52 -04:00
co-authored by Claude Opus 5
parent 4bb3ededcb
commit 1bec8ee90e
4 changed files with 72 additions and 54 deletions
@@ -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', () => {
@@ -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 };
+1
View File
@@ -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": {