mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
Alerting: Provisioning UI (#50776)
Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
co-authored by
Konrad Lalik
parent
67046c5e79
commit
a37b868da7
@@ -11,6 +11,7 @@ import { useCleanup } from '../../../core/hooks/useCleanup';
|
||||
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
|
||||
import { ProvisionedResource, ProvisioningAlert } from './components/Provisioning';
|
||||
import { AmRootRoute } from './components/amroutes/AmRootRoute';
|
||||
import { AmSpecificRouting } from './components/amroutes/AmSpecificRouting';
|
||||
import { MuteTimingsTable } from './components/amroutes/MuteTimingsTable';
|
||||
@@ -30,8 +31,6 @@ const AmRoutes: FC = () => {
|
||||
const alertManagers = useAlertManagersByPermission('notification');
|
||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
|
||||
|
||||
const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : true;
|
||||
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const fetchConfig = useCallback(() => {
|
||||
@@ -57,6 +56,8 @@ const AmRoutes: FC = () => {
|
||||
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
|
||||
) as AmRouteReceiver[];
|
||||
|
||||
const isProvisioned = useMemo(() => Boolean(config?.route?.provenance), [config?.route]);
|
||||
|
||||
const enterRootRouteEditMode = () => {
|
||||
setIsRootRouteEditMode(true);
|
||||
};
|
||||
@@ -109,6 +110,10 @@ const AmRoutes: FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const readOnly = alertManagerSourceName
|
||||
? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) || isProvisioned
|
||||
: true;
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper pageId="am-routes">
|
||||
<AlertManagerPicker
|
||||
@@ -121,10 +126,12 @@ const AmRoutes: FC = () => {
|
||||
{resultError.message || 'Unknown error.'}
|
||||
</Alert>
|
||||
)}
|
||||
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.RootNotificationPolicy} />}
|
||||
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
|
||||
{result && !resultLoading && !resultError && (
|
||||
<>
|
||||
<AmRootRoute
|
||||
readOnly={readOnly}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
isEditMode={isRootRouteEditMode}
|
||||
onSave={handleSave}
|
||||
|
||||
@@ -38,7 +38,18 @@ const MuteTimings = () => {
|
||||
|
||||
const getMuteTimingByName = useCallback(
|
||||
(id: string): MuteTimeInterval | undefined => {
|
||||
return config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
|
||||
const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
|
||||
|
||||
if (timing) {
|
||||
const provenance = (config?.muteTimeProvenances ?? {})[timing.name];
|
||||
|
||||
return {
|
||||
...timing,
|
||||
provenance,
|
||||
};
|
||||
}
|
||||
|
||||
return timing;
|
||||
},
|
||||
[config]
|
||||
);
|
||||
@@ -60,7 +71,9 @@ const MuteTimings = () => {
|
||||
{() => {
|
||||
if (queryParams['muteName']) {
|
||||
const muteTiming = getMuteTimingByName(String(queryParams['muteName']));
|
||||
return <MuteTimingForm muteTiming={muteTiming} showError={!muteTiming} />;
|
||||
const provenance = muteTiming?.provenance;
|
||||
|
||||
return <MuteTimingForm muteTiming={muteTiming} showError={!muteTiming} provenance={provenance} />;
|
||||
}
|
||||
return <Redirect to="/alerting/routes" />;
|
||||
}}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { AlertQuery } from '../../../types/unified-alerting-dto';
|
||||
|
||||
import { AlertLabels } from './components/AlertLabels';
|
||||
import { DetailsField } from './components/DetailsField';
|
||||
import { ProvisionedResource, ProvisioningAlert } from './components/Provisioning';
|
||||
import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout';
|
||||
import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization';
|
||||
import { RuleDetailsActionButtons } from './components/rules/RuleDetailsActionButtons';
|
||||
@@ -35,7 +36,7 @@ import { AlertingQueryRunner } from './state/AlertingQueryRunner';
|
||||
import { getRulesSourceByName } from './utils/datasource';
|
||||
import { alertRuleToQueries } from './utils/query';
|
||||
import * as ruleId from './utils/rule-id';
|
||||
import { isFederatedRuleGroup } from './utils/rules';
|
||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from './utils/rules';
|
||||
|
||||
type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
|
||||
|
||||
@@ -131,6 +132,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
||||
|
||||
const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim());
|
||||
const isFederatedRule = isFederatedRuleGroup(rule.group);
|
||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
|
||||
return (
|
||||
<RuleViewerLayout wrapInContent={false} title={pageTitle}>
|
||||
@@ -146,6 +148,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
||||
</VerticalGroup>
|
||||
</Alert>
|
||||
)}
|
||||
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.AlertRule} />}
|
||||
<RuleViewerLayoutContent>
|
||||
<div>
|
||||
<h4>
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P
|
||||
);
|
||||
return {
|
||||
template_files: result.data.template_files ?? {},
|
||||
template_file_provenances: result.data.template_file_provenances ?? {},
|
||||
alertmanager_config: result.data.alertmanager_config ?? {},
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Badge } from '@grafana/ui';
|
||||
|
||||
export enum ProvisionedResource {
|
||||
ContactPoint = 'contact point',
|
||||
Template = 'template',
|
||||
MuteTiming = 'mute timing',
|
||||
AlertRule = 'alert rule',
|
||||
RootNotificationPolicy = 'root notification policy',
|
||||
}
|
||||
|
||||
interface ProvisioningAlertProps {
|
||||
resource: ProvisionedResource;
|
||||
}
|
||||
|
||||
export const ProvisioningAlert = ({ resource }: ProvisioningAlertProps) => {
|
||||
return (
|
||||
<Alert title={`This ${resource} cannot be edited through the UI`} severity="info">
|
||||
This {resource} has been provisioned, that means it was created by config. Please contact your server admin to
|
||||
update this {resource}.
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProvisioningBadge = () => {
|
||||
return <Badge text={'Provisioned'} color={'purple'} />;
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
|
||||
import { AmRootRouteForm } from './AmRootRouteForm';
|
||||
import { AmRootRouteRead } from './AmRootRouteRead';
|
||||
@@ -20,6 +19,7 @@ export interface AmRootRouteProps {
|
||||
receivers: AmRouteReceiver[];
|
||||
routes: FormAmRoute;
|
||||
alertManagerSourceName: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const AmRootRoute: FC<AmRootRouteProps> = ({
|
||||
@@ -30,11 +30,11 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
||||
receivers,
|
||||
routes,
|
||||
alertManagerSourceName,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
return (
|
||||
<div className={styles.container} data-testid="am-root-route-container">
|
||||
@@ -42,7 +42,7 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
||||
<h5 className={styles.title}>
|
||||
Root policy - <i>default for all alerts</i>
|
||||
</h5>
|
||||
{!isEditMode && !isReadOnly && (
|
||||
{!isEditMode && !readOnly && (
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary">
|
||||
Edit
|
||||
|
||||
@@ -22,12 +22,14 @@ import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings'
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { AlertManagerPicker } from '../AlertManagerPicker';
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
|
||||
import { MuteTimingTimeInterval } from './MuteTimingTimeInterval';
|
||||
|
||||
interface Props {
|
||||
muteTiming?: MuteTimeInterval;
|
||||
showError?: boolean;
|
||||
provenance?: string;
|
||||
}
|
||||
|
||||
const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => {
|
||||
@@ -56,7 +58,7 @@ const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => {
|
||||
}, [muteTiming]);
|
||||
};
|
||||
|
||||
const MuteTimingForm = ({ muteTiming, showError }: Props) => {
|
||||
const MuteTimingForm = ({ muteTiming, showError, provenance }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const alertManagers = useAlertManagersByPermission('notification');
|
||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
|
||||
@@ -109,11 +111,12 @@ const MuteTimingForm = ({ muteTiming, showError }: Props) => {
|
||||
disabled
|
||||
dataSources={alertManagers}
|
||||
/>
|
||||
{provenance && <ProvisioningAlert resource={ProvisionedResource.MuteTiming} />}
|
||||
{result && !loading && (
|
||||
<FormProvider {...formApi}>
|
||||
<form onSubmit={formApi.handleSubmit(onSubmit)} data-testid="mute-timing-form">
|
||||
{showError && <Alert title="No matching mute timing found" />}
|
||||
<FieldSet label={'Create mute timing'}>
|
||||
<FieldSet label={'Create mute timing'} disabled={Boolean(provenance)}>
|
||||
<Field
|
||||
required
|
||||
label="Name"
|
||||
|
||||
@@ -22,6 +22,7 @@ import { makeAMLink } from '../../utils/misc';
|
||||
import { AsyncRequestState, initialAsyncRequestState } from '../../utils/redux';
|
||||
import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable';
|
||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
|
||||
interface Props {
|
||||
alertManagerSourceName: string;
|
||||
@@ -40,15 +41,24 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
|
||||
|
||||
const items = useMemo((): Array<DynamicTableItemProps<MuteTimeInterval>> => {
|
||||
const muteTimings = result?.alertmanager_config?.mute_time_intervals ?? [];
|
||||
const muteTimingsProvenances = result?.alertmanager_config?.muteTimeProvenances ?? {};
|
||||
|
||||
return muteTimings
|
||||
.filter(({ name }) => (muteTimingNames ? muteTimingNames.includes(name) : true))
|
||||
.map((mute) => {
|
||||
return {
|
||||
id: mute.name,
|
||||
data: mute,
|
||||
data: {
|
||||
...mute,
|
||||
provenance: muteTimingsProvenances[mute.name],
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [result?.alertmanager_config?.mute_time_intervals, muteTimingNames]);
|
||||
}, [
|
||||
result?.alertmanager_config?.mute_time_intervals,
|
||||
result?.alertmanager_config?.muteTimeProvenances,
|
||||
muteTimingNames,
|
||||
]);
|
||||
|
||||
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName);
|
||||
|
||||
@@ -107,13 +117,18 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
const userHasEditPermissions = contextSrv.hasPermission(permissions.update);
|
||||
const userHasDeletePermissions = contextSrv.hasPermission(permissions.delete);
|
||||
const showActions = !hideActions && (userHasEditPermissions || userHasDeletePermissions);
|
||||
|
||||
return useMemo((): Array<DynamicTableColumnProps<MuteTimeInterval>> => {
|
||||
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
renderCell: function renderName({ data }) {
|
||||
return data.name;
|
||||
return (
|
||||
<>
|
||||
{data.name} {data.provenance && <ProvisioningBadge />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
size: '250px',
|
||||
},
|
||||
@@ -128,6 +143,19 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
renderCell: function renderActions({ data }) {
|
||||
if (data.provenance) {
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, {
|
||||
muteName: data.name,
|
||||
})}
|
||||
>
|
||||
<IconButton name="file-alt" title="View mute timing" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { InfoBox } from '@grafana/ui';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
@@ -18,9 +18,9 @@ export const EditReceiverView: FC<Props> = ({ config, receiverName, alertManager
|
||||
const receiver = config.alertmanager_config.receivers?.find(({ name }) => name === receiverName);
|
||||
if (!receiver) {
|
||||
return (
|
||||
<InfoBox severity="error" title="Receiver not found">
|
||||
Sorry, this receiver does not seem to exit.
|
||||
</InfoBox>
|
||||
<Alert severity="error" title="Receiver not found">
|
||||
Sorry, this receiver does not seem to exist.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ interface Props {
|
||||
|
||||
export const EditTemplateView: FC<Props> = ({ config, templateName, alertManagerSourceName }) => {
|
||||
const template = config.template_files?.[templateName];
|
||||
const provenance = config.template_file_provenances?.[templateName];
|
||||
|
||||
if (!template) {
|
||||
return (
|
||||
<InfoBox severity="error" title="Template not found">
|
||||
@@ -25,6 +27,7 @@ export const EditTemplateView: FC<Props> = ({ config, templateName, alertManager
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
config={config}
|
||||
existing={{ name: templateName, content: template }}
|
||||
provenance={provenance}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { isReceiverUsed } from '../../utils/alertmanager';
|
||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { extractNotifierTypeCounts } from '../../utils/receivers';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { ActionIcon } from '../rules/ActionIcon';
|
||||
|
||||
import { ReceiversSection } from './ReceiversSection';
|
||||
@@ -64,6 +65,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
return type;
|
||||
}
|
||||
),
|
||||
provisioned: receiver.grafana_managed_receiver_configs?.some((receiver) => receiver.provenance),
|
||||
})) ?? [],
|
||||
[config, grafanaNotifiers.result]
|
||||
);
|
||||
@@ -102,11 +104,13 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
)}
|
||||
{rows.map((receiver, idx) => (
|
||||
<tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td>{receiver.name}</td>
|
||||
<td>
|
||||
{receiver.name} {receiver.provisioned && <ProvisioningBadge />}
|
||||
</td>
|
||||
<td>{receiver.types.join(', ')}</td>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
{!isVanillaAM && (
|
||||
{!isVanillaAM && !receiver.provisioned && (
|
||||
<>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
@@ -129,7 +133,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
</Authorize>
|
||||
</>
|
||||
)}
|
||||
{isVanillaAM && (
|
||||
{(isVanillaAM || receiver.provisioned) && (
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
data-testid="view"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useForm, Validate } from 'react-hook-form';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, Field, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect
|
||||
import { updateAlertManagerConfigAction } from '../../state/actions';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { ensureDefine } from '../../utils/templates';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
|
||||
interface Values {
|
||||
name: string;
|
||||
@@ -27,9 +28,10 @@ interface Props {
|
||||
existing?: Values;
|
||||
config: AlertManagerCortexConfig;
|
||||
alertManagerSourceName: string;
|
||||
provenance?: string;
|
||||
}
|
||||
|
||||
export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, config }) => {
|
||||
export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, config, provenance }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -100,73 +102,76 @@ export const TemplateForm: FC<Props> = ({ existing, alertManagerSourceName, conf
|
||||
{error.message || (error as any)?.data?.message || String(error)}
|
||||
</Alert>
|
||||
)}
|
||||
<Field label="Template name" error={errors?.name?.message} invalid={!!errors.name?.message} required>
|
||||
<Input
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Required.' },
|
||||
validate: { nameIsUnique: validateNameIsUnique },
|
||||
})}
|
||||
placeholder="Give your template a name"
|
||||
width={42}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
description={
|
||||
<>
|
||||
You can use the{' '}
|
||||
<a
|
||||
href="https://pkg.go.dev/text/template?utm_source=godoc"
|
||||
target="__blank"
|
||||
rel="noreferrer"
|
||||
className={styles.externalLink}
|
||||
>
|
||||
Go templating language
|
||||
</a>
|
||||
.{' '}
|
||||
<a
|
||||
href="https://prometheus.io/blog/2016/03/03/custom-alertmanager-templates/"
|
||||
target="__blank"
|
||||
rel="noreferrer"
|
||||
className={styles.externalLink}
|
||||
>
|
||||
More info about alertmanager templates
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
label="Content"
|
||||
error={errors?.content?.message}
|
||||
invalid={!!errors.content?.message}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
{...register('content', { required: { value: true, message: 'Required.' } })}
|
||||
className={styles.textarea}
|
||||
placeholder="Message"
|
||||
rows={12}
|
||||
/>
|
||||
</Field>
|
||||
<div className={styles.buttons}>
|
||||
{loading && (
|
||||
<Button disabled={true} icon="fa fa-spinner" variant="primary">
|
||||
Saving...
|
||||
</Button>
|
||||
)}
|
||||
{!loading && (
|
||||
<Button type="submit" variant="primary">
|
||||
Save template
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton
|
||||
disabled={loading}
|
||||
href={makeAMLink('alerting/notifications', alertManagerSourceName)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
fill="outline"
|
||||
{provenance && <ProvisioningAlert resource={ProvisionedResource.Template} />}
|
||||
<FieldSet disabled={Boolean(provenance)}>
|
||||
<Field label="Template name" error={errors?.name?.message} invalid={!!errors.name?.message} required>
|
||||
<Input
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Required.' },
|
||||
validate: { nameIsUnique: validateNameIsUnique },
|
||||
})}
|
||||
placeholder="Give your template a name"
|
||||
width={42}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
description={
|
||||
<>
|
||||
You can use the{' '}
|
||||
<a
|
||||
href="https://pkg.go.dev/text/template?utm_source=godoc"
|
||||
target="__blank"
|
||||
rel="noreferrer"
|
||||
className={styles.externalLink}
|
||||
>
|
||||
Go templating language
|
||||
</a>
|
||||
.{' '}
|
||||
<a
|
||||
href="https://prometheus.io/blog/2016/03/03/custom-alertmanager-templates/"
|
||||
target="__blank"
|
||||
rel="noreferrer"
|
||||
className={styles.externalLink}
|
||||
>
|
||||
More info about alertmanager templates
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
label="Content"
|
||||
error={errors?.content?.message}
|
||||
invalid={!!errors.content?.message}
|
||||
required
|
||||
>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
</div>
|
||||
<TextArea
|
||||
{...register('content', { required: { value: true, message: 'Required.' } })}
|
||||
className={styles.textarea}
|
||||
placeholder="Message"
|
||||
rows={12}
|
||||
/>
|
||||
</Field>
|
||||
<div className={styles.buttons}>
|
||||
{loading && (
|
||||
<Button disabled={true} icon="fa fa-spinner" variant="primary">
|
||||
Saving...
|
||||
</Button>
|
||||
)}
|
||||
{!loading && (
|
||||
<Button type="submit" variant="primary">
|
||||
Save template
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton
|
||||
disabled={loading}
|
||||
href={makeAMLink('alerting/notifications', alertManagerSourceName)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
fill="outline"
|
||||
>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
</div>
|
||||
</FieldSet>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { ActionIcon } from '../rules/ActionIcon';
|
||||
|
||||
import { ReceiversSection } from './ReceiversSection';
|
||||
@@ -27,7 +28,15 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
const permissions = getNotificationsPermissions(alertManagerName);
|
||||
|
||||
const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
|
||||
const templateRows = useMemo(() => {
|
||||
const templates = Object.entries(config.template_files);
|
||||
|
||||
return templates.map(([name, template]) => ({
|
||||
name,
|
||||
template,
|
||||
provenance: (config.template_file_provenances ?? {})[name],
|
||||
}));
|
||||
}, [config]);
|
||||
const [templateToDelete, setTemplateToDelete] = useState<string>();
|
||||
|
||||
const deleteTemplate = () => {
|
||||
@@ -66,7 +75,7 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
<td colSpan={3}>No templates defined.</td>
|
||||
</tr>
|
||||
)}
|
||||
{templateRows.map(([name, content], idx) => {
|
||||
{templateRows.map(({ name, template, provenance }, idx) => {
|
||||
const isExpanded = !!expandedTemplates[name];
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
@@ -77,35 +86,49 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
onToggle={() => setExpandedTemplates({ ...expandedTemplates, [name]: !isExpanded })}
|
||||
/>
|
||||
</td>
|
||||
<td>{name}</td>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="edit template"
|
||||
icon="pen"
|
||||
/>
|
||||
<td>
|
||||
{name} {provenance && <ProvisioningBadge />}
|
||||
</td>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
{provenance && (
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="view template"
|
||||
icon="file-alt"
|
||||
/>
|
||||
)}
|
||||
{!provenance && (
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="edit template"
|
||||
icon="pen"
|
||||
/>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<ActionIcon
|
||||
onClick={() => setTemplateToDelete(name)}
|
||||
tooltip="delete template"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<ActionIcon
|
||||
onClick={() => setTemplateToDelete(name)}
|
||||
tooltip="delete template"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
</td>
|
||||
</Authorize>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td></td>
|
||||
<td colSpan={2}>
|
||||
<DetailsField label="Description" horizontal={true}>
|
||||
<pre>{content}</pre>
|
||||
<pre>{template}</pre>
|
||||
</DetailsField>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -62,6 +62,8 @@ export const CloudReceiverForm: FC<Props> = ({ existing, alertManagerSourceName,
|
||||
[config, existing]
|
||||
);
|
||||
|
||||
const readOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isVanillaAM && (
|
||||
@@ -70,6 +72,7 @@ export const CloudReceiverForm: FC<Props> = ({ existing, alertManagerSourceName,
|
||||
</Alert>
|
||||
)}
|
||||
<ReceiverForm<CloudChannelValues>
|
||||
readOnly={readOnly}
|
||||
config={config}
|
||||
onSubmit={onSubmit}
|
||||
initialValues={existingValue}
|
||||
|
||||
+6
-1
@@ -5,7 +5,11 @@ import { Checkbox, Field } from '@grafana/ui';
|
||||
|
||||
import { CommonSettingsComponentProps } from '../../../types/receiver-form';
|
||||
|
||||
export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({ pathPrefix, className }) => {
|
||||
export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({
|
||||
pathPrefix,
|
||||
className,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const { register } = useFormContext();
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -14,6 +18,7 @@ export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({
|
||||
{...register(`${pathPrefix}disableResolveMessage`)}
|
||||
label="Disable resolved message"
|
||||
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
+12
-1
@@ -16,13 +16,14 @@ import {
|
||||
updateAlertManagerConfigAction,
|
||||
} from '../../../state/actions';
|
||||
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
||||
import {
|
||||
formChannelValuesToGrafanaChannelConfig,
|
||||
formValuesToGrafanaReceiver,
|
||||
grafanaReceiverToFormValues,
|
||||
updateConfigWithReceiver,
|
||||
} from '../../../utils/receiver-form';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
||||
|
||||
import { GrafanaCommonChannelSettings } from './GrafanaCommonChannelSettings';
|
||||
import { ReceiverForm } from './ReceiverForm';
|
||||
@@ -108,10 +109,20 @@ export const GrafanaReceiverForm: FC<Props> = ({ existing, alertManagerSourceNam
|
||||
[config, existing]
|
||||
);
|
||||
|
||||
// if any receivers in the contact point have a "provenance", the entire contact point should be readOnly
|
||||
const hasProvisionedItems = existing
|
||||
? (existing.grafana_managed_receiver_configs ?? []).some((item) => Boolean(item.provenance))
|
||||
: false;
|
||||
|
||||
const readOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) || hasProvisionedItems;
|
||||
|
||||
if (grafanaNotifiers.result) {
|
||||
return (
|
||||
<>
|
||||
{hasProvisionedItems && <ProvisioningAlert resource={ProvisionedResource.ContactPoint} />}
|
||||
|
||||
<ReceiverForm<GrafanaChannelValues>
|
||||
readOnly={readOnly}
|
||||
config={config}
|
||||
onSubmit={onSubmit}
|
||||
initialValues={existingValue}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { NotifierDTO } from 'app/types';
|
||||
import { useControlledFieldArray } from '../../../hooks/useControlledFieldArray';
|
||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||
import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form';
|
||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
||||
import { makeAMLink } from '../../../utils/misc';
|
||||
|
||||
import { ChannelSubForm } from './ChannelSubForm';
|
||||
@@ -28,6 +27,7 @@ interface Props<R extends ChannelValues> {
|
||||
takenReceiverNames: string[]; // will validate that user entered receiver name is not one of these
|
||||
commonSettingsComponent: CommonSettingsComponentType;
|
||||
initialValues?: ReceiverFormValues<R>;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export function ReceiverForm<R extends ChannelValues>({
|
||||
@@ -40,10 +40,11 @@ export function ReceiverForm<R extends ChannelValues>({
|
||||
onTestChannel,
|
||||
takenReceiverNames,
|
||||
commonSettingsComponent,
|
||||
readOnly,
|
||||
}: Props<R>): JSX.Element {
|
||||
const notifyApp = useAppNotification();
|
||||
const styles = useStyles2(getStyles);
|
||||
const readOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
const defaultValues = initialValues || {
|
||||
name: '',
|
||||
items: [
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||
import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
||||
@@ -20,7 +19,7 @@ import { Annotation } from '../../utils/constants';
|
||||
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isFederatedRuleGroup } from '../../utils/rules';
|
||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||
|
||||
interface Props {
|
||||
rule: CombinedRule;
|
||||
@@ -43,6 +42,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
const rulesSourceName = getRulesSourceName(rulesSource);
|
||||
|
||||
const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
|
||||
const leftButtons: JSX.Element[] = [];
|
||||
const rightButtons: JSX.Element[] = [];
|
||||
@@ -185,7 +185,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isEditable && rulerRule && !isFederated) {
|
||||
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
|
||||
const sourceName = getRulesSourceName(rulesSource);
|
||||
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
|
||||
|
||||
@@ -222,7 +222,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isRemovable && rulerRule && !isFederated) {
|
||||
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
|
||||
rightButtons.push(
|
||||
<Button
|
||||
className={style.button}
|
||||
@@ -282,10 +282,3 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
});
|
||||
|
||||
function isGrafanaRulerRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||
if (!rule) {
|
||||
return false;
|
||||
}
|
||||
return (rule as RulerGrafanaRuleDTO).grafana_alert != null;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { DynamicTableWithGuidelines } from '../DynamicTableWithGuidelines';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { RuleLocation } from '../RuleLocation';
|
||||
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
@@ -116,6 +118,23 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
|
||||
renderCell: ({ data: rule }) => rule.name,
|
||||
size: 5,
|
||||
},
|
||||
{
|
||||
id: 'provisioned',
|
||||
label: '',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: rule }) => {
|
||||
const rulerRule = rule.rulerRule;
|
||||
const isGrafanaManagedRule = isGrafanaRulerRule(rulerRule);
|
||||
|
||||
if (!isGrafanaManagedRule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provenance = rulerRule.grafana_alert.provenance;
|
||||
return provenance ? <ProvisioningBadge /> : null;
|
||||
},
|
||||
size: '100px',
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
label: 'Health',
|
||||
|
||||
@@ -129,6 +129,7 @@ export const fetchAlertManagerConfigAction = createAsyncThunk(
|
||||
return fetchStatus(alertManagerSourceName).then((status) => ({
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
template_file_provenances: result.template_file_provenances,
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface CloudChannelValues extends ChannelValues {
|
||||
|
||||
export interface GrafanaChannelValues extends ChannelValues {
|
||||
type: NotifierType;
|
||||
provenance?: string;
|
||||
disableResolveMessage: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ function grafanaChannelConfigToFormChannelValues(
|
||||
const values: GrafanaChannelValues = {
|
||||
__id: id,
|
||||
type: channel.type as NotifierType,
|
||||
provenance: channel.provenance,
|
||||
secureSettings: {},
|
||||
settings: { ...channel.settings },
|
||||
secureFields: { ...channel.secureFields },
|
||||
|
||||
@@ -5,6 +5,8 @@ import { DataSourceJsonData } from '@grafana/data';
|
||||
export type AlertManagerCortexConfig = {
|
||||
template_files: Record<string, string>;
|
||||
alertmanager_config: AlertmanagerConfig;
|
||||
/** { [name]: provenance } */
|
||||
template_file_provenances?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TLSConfig = {
|
||||
@@ -72,6 +74,7 @@ export type GrafanaManagedReceiverConfig = {
|
||||
name: string;
|
||||
updated?: string;
|
||||
created?: string;
|
||||
provenance?: string;
|
||||
};
|
||||
|
||||
export type Receiver = {
|
||||
@@ -106,6 +109,8 @@ export type Route = {
|
||||
repeat_interval?: string;
|
||||
routes?: Route[];
|
||||
mute_time_intervals?: string[];
|
||||
/** only the root policy might have a provenance field defined */
|
||||
provenance?: string;
|
||||
};
|
||||
|
||||
export type InhibitRule = {
|
||||
@@ -143,6 +148,8 @@ export type AlertmanagerConfig = {
|
||||
inhibit_rules?: InhibitRule[];
|
||||
receivers?: Receiver[];
|
||||
mute_time_intervals?: MuteTimeInterval[];
|
||||
/** { [name]: provenance } */
|
||||
muteTimeProvenances?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type Matcher = {
|
||||
@@ -300,6 +307,7 @@ export interface TimeInterval {
|
||||
export type MuteTimeInterval = {
|
||||
name: string;
|
||||
time_intervals: TimeInterval[];
|
||||
provenance?: string;
|
||||
};
|
||||
|
||||
export type AlertManagerDataSourceJsonData = DataSourceJsonData & { implementation?: AlertManagerImplementation };
|
||||
|
||||
@@ -182,6 +182,7 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
||||
uid: string;
|
||||
namespace_uid: string;
|
||||
namespace_id: number;
|
||||
provenance?: string;
|
||||
}
|
||||
|
||||
export interface RulerGrafanaRuleDTO {
|
||||
|
||||
Reference in New Issue
Block a user