Compare commits
3 Commits
sqlkv-enab
...
rodrigopk/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9399059050 | ||
|
|
eaa11e4798 | ||
|
|
c27721a9d7 |
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user