Compare commits

...

3 Commits

Author SHA1 Message Date
rodrigopk
9399059050 Refactor provenance inference in Policy component 2026-01-14 13:54:39 -05:00
rodrigopk
eaa11e4798 Use isProvisionedResource in useMuteTimings
- Removed duplicated logic
- Added regression tests for useMuteTimings hook
2026-01-14 11:55:43 -05:00
rodrigopk
c27721a9d7 Remove provisioned prop from Policy component 2026-01-14 11:42:45 -05:00
5 changed files with 174 additions and 30 deletions

View File

@@ -0,0 +1,115 @@
import { renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { getWrapper } from 'test/test-utils';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import {
TIME_INTERVAL_NAME_FILE_PROVISIONED,
TIME_INTERVAL_NAME_HAPPY_PATH,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { AccessControlAction } from 'app/types/accessControl';
import { useGetMuteTiming, useMuteTimings } from './useMuteTimings';
const wrapper = ({ children }: { children: ReactNode }) => {
const ProviderWrapper = getWrapper({ renderWithRouter: true });
return <ProviderWrapper>{children}</ProviderWrapper>;
};
setupMswServer();
describe('useMuteTimings', () => {
beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
});
describe('useMuteTimings', () => {
it('should return mute timings with correct data structure', async () => {
const { result } = renderHook(
() =>
useMuteTimings({
alertmanager: GRAFANA_RULES_SOURCE_NAME,
skip: false,
}),
{
wrapper,
}
);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeDefined();
expect(Array.isArray(result.current.data)).toBe(true);
const timings = result.current.data!;
expect(timings.length).toBeGreaterThan(0);
// Verify structure of first timing
const firstTiming = timings[0];
expect(firstTiming).toHaveProperty('id');
expect(firstTiming).toHaveProperty('name');
expect(firstTiming).toHaveProperty('time_intervals');
expect(typeof firstTiming.id).toBe('string');
expect(typeof firstTiming.name).toBe('string');
expect(Array.isArray(firstTiming.time_intervals)).toBe(true);
});
it('should correctly identify provisioned intervals', async () => {
const { result } = renderHook(
() =>
useMuteTimings({
alertmanager: GRAFANA_RULES_SOURCE_NAME,
skip: false,
}),
{
wrapper,
}
);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
const timings = result.current.data!;
// Find the provisioned interval
const provisionedTiming = timings.find((t) => t.name === TIME_INTERVAL_NAME_FILE_PROVISIONED);
expect(provisionedTiming).toBeDefined();
expect(provisionedTiming?.provisioned).toBe(true);
// Find the non-provisioned interval
const nonProvisionedTiming = timings.find((t) => t.name === TIME_INTERVAL_NAME_HAPPY_PATH);
expect(nonProvisionedTiming).toBeDefined();
expect(nonProvisionedTiming?.provisioned).toBe(false);
});
});
describe('useGetMuteTiming', () => {
it('should return single mute timing by name for editing', async () => {
const { result } = renderHook(
() =>
useGetMuteTiming({
alertmanager: GRAFANA_RULES_SOURCE_NAME,
name: TIME_INTERVAL_NAME_HAPPY_PATH,
}),
{
wrapper,
}
);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeDefined();
expect(result.current.data?.name).toBe(TIME_INTERVAL_NAME_HAPPY_PATH);
expect(result.current.data?.id).toBe(TIME_INTERVAL_NAME_HAPPY_PATH);
expect(result.current.data).toHaveProperty('time_intervals');
expect(result.current.isError).toBe(false);
});
});
});

View File

@@ -9,9 +9,9 @@ import {
IoK8SApimachineryPkgApisMetaV1ObjectMeta,
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
import {
isK8sEntityProvisioned,
isProvisionedResource,
shouldUseK8sApi,
stringifyFieldSelector,
} from 'app/features/alerting/unified/utils/k8s/utils';
@@ -62,7 +62,7 @@ const parseAmTimeInterval: (interval: MuteTimeInterval, provenance: string) => M
return {
...interval,
id: interval.name,
provisioned: Boolean(provenance && provenance !== KnownProvenance.None),
provisioned: isProvisionedResource(provenance),
};
};

