WIP: create modal
This commit is contained in:
@@ -38,13 +38,14 @@ import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
import { AlertmanagerGroup, Receiver, Route, ROUTES_META_SYMBOL } from '../../../plugins/datasource/alertmanager/types';
|
||||
import { stringifyErrorLike } from './utils/misc';
|
||||
import {
|
||||
useCreateRoutingTree,
|
||||
useDeleteRoutingTree,
|
||||
useListNotificationPolicyRoutes,
|
||||
useRootRouteSearch,
|
||||
} from './components/notification-policies/useNotificationPolicyRoute';
|
||||
import { useURLSearchParams } from './hooks/useURLSearchParams';
|
||||
import { usePagination } from './hooks/usePagination';
|
||||
import { useDeleteRoutingTreeModal } from './components/notification-policies/components/Modals';
|
||||
import { useCreateRoutingTreeModal, useDeleteRoutingTreeModal } from './components/notification-policies/components/Modals';
|
||||
import { ALL_ROUTING_TREES, useExportRoutingTree } from './components/notification-policies/useExportRoutingTree';
|
||||
import { K8sAnnotations, ROOT_ROUTE_NAME } from './utils/k8s/constants';
|
||||
import ConditionalWrap from './components/ConditionalWrap';
|
||||
@@ -52,10 +53,11 @@ import { ProvisioningBadge } from './components/Provisioning';
|
||||
import { getAnnotation } from './utils/k8s/utils';
|
||||
import { Spacer } from './components/Spacer';
|
||||
import MoreButton from './components/MoreButton';
|
||||
import { useContactPointsWithStatus } from './components/contact-points/useContactPoints';
|
||||
import { useGrafanaContactPoints } from './components/contact-points/useContactPoints';
|
||||
import { useAlertGroupsModal } from './components/notification-policies/Modals';
|
||||
import { normalizeMatchers } from './utils/matchers';
|
||||
import { RoutingTreeFilter } from './components/notification-policies/components/RoutingTreeFilter';
|
||||
import {config} from '@grafana/runtime';
|
||||
|
||||
enum ActiveTab {
|
||||
NotificationPolicies = 'notification_policies',
|
||||
@@ -126,7 +128,9 @@ const NotificationPoliciesTabs = () => {
|
||||
const PolicyTreeTab = () => {
|
||||
const { isGrafanaAlertmanager } = useAlertmanager();
|
||||
|
||||
if (!isGrafanaAlertmanager) {
|
||||
const useMultiplePoliciesView = config.featureToggles.alertingMultiplePolicies;
|
||||
|
||||
if (!isGrafanaAlertmanager || !useMultiplePoliciesView) {
|
||||
return <NotificationPoliciesTree />;
|
||||
}
|
||||
|
||||
@@ -138,7 +142,6 @@ const PolicyTreeTab = () => {
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
const PolicyTreeTabContents = () => {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const [queryParams] = useURLSearchParams();
|
||||
|
||||
const [[createPoliciesSupported, createPoliciesAllowed], [exportPoliciesSupported, exportPoliciesAllowed]] =
|
||||
@@ -151,18 +154,21 @@ const PolicyTreeTabContents = () => {
|
||||
currentData: allPolicies,
|
||||
isLoading,
|
||||
error: fetchPoliciesError,
|
||||
} = useListNotificationPolicyRoutes({ alertmanager: selectedAlertmanager ?? '' });
|
||||
} = useListNotificationPolicyRoutes();
|
||||
|
||||
const [ExportDrawer, showExportDrawer] = useExportRoutingTree();
|
||||
|
||||
const [createTrigger] = useCreateRoutingTree();
|
||||
const [CreateModal, showCreateModal] = useCreateRoutingTreeModal(createTrigger.execute);
|
||||
|
||||
const search = queryParams.get('search');
|
||||
|
||||
const [contactPointsSupported, canSeeContactPoints] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
|
||||
const shouldFetchContactPoints = contactPointsSupported && canSeeContactPoints;
|
||||
const { contactPoints: receivers } = useContactPointsWithStatus({
|
||||
alertmanager: selectedAlertmanager ?? '',
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: true,
|
||||
const { contactPoints: receivers } = useGrafanaContactPoints({
|
||||
skip: !shouldFetchContactPoints,
|
||||
fetchStatuses: false,
|
||||
fetchPolicies: false,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
@@ -177,15 +183,15 @@ const PolicyTreeTabContents = () => {
|
||||
|
||||
<Stack direction="row" gap={1}>
|
||||
{createPoliciesSupported && (
|
||||
<LinkButton
|
||||
<Button
|
||||
icon="plus"
|
||||
aria-label={'add policy'}
|
||||
variant="primary"
|
||||
href="/alerting/routes/new"
|
||||
disabled={!createPoliciesAllowed}
|
||||
onClick={() => showCreateModal()}
|
||||
>
|
||||
Create policy
|
||||
</LinkButton>
|
||||
</Button>
|
||||
)}
|
||||
{exportPoliciesSupported && (
|
||||
<Button
|
||||
@@ -210,6 +216,7 @@ const PolicyTreeTabContents = () => {
|
||||
receivers={receivers}
|
||||
/>
|
||||
)}
|
||||
{CreateModal}
|
||||
{ExportDrawer}
|
||||
</>
|
||||
);
|
||||
@@ -218,7 +225,7 @@ const PolicyTreeTabContents = () => {
|
||||
interface RoutingTreeListProps {
|
||||
policies: Route[];
|
||||
search?: string | null;
|
||||
pageSize?: number;
|
||||
pageSize: number;
|
||||
receivers?: Receiver[];
|
||||
}
|
||||
|
||||
|
||||
+27
-2
@@ -3,7 +3,7 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { ContactPointSelector as GrafanaManagedContactPointSelector } from '@grafana/alerting/unstable';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Collapse, Field, Link, MultiSelect, useStyles2 } from '@grafana/ui';
|
||||
import { Collapse, Field, Input, Link, MultiSelect, useStyles2 } from '@grafana/ui';
|
||||
import { ExternalAlertmanagerContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector';
|
||||
import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils';
|
||||
import { RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
@@ -30,9 +30,10 @@ export interface AmRootRouteFormProps {
|
||||
actionButtons: ReactNode;
|
||||
onSubmit: (route: Partial<FormAmRoute>) => void;
|
||||
route: RouteWithID;
|
||||
showNameField?: boolean;
|
||||
}
|
||||
|
||||
export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmit, route }: AmRootRouteFormProps) => {
|
||||
export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmit, route, showNameField }: AmRootRouteFormProps) => {
|
||||
const styles = useStyles2(getFormStyles);
|
||||
const [isTimingOptionsExpanded, setIsTimingOptionsExpanded] = useState(false);
|
||||
const { isGrafanaAlertmanager } = useAlertmanager();
|
||||
@@ -55,6 +56,30 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
|
||||
});
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{showNameField && (<Field
|
||||
required
|
||||
label={t('alerting.am-root-route-form.name', 'Name')}
|
||||
description={t(
|
||||
'alerting.am-root-route-form.description-unique-route',
|
||||
'A unique name for the routing tree'
|
||||
)}
|
||||
invalid={!!errors.name}
|
||||
error={errors.name?.message}
|
||||
>
|
||||
<Input
|
||||
{...register('name', {
|
||||
required: true,
|
||||
validate: (value) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return 'Name is required';
|
||||
}
|
||||
return true
|
||||
},
|
||||
})}
|
||||
className={styles.input}
|
||||
data-testid="routing-tree-name"
|
||||
/>
|
||||
</Field>)}
|
||||
<Field
|
||||
label={t('alerting.am-root-route-form.label-default-contact-point', 'Default contact point')}
|
||||
invalid={Boolean(errors.receiver) ? true : undefined}
|
||||
|
||||
@@ -298,10 +298,10 @@ const useAlertGroupsModal = (
|
||||
return [modalElement, handleShow, handleDismiss];
|
||||
};
|
||||
|
||||
const UpdatingModal: FC<Pick<ModalProps, 'isOpen'>> = ({ isOpen }) => (
|
||||
export const UpdatingModal: FC<Pick<ModalProps, 'isOpen' | 'onDismiss'>> = ({ isOpen, onDismiss = () => {}} ) => (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={() => {}}
|
||||
onDismiss={onDismiss}
|
||||
closeOnBackdropClick={false}
|
||||
closeOnEscape={false}
|
||||
ariaLabel={t('alerting.policies.update.updating', 'Updating...')}
|
||||
|
||||
@@ -694,9 +694,9 @@ const AllMatchesIndicator: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
type DefaultPolicyIndicatorProps = { route: RouteWithID };
|
||||
type DefaultPolicyIndicatorProps = { route?: RouteWithID };
|
||||
|
||||
const DefaultPolicyIndicator: FC<DefaultPolicyIndicatorProps> = ({route}) => {
|
||||
export const DefaultPolicyIndicator: FC<DefaultPolicyIndicatorProps> = ({route={}}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<>
|
||||
|
||||
+97
-10
@@ -1,9 +1,16 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, Modal, ModalProps } from '@grafana/ui';
|
||||
import { Button, Modal, ModalProps, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { stringifyErrorLike } from '../../../utils/misc';
|
||||
import { FormAmRoute } from '../../../types/amroutes';
|
||||
import { AmRootRouteForm } from '../EditDefaultPolicyForm';
|
||||
import { UpdatingModal } from '../Modals';
|
||||
import { getFormStyles } from '../formStyles';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
import { RouteWithID } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { defaultGroupBy } from '../../../utils/amroutes';
|
||||
|
||||
/**
|
||||
* This hook controls the delete modal for routing trees, showing loading and error states when appropriate
|
||||
@@ -56,18 +63,12 @@ export const useDeleteRoutingTreeModal = (
|
||||
closeOnEscape={!isLoading}
|
||||
title={'Delete routing tree'}
|
||||
>
|
||||
<p>
|
||||
Deleting this routing tree will permanently remove it.
|
||||
</p>
|
||||
<p>
|
||||
Are you sure you want to delete this routing tree?
|
||||
</p>
|
||||
<p>Deleting this routing tree will permanently remove it.</p>
|
||||
<p>Are you sure you want to delete this routing tree?</p>
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button type="button" variant="destructive" onClick={handleSubmit} disabled={isLoading}>
|
||||
{isLoading
|
||||
? "Deleting..."
|
||||
: "Yes, delete routing tree"}
|
||||
{isLoading ? 'Deleting...' : 'Yes, delete routing tree'}
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={handleDismiss} disabled={isLoading}>
|
||||
<Trans i18nKey="alerting.common.cancel">Cancel</Trans>
|
||||
@@ -80,6 +81,92 @@ export const useDeleteRoutingTreeModal = (
|
||||
return [modalElement, handleShow, handleDismiss] as const;
|
||||
};
|
||||
|
||||
const emptyRouteWithID = {
|
||||
id: '',
|
||||
name: '',
|
||||
group_by: defaultGroupBy,
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook controls the create modal for routing trees, showing loading and error states when appropriate
|
||||
*/
|
||||
export const useCreateRoutingTreeModal = (handleCreate: (route: Partial<FormAmRoute>) => Promise<unknown>) => {
|
||||
const styles = useStyles2(getFormStyles);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [route, setRoute] = useState<RouteWithID>(emptyRouteWithID);
|
||||
const [error, setError] = useState<unknown | undefined>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setShowModal(false);
|
||||
setError(undefined);
|
||||
setRoute(emptyRouteWithID);
|
||||
}, [isLoading]);
|
||||
|
||||
const handleShow = useCallback(() => {
|
||||
setShowModal(true);
|
||||
setError(undefined);
|
||||
setRoute(emptyRouteWithID);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(newRoute: Partial<FormAmRoute>) => {
|
||||
if (newRoute) {
|
||||
setIsLoading(true);
|
||||
handleCreate(newRoute)
|
||||
.then(() => setShowModal(false))
|
||||
.catch(setError)
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleCreate]
|
||||
);
|
||||
|
||||
const modalElement = useMemo(() => {
|
||||
if (error) {
|
||||
return <ErrorModal isOpen={showModal} onDismiss={handleDismiss} error={error} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <UpdatingModal isOpen={showModal} onDismiss={handleDismiss} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={showModal}
|
||||
onDismiss={handleDismiss}
|
||||
closeOnBackdropClick={true}
|
||||
closeOnEscape={true}
|
||||
title={'Create routing tree'}
|
||||
>
|
||||
<AmRootRouteForm
|
||||
route={route}
|
||||
showNameField={true}
|
||||
onSubmit={handleSubmit}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
actionButtons={
|
||||
<Modal.ButtonRow>
|
||||
<Button type="button" variant="secondary" onClick={handleDismiss} fill="outline">
|
||||
<Trans i18nKey="alerting.common.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
<Trans i18nKey="alerting.policies.save-policy">Add routing tree</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}, [route, error, handleSubmit, handleDismiss, isLoading, showModal, styles.input]);
|
||||
|
||||
return [modalElement, handleShow, handleDismiss] as const;
|
||||
};
|
||||
|
||||
interface ErrorModalProps extends Pick<ModalProps, 'isOpen' | 'onDismiss'> {
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
+45
-4
@@ -35,6 +35,7 @@ import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const {
|
||||
useCreateNamespacedRoutingTreeMutation,
|
||||
useDeleteNamespacedRoutingTreeMutation,
|
||||
useListNamespacedRoutingTreeQuery,
|
||||
useReplaceNamespacedRoutingTreeMutation,
|
||||
@@ -83,13 +84,11 @@ export const useNotificationPolicyRoute = (
|
||||
return k8sApiSupported ? k8sRouteQuery : amConfigQuery;
|
||||
};
|
||||
|
||||
export const useListNotificationPolicyRoutes = ({ alertmanager }: BaseAlertmanagerArgs, { skip }: Skippable = {}) => {
|
||||
const k8sApiSupported = shouldUseK8sApi(alertmanager);
|
||||
|
||||
export const useListNotificationPolicyRoutes = ({ skip }: Skippable = {}) => {
|
||||
return useListNamespacedRoutingTreeQuery(
|
||||
{ namespace: getAPINamespace() },
|
||||
{
|
||||
skip: skip || !k8sApiSupported,
|
||||
skip: skip,
|
||||
selectFromResult: (result) => {
|
||||
return {
|
||||
...result,
|
||||
@@ -267,6 +266,48 @@ export function useDeleteRoutingTree() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateRoutingTree() {
|
||||
const [createNamespacedRoutingTree] = useCreateNamespacedRoutingTreeMutation();
|
||||
|
||||
return useAsync(async (partialFormRoute: Partial<FormAmRoute>) => {
|
||||
const namespace = getAPINamespace();
|
||||
|
||||
const {
|
||||
name,
|
||||
overrideGrouping,
|
||||
groupBy,
|
||||
overrideTimings,
|
||||
groupWaitValue,
|
||||
groupIntervalValue,
|
||||
repeatIntervalValue,
|
||||
receiver,
|
||||
} = partialFormRoute;
|
||||
|
||||
// This does not "inherit" from any existing route, as this is a new routing tree. If not set, it will use the system
|
||||
// defaults. Currently supported by group_by, group_wait, group_interval, and repeat_interval
|
||||
const USE_DEFAULTS = undefined;
|
||||
|
||||
const newRoute: Route = {
|
||||
name: name,
|
||||
group_by: overrideGrouping ? groupBy : USE_DEFAULTS,
|
||||
group_wait: overrideTimings && groupWaitValue ? groupWaitValue : USE_DEFAULTS,
|
||||
group_interval: overrideTimings && groupIntervalValue ? groupIntervalValue : USE_DEFAULTS,
|
||||
repeat_interval: overrideTimings && repeatIntervalValue ? repeatIntervalValue : USE_DEFAULTS,
|
||||
receiver: receiver,
|
||||
};
|
||||
|
||||
// defaults(newRoute, TIMING_OPTIONS_DEFAULTS)
|
||||
|
||||
// Create the K8s route object
|
||||
const routeObject = createKubernetesRoutingTreeSpec(newRoute);
|
||||
|
||||
return createNamespacedRoutingTree({
|
||||
namespace,
|
||||
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree: cleanKubernetesRouteIDs(routeObject),
|
||||
}).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
const fuzzyFinder = new uFuzzy({
|
||||
intraMode: 1,
|
||||
intraIns: 1,
|
||||
|
||||
Reference in New Issue
Block a user