oncall-engine/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx
2023-12-11 17:59:02 +08:00

395 lines
14 KiB
TypeScript

import React, { ChangeEvent, useCallback, useState } from 'react';
import { ServiceLabels } from '@grafana/labels';
import {
Alert,
Button,
Drawer,
Dropdown,
HorizontalGroup,
InlineSwitch,
Input,
Menu,
VerticalGroup,
} from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Collapse from 'components/Collapse/Collapse';
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
import PluginLink from 'components/PluginLink/PluginLink';
import RenderConditionally from 'components/RenderConditionally/RenderConditionally';
import Text from 'components/Text/Text';
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { LabelsErrors } from 'models/label/label.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import { openErrorNotification } from 'utils';
import { DOCS_ROOT } from 'utils/consts';
import { getIsAddBtnDisabled, getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
import styles from './IntegrationLabelsForm.module.css';
const cx = cn.bind(styles);
const INPUT_WIDTH = 280;
interface IntegrationLabelsFormProps {
id: AlertReceiveChannel['id'];
onSubmit: () => void;
onHide: () => void;
onOpenIntegrationSettings: (id: AlertReceiveChannel['id']) => void;
}
const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
const { id, onHide, onSubmit, onOpenIntegrationSettings } = props;
const store = useStore();
const [showTemplateEditor, setShowTemplateEditor] = useState<boolean>(false);
const [customLabelsErrors, setCustomLabelsErrors] = useState<LabelsErrors>([]);
const [customLabelIndexToShowTemplateEditor, setCustomLabelIndexToShowTemplateEditor] = useState<number>(undefined);
const { alertReceiveChannelStore } = store;
const alertReceiveChannel = alertReceiveChannelStore.items[id];
const templates = alertReceiveChannelStore.templates[id];
const [alertGroupLabels, setAlertGroupLabels] = useState(alertReceiveChannel.alert_group_labels);
const handleSave = async () => {
try {
await alertReceiveChannelStore.saveAlertReceiveChannel(id, { alert_group_labels: alertGroupLabels });
onSubmit();
onHide();
} catch (err) {
if (err.response?.data?.alert_group_labels?.custom) {
setCustomLabelsErrors(err.response.data.alert_group_labels.custom);
}
}
};
const handleOpenIntegrationSettings = () => {
onHide();
onOpenIntegrationSettings(id);
};
const onInheritanceChange = (keyId: ApiSchemas['LabelKey']['id']) => {
setAlertGroupLabels((alertGroupLabels) => ({
...alertGroupLabels,
inheritable: { ...alertGroupLabels.inheritable, [keyId]: !alertGroupLabels.inheritable[keyId] },
}));
};
return (
<>
<Drawer
scrollableContent
title="Alert group labeling"
subtitle={
<Text size="small" className="u-margin-top-xs">
Combination of settings that manage the labeling of alert groups. More information in{' '}
<a href={DOCS_ROOT} target="_blank" rel="noreferrer">
<Text type="link">documentation</Text>
</a>
.
</Text>
}
onClose={onHide}
closeOnMaskClick={false}
width="640px"
>
<VerticalGroup spacing="lg">
<RenderConditionally shouldRender={getIsTooManyLabelsWarningVisible(alertGroupLabels)}>
<Alert title="More than 15 labels added" severity="warning">
We support up to 15 labels per Alert group. Please remove extra labels.
<br />
Otherwise, only the first 15 labels (alphabetically sorted by keys) will be applied.
</Alert>
</RenderConditionally>
<VerticalGroup>
<Text>Integration labels</Text>
{alertReceiveChannel.labels.length ? (
<VerticalGroup spacing="xs">
<Text type="secondary" size="small">
Labels inherited from <PluginLink onClick={handleOpenIntegrationSettings}>the integration</PluginLink>
. This behavior can be disabled using the toggle option.
</Text>
<ul className={cx('labels-list')}>
{alertReceiveChannel.labels.map((label) => (
<li key={label.key.id}>
<HorizontalGroup spacing="xs">
<Input width={INPUT_WIDTH / 8} value={label.key.name} disabled />
<Input width={INPUT_WIDTH / 8} value={label.value.name} disabled />
<InlineSwitch
value={alertGroupLabels.inheritable[label.key.id]}
transparent
onChange={() => onInheritanceChange(label.key.id)}
/>
</HorizontalGroup>
</li>
))}
</ul>
</VerticalGroup>
) : (
<VerticalGroup>
<Text type="secondary">There are no labels to inherit yet</Text>
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
Add labels to the integration
</Text>
</VerticalGroup>
)}
</VerticalGroup>
<CustomLabels
alertGroupLabels={alertGroupLabels}
onChange={(val) => {
setCustomLabelsErrors([]);
setAlertGroupLabels(val);
}}
onShowTemplateEditor={setCustomLabelIndexToShowTemplateEditor}
customLabelsErrors={customLabelsErrors}
/>
<Collapse isOpen={false} label="Multi-label extraction template" contentClassName="u-padding-top-none">
<VerticalGroup>
<HorizontalGroup justify="space-between" style={{ marginBottom: '10px' }}>
<Text type="secondary" size="small" className="u-padding-left-lg">
Allows for the extraction and modification of multiple labels from the alert payload using a single
template. Supports not only dynamic values but also dynamic keys. The Jinja template must result in
valid JSON dictionary.
</Text>
<Button
variant="secondary"
icon="edit"
onClick={() => {
setShowTemplateEditor(true);
}}
/>
</HorizontalGroup>
<MonacoEditor
value={alertGroupLabels.template}
height="200px"
data={{}}
showLineNumbers={false}
language={MONACO_LANGUAGE.jinja2}
onChange={(value) => {
setAlertGroupLabels({ ...alertGroupLabels, template: value });
}}
/>
</VerticalGroup>
</Collapse>
<div className={cx('buttons')}>
<HorizontalGroup justify="flex-end">
<Button variant="secondary" onClick={onHide}>
Close
</Button>
<Button variant="primary" onClick={handleSave}>
Save
</Button>
</HorizontalGroup>
</div>
</VerticalGroup>
</Drawer>
{customLabelIndexToShowTemplateEditor !== undefined && (
<IntegrationTemplate
id={id}
template={{
name: 'alert_group_labels',
displayName: ``,
}}
templates={templates}
templateBody={alertGroupLabels.custom[customLabelIndexToShowTemplateEditor].value.name}
onHide={() => setCustomLabelIndexToShowTemplateEditor(undefined)}
onUpdateTemplates={({ alert_group_labels }) => {
const newCustom = [...alertGroupLabels.custom];
newCustom[customLabelIndexToShowTemplateEditor].value.name = alert_group_labels;
setAlertGroupLabels({
...alertGroupLabels,
custom: newCustom,
});
setCustomLabelIndexToShowTemplateEditor(undefined);
}}
/>
)}
{showTemplateEditor && (
<IntegrationTemplate
id={id}
template={{
name: 'alert_group_labels',
displayName: ``,
}}
templates={templates}
templateBody={alertGroupLabels.template}
onHide={() => setShowTemplateEditor(false)}
onUpdateTemplates={({ alert_group_labels }) => {
setAlertGroupLabels({
...alertGroupLabels,
template: alert_group_labels,
});
setShowTemplateEditor(undefined);
}}
/>
)}
</>
);
});
interface CustomLabelsProps {
alertGroupLabels: AlertReceiveChannel['alert_group_labels'];
customLabelsErrors: LabelsErrors;
onChange: (value: AlertReceiveChannel['alert_group_labels']) => void;
onShowTemplateEditor: (index: number) => void;
}
const CustomLabels = (props: CustomLabelsProps) => {
const { alertGroupLabels, onChange, onShowTemplateEditor, customLabelsErrors } = props;
const { labelsStore } = useStore();
const handleStaticLabelAdd = () => {
onChange({
...alertGroupLabels,
custom: [
...alertGroupLabels.custom,
{
key: { id: undefined, name: undefined },
value: { id: undefined, name: undefined },
},
],
});
};
const handleDynamicLabelAdd = () => {
onChange({
...alertGroupLabels,
custom: [
...alertGroupLabels.custom,
{
key: { id: undefined, name: undefined },
value: { id: null, name: undefined }, // id = null means it's a templated value
},
],
});
};
const cachedOnLoadKeys = useCallback(() => {
let result = undefined;
return async (search?: string) => {
if (!result) {
try {
result = await labelsStore.loadKeys();
} catch (error) {
openErrorNotification('There was an error processing your request. Please try again');
}
}
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
};
}, []);
const cachedOnLoadValuesForKey = useCallback(() => {
let result = undefined;
return async (key: string, search?: string) => {
if (!result) {
try {
const { values } = await labelsStore.loadValuesForKey(key, search);
result = values;
} catch (error) {
openErrorNotification('There was an error processing your request. Please try again');
}
}
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
};
}, []);
return (
<VerticalGroup>
<Text>Dynamic & Static labels</Text>
<Text type="secondary" size="small">
Dynamic: label values are extracted from the alert payload using Jinja. Keys remain static.
<br />
Static: these are not derived from the payload; both key and value are static.
<br />
These labels will not be attached to the integration.
</Text>
<ServiceLabels
isAddingDisabled
loadById
inputWidth={INPUT_WIDTH}
errors={customLabelsErrors}
value={alertGroupLabels.custom}
onLoadKeys={cachedOnLoadKeys()}
onLoadValuesForKey={cachedOnLoadValuesForKey()}
onCreateKey={labelsStore.createKey}
onUpdateKey={labelsStore.updateKey}
onCreateValue={labelsStore.createValue}
onUpdateValue={labelsStore.updateKeyValue}
onUpdateError={(res) => {
if (res?.response?.status === 409) {
openErrorNotification(`Duplicate values are not allowed`);
} else {
openErrorNotification('An error has occurred. Please try again');
}
}}
renderValue={(option, index, renderValueDefault) => {
if (option.value.id === null) {
return (
<Input
placeholder="Jinja2 template"
autoFocus
disabled={!alertGroupLabels.custom[index].key.id}
width={INPUT_WIDTH / 8}
value={option.value.name}
addonAfter={
<Button
variant="secondary"
icon="edit"
onClick={() => {
onShowTemplateEditor(index);
}}
/>
}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const newCustom = [...alertGroupLabels.custom];
newCustom[index].value.name = e.currentTarget.value;
onChange({ ...alertGroupLabels, custom: newCustom });
}}
/>
);
} else {
return renderValueDefault(option, index);
}
}}
onDataUpdate={(value) => {
onChange({
...alertGroupLabels,
custom: value,
});
}}
/>
<Dropdown
overlay={
<Menu>
<Menu.Item label="Static label" onClick={handleStaticLabelAdd} />
<Menu.Item label="Dynamic label" onClick={handleDynamicLabelAdd} />
</Menu>
}
>
<Button variant="secondary" icon="plus" disabled={getIsAddBtnDisabled(alertGroupLabels)}>
Add label
</Button>
</Dropdown>
</VerticalGroup>
);
};
export default IntegrationLabelsForm;