View File

@@ -27,7 +27,6 @@ import { useAddPolicyModal, useAlertGroupsModal, useDeletePolicyModal, useEditPo
import { Policy } from './Policy';
import { TIMING_OPTIONS_DEFAULTS } from './timingOptions';
import {
isRouteProvisioned,
useAddNotificationPolicy,
useDeleteNotificationPolicy,
useNotificationPolicyRoute,
@@ -100,8 +99,6 @@ export const NotificationPoliciesList = () => {
}
return;
}, [defaultPolicy]);
const routeProvenance = defaultPolicy?.provenance;
const isRootRouteProvisioned = rootRoute ? isRouteProvisioned(rootRoute) : false;
// useAsync could also work but it's hard to wait until it's done in the tests
// Combining with useEffect gives more predictable results because the condition is in useEffect
@@ -247,8 +244,6 @@ export const NotificationPoliciesList = () => {
currentRoute={defaults(rootRoute, TIMING_OPTIONS_DEFAULTS)}
contactPointsState={contactPointsState.receivers}
readOnly={!hasConfigurationAPI}
provisioned={isRootRouteProvisioned}
provenance={routeProvenance}
alertManagerSourceName={selectedAlertmanager}
onAddPolicy={openAddModal}
onEditPolicy={openEditModal}

View File

@@ -11,6 +11,7 @@ import {
AlertmanagerGroup,
MatcherOperator,
ObjectMatcher,
ROUTES_META_SYMBOL,
RouteWithID,
} from 'app/plugins/datasource/alertmanager/types';
@@ -338,6 +339,7 @@ describe('Policy', () => {
id: 'test-route',
receiver: 'test-receiver',
routes: [],
[ROUTES_META_SYMBOL]: { provenance: KnownProvenance.File },
};
renderPolicy(
@@ -351,8 +353,6 @@ describe('Policy', () => {
onAddPolicy={noop}
onDeletePolicy={noop}
onShowAlertInstances={noop}
provisioned
provenance={KnownProvenance.File}
/>
);
@@ -365,6 +365,7 @@ describe('Policy', () => {
id: 'test-route',
receiver: 'test-receiver',
routes: [],
[ROUTES_META_SYMBOL]: { provenance: KnownProvenance.ConvertedPrometheus },
};
renderPolicy(
@@ -378,14 +379,38 @@ describe('Policy', () => {
onAddPolicy={noop}
onDeletePolicy={noop}
onShowAlertInstances={noop}
provisioned
provenance={KnownProvenance.ConvertedPrometheus}
/>
);
const badge = screen.getByText('Imported');
expect(badge).toBeInTheDocument();
});
it('correctly identifies provisioned status from ROUTES_META_SYMBOL', () => {
const mockRoute: RouteWithID = {
id: 'test-route',
receiver: 'test-receiver',
routes: [],
[ROUTES_META_SYMBOL]: { provenance: KnownProvenance.File },
};
renderPolicy(
<Policy
isDefaultPolicy
currentRoute={mockRoute}
contactPointsState={mockReceiversState()}
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
onEditPolicy={noop}
onAddPolicy={noop}
onDeletePolicy={noop}
onShowAlertInstances={noop}
/>
);
expect(screen.getByText('Provisioned')).toBeInTheDocument();
// Verify add/edit buttons are disabled
expect(screen.getByRole('button', { name: /new child policy/i })).toBeDisabled();
});
});
// Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element
@@ -477,47 +502,52 @@ describe('useCreateDropdownMenuActions', () => {
{
isAutoGenerated: false,
isDefaultPolicy: true,
provisioned: false,
provenance: undefined,
expectedMenu: ['edit-policy', 'export-policy'],
},
{
isAutoGenerated: false,
isDefaultPolicy: true,
provisioned: true,
provenance: KnownProvenance.File,
expectedMenu: ['edit-policy', 'export-policy'],
},
{
isAutoGenerated: false,
isDefaultPolicy: false,
provisioned: false,
provenance: undefined,
expectedMenu: ['edit-policy', 'delete-policy'],
},
{
isAutoGenerated: false,
isDefaultPolicy: false,
provisioned: true,
provenance: KnownProvenance.File,
expectedMenu: ['edit-policy', 'delete-policy'],
},
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: true, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: false, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: false, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: true, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: true, provenance: KnownProvenance.File, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: false, provenance: undefined, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: true, provenance: undefined, expectedMenu: ['edit-policy'] },
{ isAutoGenerated: true, isDefaultPolicy: false, provenance: KnownProvenance.File, expectedMenu: ['edit-policy'] },
];
testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provisioned, expectedMenu }) => {
it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, provisioned=${provisioned}`, () => {
testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provenance, expectedMenu }) => {
const provisionedStatus = provenance ? 'provisioned' : 'not provisioned';
it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, ${provisionedStatus}`, () => {
useAlertmanagerAbilitiesMock.mockReturnValue([
[true, true],
[true, true],
[true, true],
]);
// Create route with provenance in metadata or top-level to match real usage
const routeWithProvenance: RouteWithID = provenance
? { ...currentRoute, [ROUTES_META_SYMBOL]: { provenance } }
: currentRoute;
const { result } = renderHook(() =>
useCreateDropdownMenuActions(
isAutoGenerated,
isDefaultPolicy,
provisioned,
provenance,
openDetailModal,
currentRoute,
routeWithProvenance,
toggleShowExportDrawer,
onDeletePolicy
)

View File

@@ -31,12 +31,14 @@ import {
AlertmanagerGroup,
MatcherOperator,
ObjectMatcher,
ROUTES_META_SYMBOL,
Receiver,
RouteWithID,
} from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { getAmMatcherFormatter } from '../../utils/alertmanager';
import { isProvisionedResource } from '../../utils/k8s/utils';
import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers';
import { createContactPointLink, createContactPointSearchLink, createMuteTimingLink } from '../../utils/misc';
import { routeAdapter } from '../../utils/routeAdapter';
@@ -60,8 +62,6 @@ interface PolicyComponentProps {
receivers?: Receiver[];
contactPointsState?: ReceiversState;
readOnly?: boolean;
provisioned?: boolean;
provenance?: string;
inheritedProperties?: InheritableProperties;
routesMatchingFilters?: RoutesMatchingFilters;
@@ -89,8 +89,6 @@ const Policy = (props: PolicyComponentProps) => {
receivers = [],
contactPointsState,
readOnly = false,
provisioned = false,
provenance,
alertManagerSourceName,
currentRoute,
inheritedProperties,
@@ -109,6 +107,10 @@ const Policy = (props: PolicyComponentProps) => {
const styles = useStyles2(getStyles);
// Derive provenance from route metadata or top-level (consistent with child handling)
const provenance = currentRoute[ROUTES_META_SYMBOL]?.provenance ?? currentRoute.provenance;
const provisioned = isProvisionedResource(provenance);
const contactPoint = currentRoute.receiver;
const continueMatching = currentRoute.continue ?? false;
@@ -183,7 +185,7 @@ const Policy = (props: PolicyComponentProps) => {
const dropdownMenuActions: JSX.Element[] = useCreateDropdownMenuActions(
isAutoGenerated,
isDefaultPolicy,
provisioned,
provenance,
onEditPolicy,
currentRoute,
toggleShowExportDrawer,
@@ -377,7 +379,6 @@ const Policy = (props: PolicyComponentProps) => {
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={matchingInstancesPreview}
isAutoGenerated={isThisChildAutoGenerated}
provisioned={provisioned}
/>
);
})}
@@ -542,7 +543,7 @@ function MetadataRow({
export const useCreateDropdownMenuActions = (
isAutoGenerated: boolean,
isDefaultPolicy: boolean,
provisioned: boolean,
provenance: string | undefined,
onEditPolicy: (route: RouteWithID, isDefault?: boolean, readOnly?: boolean) => void,
currentRoute: RouteWithID,
toggleShowExportDrawer: () => void,
@@ -558,6 +559,9 @@ export const useCreateDropdownMenuActions = (
AlertmanagerAction.ExportNotificationPolicies,
]);
// Compute provisioned status from provenance
const provisioned = isProvisionedResource(provenance);
const dropdownMenuActions = [];
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy && !isAutoGenerated;
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;