commit
a7e0859cba
21 changed files with 316 additions and 97 deletions
|
|
@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
## v1.3.76 (2023-12-11)
|
||||
|
||||
### Fixed
|
||||
|
||||
– Fix minor UI bugs
|
||||
|
||||
## v1.3.75 (2023-12-08)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -36,12 +36,32 @@
|
|||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.u-margin-bottom-none {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.u-margin-bottom-md {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.u-margin-top-xs {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.u-padding-top-md {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.u-margin-bottom-none {
|
||||
margin-bottom: 0;
|
||||
.u-padding-top-none {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.u-padding-left-lg {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.u-padding-vertical-xs {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.u-pull-right {
|
||||
|
|
|
|||
|
|
@ -238,11 +238,13 @@ class GForm extends React.Component<GFormProps, {}> {
|
|||
error={formItem.label ? `${formItem.label} is required` : `${capitalCase(formItem.name)} is required`}
|
||||
description={formItem.description}
|
||||
>
|
||||
{onFieldRender
|
||||
? onFieldRender(formItem, disabled, formControl, getValues(), (value) =>
|
||||
setValue(formItem.name, value)
|
||||
)
|
||||
: formControl}
|
||||
<div className="u-margin-top-xs">
|
||||
{onFieldRender
|
||||
? onFieldRender(formItem, disabled, formControl, getValues(), (value) =>
|
||||
setValue(formItem.name, value)
|
||||
)
|
||||
: formControl}
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export enum FormItemType {
|
||||
'Input' = 'input',
|
||||
'Password' = 'password',
|
||||
|
|
@ -13,10 +15,10 @@ export enum FormItemType {
|
|||
|
||||
export interface FormItem {
|
||||
name: string;
|
||||
label?: string;
|
||||
label?: ReactNode;
|
||||
type: FormItemType;
|
||||
disabled?: boolean;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
placeholder?: string;
|
||||
normalize?: (value: any) => any;
|
||||
isVisible?: (data: any) => any;
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ const IntegrationContactPoint: React.FC<{
|
|||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
<Text type="primary">Grafana Alerting Contact point</Text>
|
||||
<Icon name="info-circle" className={cx('extra-fields__icon')} />
|
||||
<Icon name="info-circle" />
|
||||
</HorizontalGroup>
|
||||
|
||||
{isConnectOpen ? <Icon name="arrow-down" /> : <Icon name="arrow-right" />}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ interface PluginLinkProps {
|
|||
children: any;
|
||||
query?: Record<string, any>;
|
||||
target?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const PluginLink: FC<PluginLinkProps> = (props) => {
|
||||
const { children, query, disabled, className, wrap = true, target } = props;
|
||||
const { children, query, disabled, className, wrap = true, target, onClick } = props;
|
||||
|
||||
const newPath = useMemo(() => getPathFromQueryParams(query), [query]);
|
||||
|
||||
|
|
@ -27,11 +28,15 @@ const PluginLink: FC<PluginLinkProps> = (props) => {
|
|||
(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (disabled) {
|
||||
if (disabled || onClick) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
},
|
||||
[disabled]
|
||||
[disabled, onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Icon, Label, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import { generateAssignToTeamInputDescription } from 'utils/consts';
|
||||
|
||||
|
|
@ -19,8 +23,14 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
},
|
||||
{
|
||||
name: 'team',
|
||||
label: 'Assign to team',
|
||||
description: generateAssignToTeamInputDescription('Integrations'),
|
||||
label: (
|
||||
<Label>
|
||||
<span>Assign to team</span>
|
||||
<Tooltip content={generateAssignToTeamInputDescription('Integrations')} placement="right">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
),
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
RadioButtonGroup,
|
||||
Select,
|
||||
Icon,
|
||||
Label,
|
||||
Field,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -23,6 +22,7 @@ import Collapse from 'components/Collapse/Collapse';
|
|||
import Block from 'components/GBlock/Block';
|
||||
import GForm, { CustomFieldSectionRendererProps } from 'components/GForm/GForm';
|
||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import Labels from 'containers/Labels/Labels';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
|
|
@ -48,6 +48,7 @@ interface IntegrationFormProps {
|
|||
isTableView?: boolean;
|
||||
onHide: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
navigateToAlertGroupLabels: (id: AlertReceiveChannel['id']) => void;
|
||||
}
|
||||
|
||||
const IntegrationForm = observer((props: IntegrationFormProps) => {
|
||||
|
|
@ -56,7 +57,7 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
|
||||
const labelsRef = useRef(null);
|
||||
|
||||
const { id, onHide, onSubmit, isTableView = true } = props;
|
||||
const { id, onHide, onSubmit, isTableView = true, navigateToAlertGroupLabels } = props;
|
||||
const {
|
||||
alertReceiveChannelStore,
|
||||
userStore: { currentUser: user },
|
||||
|
|
@ -139,7 +140,25 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
|
||||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<div className={cx('labels')}>
|
||||
<Labels ref={labelsRef} errors={errors?.labels} value={data.labels} />
|
||||
<Labels
|
||||
ref={labelsRef}
|
||||
errors={errors?.labels}
|
||||
value={data.labels}
|
||||
description={
|
||||
<>
|
||||
Labels{id === 'new' ? ' will be ' : ' '}applied to the integration and inherited by alert
|
||||
groups.
|
||||
<br />
|
||||
You can modify behaviour in{' '}
|
||||
{id === 'new' ? (
|
||||
'Alert group labeling'
|
||||
) : (
|
||||
<PluginLink onClick={() => navigateToAlertGroupLabels(id)}>Alert group labels</PluginLink>
|
||||
)}{' '}
|
||||
drawer.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -338,8 +357,10 @@ const CustomFieldSectionRenderer: React.FC<CustomFieldSectionRendererProps> = ({
|
|||
<div className={cx('extra-fields')}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
<Label>Grafana Alerting Contact point</Label>
|
||||
<Icon name="info-circle" className={cx('extra-fields__icon')} />
|
||||
<Text type="primary" size="small">
|
||||
Grafana Alerting Contact point
|
||||
</Text>
|
||||
<Icon name="info-circle" />
|
||||
</HorizontalGroup>
|
||||
|
||||
<div className={cx('extra-fields__radio')}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
|
||||
|
||||
describe('getIsTooManyLabelsWarningVisible()', () => {
|
||||
const CUSTOM_LABEL = { key: { id: 'c', name: 'c' }, value: { id: 'c', name: 'c' } };
|
||||
|
||||
it('should return false if limit is not exceeded', () => {
|
||||
expect(
|
||||
getIsTooManyLabelsWarningVisible(
|
||||
{
|
||||
inheritable: undefined,
|
||||
custom: undefined,
|
||||
template: null,
|
||||
},
|
||||
3
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
getIsTooManyLabelsWarningVisible(
|
||||
{
|
||||
inheritable: { a: true, b: false },
|
||||
custom: [CUSTOM_LABEL, CUSTOM_LABEL],
|
||||
template: null,
|
||||
},
|
||||
3
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if limit is exceeded', () => {
|
||||
expect(
|
||||
getIsTooManyLabelsWarningVisible(
|
||||
{
|
||||
inheritable: { a: true, b: true },
|
||||
custom: [CUSTOM_LABEL, CUSTOM_LABEL],
|
||||
template: null,
|
||||
},
|
||||
3
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
|
||||
const countNumberOfInheritedAndCustomLabels = (alert_group_labels: AlertReceiveChannel['alert_group_labels']) => {
|
||||
const inheritedCount = alert_group_labels.inheritable
|
||||
? Object.keys(alert_group_labels.inheritable).filter((labelKey) => alert_group_labels.inheritable?.[labelKey])
|
||||
.length
|
||||
: 0;
|
||||
const customCount = alert_group_labels.custom?.length || 0;
|
||||
return inheritedCount + customCount;
|
||||
};
|
||||
|
||||
export const getIsTooManyLabelsWarningVisible = (
|
||||
alert_group_labels: AlertReceiveChannel['alert_group_labels'],
|
||||
limit = 15
|
||||
) => countNumberOfInheritedAndCustomLabels(alert_group_labels) > limit;
|
||||
|
||||
export const getIsAddBtnDisabled = ({ custom }: AlertReceiveChannel['alert_group_labels']) => {
|
||||
const lastItem = custom.at(-1);
|
||||
return lastItem && (lastItem?.key.id === undefined || lastItem?.value.id === undefined);
|
||||
};
|
||||
|
|
@ -2,16 +2,14 @@ import React, { ChangeEvent, useCallback, useState } from 'react';
|
|||
|
||||
import { ServiceLabels } from '@grafana/labels';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
InlineSwitch,
|
||||
Input,
|
||||
Label,
|
||||
Menu,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -19,12 +17,18 @@ 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';
|
||||
|
||||
|
|
@ -36,15 +40,16 @@ interface IntegrationLabelsFormProps {
|
|||
id: AlertReceiveChannel['id'];
|
||||
onSubmit: () => void;
|
||||
onHide: () => void;
|
||||
onOpenIntegraionSettings: (id: AlertReceiveChannel['id']) => void;
|
||||
onOpenIntegrationSettings: (id: AlertReceiveChannel['id']) => void;
|
||||
}
|
||||
|
||||
const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
||||
const { id, onHide, onSubmit, onOpenIntegraionSettings } = props;
|
||||
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;
|
||||
|
|
@ -54,18 +59,22 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
|||
|
||||
const [alertGroupLabels, setAlertGroupLabels] = useState(alertReceiveChannel.alert_group_labels);
|
||||
|
||||
const handleSave = () => {
|
||||
alertReceiveChannelStore.saveAlertReceiveChannel(id, { alert_group_labels: alertGroupLabels });
|
||||
|
||||
onSubmit();
|
||||
|
||||
onHide();
|
||||
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();
|
||||
|
||||
onOpenIntegraionSettings(id);
|
||||
onOpenIntegrationSettings(id);
|
||||
};
|
||||
|
||||
const onInheritanceChange = (keyId: ApiSchemas['LabelKey']['id']) => {
|
||||
|
|
@ -77,31 +86,54 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Drawer scrollableContent title="Alert group labels" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<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>
|
||||
<HorizontalGroup spacing="xs" align="flex-start">
|
||||
<Label>Inherited labels</Label>
|
||||
<Tooltip content="Labels inherited from integration">
|
||||
<Icon name="info-circle" className={cx('extra-fields__icon')} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
<Text>Integration labels</Text>
|
||||
{alertReceiveChannel.labels.length ? (
|
||||
<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 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>
|
||||
|
|
@ -114,14 +146,22 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
|||
|
||||
<CustomLabels
|
||||
alertGroupLabels={alertGroupLabels}
|
||||
onChange={setAlertGroupLabels}
|
||||
onChange={(val) => {
|
||||
setCustomLabelsErrors([]);
|
||||
setAlertGroupLabels(val);
|
||||
}}
|
||||
onShowTemplateEditor={setCustomLabelIndexToShowTemplateEditor}
|
||||
customLabelsErrors={customLabelsErrors}
|
||||
/>
|
||||
|
||||
<Collapse isOpen={false} label="Advanced label templating">
|
||||
<Collapse isOpen={false} label="Multi-label extraction template" contentClassName="u-padding-top-none">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between" style={{ marginBottom: '10px' }}>
|
||||
<Text type="secondary">Jinja2 template to parse all labels at once</Text>
|
||||
<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"
|
||||
|
|
@ -204,16 +244,17 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
|||
|
||||
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 } = props;
|
||||
const { alertGroupLabels, onChange, onShowTemplateEditor, customLabelsErrors } = props;
|
||||
|
||||
const { labelsStore } = useStore();
|
||||
|
||||
const handlePlainLabelAdd = () => {
|
||||
const handleStaticLabelAdd = () => {
|
||||
onChange({
|
||||
...alertGroupLabels,
|
||||
custom: [
|
||||
|
|
@ -225,7 +266,7 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
],
|
||||
});
|
||||
};
|
||||
const handleTemplatedLabelAdd = () => {
|
||||
const handleDynamicLabelAdd = () => {
|
||||
onChange({
|
||||
...alertGroupLabels,
|
||||
custom: [
|
||||
|
|
@ -271,13 +312,19 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing="xs" align="flex-start">
|
||||
<Label>Custom labels</Label>
|
||||
</HorizontalGroup>
|
||||
<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()}
|
||||
|
|
@ -332,13 +379,13 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item label="Plain label" onClick={handlePlainLabelAdd} />
|
||||
<Menu.Item label="Templated label" onClick={handleTemplatedLabelAdd} />
|
||||
<Menu.Item label="Static label" onClick={handleStaticLabelAdd} />
|
||||
<Menu.Item label="Dynamic label" onClick={handleDynamicLabelAdd} />
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" icon="plus">
|
||||
Add
|
||||
<Button variant="secondary" icon="plus" disabled={getIsAddBtnDisabled(alertGroupLabels)}>
|
||||
Add label
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</VerticalGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import { ServiceLabels, ServiceLabelsProps } from '@grafana/labels';
|
||||
import { Field } from '@grafana/ui';
|
||||
import { Field, Label } from '@grafana/ui';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -13,11 +13,12 @@ export interface LabelsProps {
|
|||
value: LabelKeyValue[];
|
||||
errors: any;
|
||||
onDataUpdate?: ServiceLabelsProps['onDataUpdate'];
|
||||
description?: React.ComponentProps<typeof Label>['description'];
|
||||
}
|
||||
|
||||
const Labels = observer(
|
||||
forwardRef(function Labels2(props: LabelsProps, ref) {
|
||||
const { value: defaultValue, errors: propsErrors, onDataUpdate } = props;
|
||||
const { value: defaultValue, errors: propsErrors, onDataUpdate, description } = props;
|
||||
|
||||
// propsErrors are 'external' caused by attaching/detaching labels to oncall entities,
|
||||
// state errors are errors caused by CRUD operations on labels storage
|
||||
|
|
@ -103,7 +104,7 @@ const Labels = observer(
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Field label="Labels">
|
||||
<Field label={<Label description={<div className="u-padding-vertical-xs">{description}</div>}>Labels</Label>}>
|
||||
<ServiceLabels
|
||||
loadById
|
||||
value={value}
|
||||
|
|
@ -132,4 +133,4 @@ function onUpdateError(res) {
|
|||
}
|
||||
}
|
||||
|
||||
export default Labels;
|
||||
export default React.memo(Labels);
|
||||
|
|
|
|||
|
|
@ -57,22 +57,27 @@ export const WebhookTabs = {
|
|||
LastRun: new KeyValuePair('LastRun', 'Last Run'),
|
||||
};
|
||||
|
||||
const CustomFieldSectionRenderer: React.FC<CustomFieldSectionRendererProps> = observer(
|
||||
({ errors, setValue, getValues }) => {
|
||||
const { hasFeature } = useStore();
|
||||
const onDataUpdate: LabelsProps['onDataUpdate'] = (val) => setValue(WebhookFormFieldName.Labels, val);
|
||||
const CustomFieldSectionRenderer: React.FC<CustomFieldSectionRendererProps> = observer(({ setValue, getValues }) => {
|
||||
const {
|
||||
hasFeature,
|
||||
outgoingWebhookStore: { labelsFormErrors },
|
||||
} = useStore();
|
||||
const onDataUpdate: LabelsProps['onDataUpdate'] = useCallback(
|
||||
(val) => setValue(WebhookFormFieldName.Labels, val),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<RenderConditionally shouldRender={hasFeature(AppFeature.Labels)}>
|
||||
<Labels
|
||||
value={getValues<LabelKeyValue[]>(WebhookFormFieldName.Labels) || []}
|
||||
errors={errors?.[WebhookFormFieldName.Labels]}
|
||||
onDataUpdate={onDataUpdate}
|
||||
/>
|
||||
</RenderConditionally>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<RenderConditionally shouldRender={hasFeature(AppFeature.Labels)}>
|
||||
<Labels
|
||||
value={getValues<LabelKeyValue[]>(WebhookFormFieldName.Labels) || []}
|
||||
errors={labelsFormErrors}
|
||||
onDataUpdate={onDataUpdate}
|
||||
description="Labels applied to the webhook will be included in the webhook payload, along with alert group and integration labels."
|
||||
/>
|
||||
</RenderConditionally>
|
||||
);
|
||||
});
|
||||
|
||||
const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => {
|
||||
const history = useHistory();
|
||||
|
|
@ -93,11 +98,21 @@ const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => {
|
|||
const form = createForm(outgoingWebhookStore.outgoingWebhookPresets, hasFeature(AppFeature.Labels));
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(data: Partial<OutgoingWebhook>) => {
|
||||
(isNewOrCopy ? outgoingWebhookStore.create(data) : outgoingWebhookStore.update(id, data)).then(() => {
|
||||
async (data: Partial<OutgoingWebhook>) => {
|
||||
try {
|
||||
if (isNewOrCopy) {
|
||||
await outgoingWebhookStore.create(data);
|
||||
} else {
|
||||
await outgoingWebhookStore.update(id, data);
|
||||
}
|
||||
outgoingWebhookStore.setLabelsFormErrors(undefined);
|
||||
onHide();
|
||||
onUpdate();
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.response?.data?.labels) {
|
||||
outgoingWebhookStore.setLabelsFormErrors(err.response.data.labels);
|
||||
}
|
||||
}
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
@action.bound
|
||||
@WithGlobalNotification({ success: 'Integration has been saved', failure: 'Failed to save integration' })
|
||||
async saveAlertReceiveChannel(id: AlertReceiveChannel['id'], data: Partial<AlertReceiveChannel>) {
|
||||
const item = await this.update(id, data);
|
||||
const item = await this.update(id, data, undefined, true);
|
||||
|
||||
this.items = {
|
||||
...this.items,
|
||||
|
|
|
|||
|
|
@ -20,13 +20,23 @@ export default class BaseStore {
|
|||
|
||||
if (error.response.status >= 400 && error.response.status < 500) {
|
||||
const payload = error.response.data;
|
||||
|
||||
const text =
|
||||
typeof payload === 'string'
|
||||
? payload
|
||||
: Object.keys(payload)
|
||||
.map((key) => `${sentenceCase(key)}: ${payload[key]}`)
|
||||
.map((key) => {
|
||||
const candidate = `${sentenceCase(key)}: ${payload[key]}`;
|
||||
if (candidate.includes('object Object')) {
|
||||
return undefined;
|
||||
}
|
||||
return candidate;
|
||||
})
|
||||
.join('\n');
|
||||
openWarningNotification(text);
|
||||
|
||||
if (text?.length) {
|
||||
openWarningNotification(text);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ export interface LabelKeyValue {
|
|||
key: ApiSchemas['LabelKey'];
|
||||
value: ApiSchemas['LabelValue'];
|
||||
}
|
||||
|
||||
export type LabelsErrors = Array<{ key?: { id: string[]; name: string[] }; value?: { id: string[]; name: string[] } }>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { action, observable } from 'mobx';
|
||||
|
||||
import BaseStore from 'models/base_store';
|
||||
import { LabelsErrors } from 'models/label/label.types';
|
||||
import { makeRequest } from 'network';
|
||||
import { RootStore } from 'state';
|
||||
|
||||
|
|
@ -16,6 +17,9 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
@observable.shallow
|
||||
outgoingWebhookPresets: OutgoingWebhookPreset[] = [];
|
||||
|
||||
@observable
|
||||
labelsFormErrors?: LabelsErrors;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore);
|
||||
|
||||
|
|
@ -106,4 +110,9 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
const response = await makeRequest(`/webhooks/preset_options/`, {});
|
||||
this.outgoingWebhookPresets = response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
setLabelsFormErrors(errors: LabelsErrors) {
|
||||
this.labelsFormErrors = errors;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -790,6 +790,10 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
onHide={() => setIsIntegrationSettingsOpen(false)}
|
||||
onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])}
|
||||
id={alertReceiveChannel['id']}
|
||||
navigateToAlertGroupLabels={(_id: AlertReceiveChannel['id']) => {
|
||||
setIsIntegrationSettingsOpen(false);
|
||||
setLabelsFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -800,7 +804,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
}}
|
||||
onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])}
|
||||
id={alertReceiveChannel['id']}
|
||||
onOpenIntegraionSettings={() => {
|
||||
onOpenIntegrationSettings={() => {
|
||||
setIsIntegrationSettingsOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -846,7 +850,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')} onClick={() => openLabelsForm()}>
|
||||
<Text type="primary">Alert group labels</Text>
|
||||
<Text type="primary">Alert group labeling</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -294,6 +294,9 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
}}
|
||||
onSubmit={this.update}
|
||||
id={alertReceiveChannelId}
|
||||
navigateToAlertGroupLabels={(id: AlertReceiveChannel['id']) => {
|
||||
this.setState({ alertReceiveChannelId: undefined, alertReceiveChannelIdToShowLabels: id });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -304,7 +307,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
}}
|
||||
onSubmit={this.update}
|
||||
id={alertReceiveChannelIdToShowLabels}
|
||||
onOpenIntegraionSettings={(id: AlertReceiveChannel['id']) => {
|
||||
onOpenIntegrationSettings={(id: AlertReceiveChannel['id']) => {
|
||||
this.setState({ alertReceiveChannelId: id });
|
||||
}}
|
||||
/>
|
||||
|
|
@ -495,7 +498,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integrations-actionItem')} onClick={() => this.onLabelsEditClick(item.id)}>
|
||||
<Text type="primary">Alert group labels</Text>
|
||||
<Text type="primary">Alert group labeling</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export const FARO_ENDPOINT_OPS =
|
|||
export const FARO_ENDPOINT_PROD =
|
||||
'https://faro-collector-prod-us-central-0.grafana.net/collect/03a11ed03c3af04dcfc3be9755f2b053';
|
||||
|
||||
export const DOCS_ROOT = 'https://grafana.com/docs/oncall/latest';
|
||||
export const DOCS_SLACK_SETUP = 'https://grafana.com/docs/oncall/latest/open-source/#slack-setup';
|
||||
export const DOCS_TELEGRAM_SETUP = 'https://grafana.com/docs/oncall/latest/notify/telegram/';
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function WithGlobalNotification({
|
|||
const open = failureType === 'error' ? openErrorNotification : openWarningNotification;
|
||||
const message = composeFailureMessageFn ? composeFailureMessageFn(err) : failure;
|
||||
open(message);
|
||||
throw new Error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue