Alerting: Provisioning UI (#50776)

Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
Gilles De Mey
2022-06-20 10:55:12 -04:00
committed by GitHub
co-authored by Konrad Lalik
parent 67046c5e79
commit a37b868da7
24 changed files with 289 additions and 127 deletions
@@ -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}
@@ -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>
@@ -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 };
+1
View File
@@ -182,6 +182,7 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
uid: string;
namespace_uid: string;
namespace_id: number;
provenance?: string;
}
export interface RulerGrafanaRuleDTO {