From c28b8664954d271f41885cc0e0b53a0bf5ccaccc Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 11 Dec 2023 08:44:54 +0100 Subject: [PATCH 1/6] Brojd/further labels polishing (#3532) # What this PR does Polishing labels ## Which issue(s) this PR fixes https://github.com/grafana/oncall-private/issues/2355 https://github.com/grafana/oncall-private/issues/2305 https://github.com/grafana/oncall-private/issues/2336 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Innokentii Konstantinov --- grafana-plugin/src/assets/style/utils.css | 24 +++- grafana-plugin/src/components/GForm/GForm.tsx | 12 +- .../src/components/GForm/GForm.types.ts | 6 +- .../IntegrationContactPoint.tsx | 2 +- .../src/components/PluginLink/PluginLink.tsx | 11 +- ...m.config.ts => IntegrationForm.config.tsx} | 14 +- .../IntegrationForm/IntegrationForm.tsx | 31 +++- .../IntegrationLabelsForm.helpers.test.ts | 41 ++++++ .../IntegrationLabelsForm.helpers.ts | 20 +++ .../IntegrationLabelsForm.tsx | 133 ++++++++++++------ .../src/containers/Labels/Labels.tsx | 16 ++- .../OutgoingWebhookForm.tsx | 50 ++++--- .../alert_receive_channel.ts | 2 +- grafana-plugin/src/models/base_store.ts | 14 +- .../src/models/label/label.types.ts | 2 + .../outgoing_webhook/outgoing_webhook.ts | 9 ++ .../src/pages/integration/Integration.tsx | 6 +- .../src/pages/integrations/Integrations.tsx | 5 +- grafana-plugin/src/utils/consts.ts | 1 + grafana-plugin/src/utils/decorators.ts | 2 +- 20 files changed, 310 insertions(+), 91 deletions(-) rename grafana-plugin/src/containers/IntegrationForm/{IntegrationForm.config.ts => IntegrationForm.config.tsx} (76%) create mode 100644 grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts create mode 100644 grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index 68e86e61..d60a6742 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -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 { diff --git a/grafana-plugin/src/components/GForm/GForm.tsx b/grafana-plugin/src/components/GForm/GForm.tsx index fb1c77ec..fdb523f8 100644 --- a/grafana-plugin/src/components/GForm/GForm.tsx +++ b/grafana-plugin/src/components/GForm/GForm.tsx @@ -238,11 +238,13 @@ class GForm extends React.Component { 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} +
+ {onFieldRender + ? onFieldRender(formItem, disabled, formControl, getValues(), (value) => + setValue(formItem.name, value) + ) + : formControl} +
); }; diff --git a/grafana-plugin/src/components/GForm/GForm.types.ts b/grafana-plugin/src/components/GForm/GForm.types.ts index bf9560a8..710c8f2d 100644 --- a/grafana-plugin/src/components/GForm/GForm.types.ts +++ b/grafana-plugin/src/components/GForm/GForm.types.ts @@ -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; diff --git a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx index c270e278..3f93ea65 100644 --- a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx +++ b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx @@ -128,7 +128,7 @@ const IntegrationContactPoint: React.FC<{ Grafana Alerting Contact point - + {isConnectOpen ? : } diff --git a/grafana-plugin/src/components/PluginLink/PluginLink.tsx b/grafana-plugin/src/components/PluginLink/PluginLink.tsx index df846bef..c3c182d9 100644 --- a/grafana-plugin/src/components/PluginLink/PluginLink.tsx +++ b/grafana-plugin/src/components/PluginLink/PluginLink.tsx @@ -14,12 +14,13 @@ interface PluginLinkProps { children: any; query?: Record; target?: string; + onClick?: () => void; } const cx = cn.bind(styles); const PluginLink: FC = (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 = (props) => { (event) => { event.stopPropagation(); - if (disabled) { + if (disabled || onClick) { event.preventDefault(); } + + if (onClick) { + onClick(); + } }, - [disabled] + [disabled, onClick] ); return ( diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.ts b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx similarity index 76% rename from grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.ts rename to grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx index 93fe8cce..fa5aedab 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.ts +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.tsx @@ -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: ( + + ), type: FormItemType.GSelect, extra: { modelName: 'grafanaTeamStore', diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx index d4000f47..efb2c5b7 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx @@ -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; + 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) && (
- + + Labels{id === 'new' ? ' will be ' : ' '}applied to the integration and inherited by alert + groups. +
+ You can modify behaviour in{' '} + {id === 'new' ? ( + 'Alert group labels' + ) : ( + navigateToAlertGroupLabels(id)}>Alert group labels + )}{' '} + drawer. + + } + />
)} @@ -338,8 +357,10 @@ const CustomFieldSectionRenderer: React.FC = ({
- - + + Grafana Alerting Contact point + +
diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts new file mode 100644 index 00000000..73c69188 --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.test.ts @@ -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); + }); +}); diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts new file mode 100644 index 00000000..b98f400d --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts @@ -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?.key.id === undefined || lastItem?.value.id === undefined; +}; diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx index 9d1703c7..bd408a1e 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -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(false); + const [customLabelsErrors, setCustomLabelsErrors] = useState([]); const [customLabelIndexToShowTemplateEditor, setCustomLabelIndexToShowTemplateEditor] = useState(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 ( <> - + + Combination of settings that manage the labeling of alert groups. More information in{' '} + + documentation + + . + + } + onClose={onHide} + closeOnMaskClick={false} + width="640px" + > + + + We support up to 15 labels per Alert group. Please remove extra labels. +
+ Otherwise, only the first 15 labels (alphabetically sorted by keys) will be applied. +
+
- - - - - - + Integration labels {alertReceiveChannel.labels.length ? ( -
    - {alertReceiveChannel.labels.map((label) => ( -
  • - - - - onInheritanceChange(label.key.id)} - /> - -
  • - ))} -
+ + + Labels inherited from the integration + . This behavior can be disabled using the toggle option. + +
    + {alertReceiveChannel.labels.map((label) => ( +
  • + + + + onInheritanceChange(label.key.id)} + /> + +
  • + ))} +
+
) : ( There are no labels to inherit yet @@ -114,14 +146,22 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => { { + setCustomLabelsErrors([]); + setAlertGroupLabels(val); + }} onShowTemplateEditor={setCustomLabelIndexToShowTemplateEditor} + customLabelsErrors={customLabelsErrors} /> - + - Jinja2 template to parse all labels at once + + 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. + diff --git a/grafana-plugin/src/containers/Labels/Labels.tsx b/grafana-plugin/src/containers/Labels/Labels.tsx index b11e1d50..4057df30 100644 --- a/grafana-plugin/src/containers/Labels/Labels.tsx +++ b/grafana-plugin/src/containers/Labels/Labels.tsx @@ -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, Icon, 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['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,14 @@ const Labels = observer( return (
- + {description}
}> + Labels  + + + } + > = observer( - ({ errors, setValue, getValues }) => { - const { hasFeature } = useStore(); - const onDataUpdate: LabelsProps['onDataUpdate'] = (val) => setValue(WebhookFormFieldName.Labels, val); +const CustomFieldSectionRenderer: React.FC = observer(({ setValue, getValues }) => { + const { + hasFeature, + outgoingWebhookStore: { labelsFormErrors }, + } = useStore(); + const onDataUpdate: LabelsProps['onDataUpdate'] = useCallback( + (val) => setValue(WebhookFormFieldName.Labels, val), + [] + ); - return ( - - (WebhookFormFieldName.Labels) || []} - errors={errors?.[WebhookFormFieldName.Labels]} - onDataUpdate={onDataUpdate} - /> - - ); - } -); + return ( + + (WebhookFormFieldName.Labels) || []} + errors={labelsFormErrors} + onDataUpdate={onDataUpdate} + /> + + ); +}); const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => { const history = useHistory(); @@ -93,11 +97,21 @@ const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => { const form = createForm(outgoingWebhookStore.outgoingWebhookPresets, hasFeature(AppFeature.Labels)); const handleSubmit = useCallback( - (data: Partial) => { - (isNewOrCopy ? outgoingWebhookStore.create(data) : outgoingWebhookStore.update(id, data)).then(() => { + async (data: Partial) => { + 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] ); diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index e2792a61..0c08b1d3 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -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) { - const item = await this.update(id, data); + const item = await this.update(id, data, undefined, true); this.items = { ...this.items, diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index 3aa9acc3..50ea6b60 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -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; diff --git a/grafana-plugin/src/models/label/label.types.ts b/grafana-plugin/src/models/label/label.types.ts index 44cbbcd0..e0fed4c3 100644 --- a/grafana-plugin/src/models/label/label.types.ts +++ b/grafana-plugin/src/models/label/label.types.ts @@ -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[] } }>; diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts index 9e869374..770c2d5d 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts @@ -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; + } } diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index ab32cbb4..b4bc5f0d 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -790,6 +790,10 @@ const IntegrationActions: React.FC = ({ 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 = ({ }} onSubmit={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])} id={alertReceiveChannel['id']} - onOpenIntegraionSettings={() => { + onOpenIntegrationSettings={() => { setIsIntegrationSettingsOpen(true); }} /> diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index b4c30e26..fc094637 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -294,6 +294,9 @@ class Integrations extends React.Component }} onSubmit={this.update} id={alertReceiveChannelId} + navigateToAlertGroupLabels={(id: AlertReceiveChannel['id']) => { + this.setState({ alertReceiveChannelId: undefined, alertReceiveChannelIdToShowLabels: id }); + }} /> )} @@ -304,7 +307,7 @@ class Integrations extends React.Component }} onSubmit={this.update} id={alertReceiveChannelIdToShowLabels} - onOpenIntegraionSettings={(id: AlertReceiveChannel['id']) => { + onOpenIntegrationSettings={(id: AlertReceiveChannel['id']) => { this.setState({ alertReceiveChannelId: id }); }} /> diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 1857c08a..34c73908 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -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/'; diff --git a/grafana-plugin/src/utils/decorators.ts b/grafana-plugin/src/utils/decorators.ts index 21f0c78e..dcedd8ff 100644 --- a/grafana-plugin/src/utils/decorators.ts +++ b/grafana-plugin/src/utils/decorators.ts @@ -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; } }; }; From 94f5fce0705a5bccbe58dbafbdde4fc1d199ba64 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 11 Dec 2023 10:11:27 +0100 Subject: [PATCH 2/6] Further improvements on labels (#3536) # What this PR does ## Which issue(s) this PR fixes ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- .../IntegrationLabelsForm.helpers.ts | 2 +- .../IntegrationLabelsForm/IntegrationLabelsForm.tsx | 2 +- grafana-plugin/src/containers/Labels/Labels.tsx | 11 ++--------- .../OutgoingWebhookForm/OutgoingWebhookForm.tsx | 1 + 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts index b98f400d..f1d9d8fd 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers.ts @@ -16,5 +16,5 @@ export const getIsTooManyLabelsWarningVisible = ( export const getIsAddBtnDisabled = ({ custom }: AlertReceiveChannel['alert_group_labels']) => { const lastItem = custom.at(-1); - return lastItem?.key.id === undefined || lastItem?.value.id === undefined; + return lastItem && (lastItem?.key.id === undefined || lastItem?.value.id === undefined); }; diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx index bd408a1e..5bb92a29 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -312,7 +312,7 @@ const CustomLabels = (props: CustomLabelsProps) => { return ( - Dynamic & Static labels ss + Dynamic & Static labels Dynamic: label values are extracted from the alert payload using Jinja. Keys remain static.
diff --git a/grafana-plugin/src/containers/Labels/Labels.tsx b/grafana-plugin/src/containers/Labels/Labels.tsx index 4057df30..ef1aadfb 100644 --- a/grafana-plugin/src/containers/Labels/Labels.tsx +++ b/grafana-plugin/src/containers/Labels/Labels.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react'; import { ServiceLabels, ServiceLabelsProps } from '@grafana/labels'; -import { Field, Icon, Label } from '@grafana/ui'; +import { Field, Label } from '@grafana/ui'; import { isEmpty } from 'lodash-es'; import { observer } from 'mobx-react'; @@ -104,14 +104,7 @@ const Labels = observer( return (
- {description}
}> - Labels  - - - } - > + {description}
}>Labels}> = ob value={getValues(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." /> ); From 885f3e53f545318dc03e95dede2f5ee38c39ec75 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Mon, 11 Dec 2023 17:59:02 +0800 Subject: [PATCH 3/6] Fix labels wording (#3537) --- .../src/containers/IntegrationForm/IntegrationForm.tsx | 2 +- .../IntegrationLabelsForm/IntegrationLabelsForm.tsx | 8 ++++---- grafana-plugin/src/pages/integration/Integration.tsx | 2 +- grafana-plugin/src/pages/integrations/Integrations.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx index efb2c5b7..cd3c62bf 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx @@ -151,7 +151,7 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
You can modify behaviour in{' '} {id === 'new' ? ( - 'Alert group labels' + 'Alert group labeling' ) : ( navigateToAlertGroupLabels(id)}>Alert group labels )}{' '} diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx index 5bb92a29..a83c912b 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -254,7 +254,7 @@ const CustomLabels = (props: CustomLabelsProps) => { const { labelsStore } = useStore(); - const handlePlainLabelAdd = () => { + const handleStaticLabelAdd = () => { onChange({ ...alertGroupLabels, custom: [ @@ -266,7 +266,7 @@ const CustomLabels = (props: CustomLabelsProps) => { ], }); }; - const handleTemplatedLabelAdd = () => { + const handleDynamicLabelAdd = () => { onChange({ ...alertGroupLabels, custom: [ @@ -379,8 +379,8 @@ const CustomLabels = (props: CustomLabelsProps) => { - - + + } > diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index b4bc5f0d..249ea223 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -850,7 +850,7 @@ const IntegrationActions: React.FC = ({ {store.hasFeature(AppFeature.Labels) && (
openLabelsForm()}> - Alert group labels + Alert group labeling
)} diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index fc094637..95af59cb 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -498,7 +498,7 @@ class Integrations extends React.Component {store.hasFeature(AppFeature.Labels) && (
this.onLabelsEditClick(item.id)}> - Alert group labels + Alert group labeling
)} From 3de2b1d4ada35413c592901342de5a66d886ea94 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Mon, 11 Dec 2023 18:25:39 +0800 Subject: [PATCH 4/6] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc47517a..757a953d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From abb698e825926418ccf8afec5d2ffab646517b00 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 11 Dec 2023 15:54:31 +0200 Subject: [PATCH 5/6] Unified logo with IRM, added few minor UI tweaks, bumped labels version (#3531) # What this PR does - Mostly this -> https://github.com/grafana/oncall/issues/1905 ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- .../e2e-tests/integrations/heartbeat.test.ts | 3 +++ grafana-plugin/e2e-tests/utils/schedule.ts | 7 ------- grafana-plugin/package.json | 2 +- grafana-plugin/src/PluginPage.tsx | 8 +++++++- grafana-plugin/src/navbar/Header/Header.module.scss | 12 +++++++++++- grafana-plugin/src/navbar/Header/Header.tsx | 5 +++-- .../src/pages/integrations/Integrations.module.scss | 4 ++++ grafana-plugin/yarn.lock | 8 ++++---- 8 files changed, 33 insertions(+), 16 deletions(-) diff --git a/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts b/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts index 374ca6ae..2ba7e16e 100644 --- a/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts +++ b/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts @@ -7,6 +7,7 @@ const HEARTBEAT_SETTINGS_FORM_TEST_ID = 'heartbeat-settings-form'; test.describe("updating an integration's heartbeat interval works", async () => { const _openHeartbeatSettingsForm = async (page: Page) => { await page.getByTestId('integration-settings-context-menu-wrapper').getByRole('img').click(); + await page.waitForTimeout(1000); await page.getByTestId('integration-heartbeat-settings').click(); }; @@ -29,6 +30,8 @@ test.describe("updating an integration's heartbeat interval works", async () => await heartbeatSettingsForm.getByTestId('update-heartbeat').click(); + await page.waitForTimeout(1000); + await _openHeartbeatSettingsForm(page); const heartbeatIntervalValue = await heartbeatSettingsForm diff --git a/grafana-plugin/e2e-tests/utils/schedule.ts b/grafana-plugin/e2e-tests/utils/schedule.ts index 3627109a..9c5257f6 100644 --- a/grafana-plugin/e2e-tests/utils/schedule.ts +++ b/grafana-plugin/e2e-tests/utils/schedule.ts @@ -20,13 +20,6 @@ export const createOnCallSchedule = async (page: Page, scheduleName: string, use await clickButton({ page, buttonText: 'Add rotation' }); - /** - * Drag the modal such that the "Create" button will always be visible within the viewport. We cannot scroll - * on the modal itself - * https://playwright.dev/docs/input#dragging-manually - */ - await page.locator('.ReactModal__Content .drag-handler').dragTo(page.locator('.page-header__logo')); - await selectDropdownValue({ page, selectType: 'grafanaSelect', diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 60ac20c9..33214b17 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -122,7 +122,7 @@ "@grafana/data": "^9.2.4", "@grafana/faro-web-sdk": "^1.0.0-beta4", "@grafana/faro-web-tracing": "^1.0.0-beta4", - "@grafana/labels": "~1.4.2", + "@grafana/labels": "~1.4.3", "@grafana/runtime": "9.3.0-beta1", "@grafana/ui": "^10.2.0", "@lifeomic/attempt": "^3.0.3", diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx index 4497af92..5b81bdc2 100644 --- a/grafana-plugin/src/PluginPage.tsx +++ b/grafana-plugin/src/PluginPage.tsx @@ -3,8 +3,10 @@ import React from 'react'; import { PluginPageProps, PluginPage as RealPluginPage } from '@grafana/runtime'; import Header from 'navbar/Header/Header'; +import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import { pages } from 'pages'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; +import { DEFAULT_PAGE } from 'utils/consts'; interface AppPluginPageProps extends PluginPageProps { page?: string; @@ -14,10 +16,14 @@ export const PluginPage = (isTopNavbar() ? RealPlugin : PluginPageFallback) as R function RealPlugin(props: AppPluginPageProps): React.ReactNode { const { page } = props; + const isDefaultPage = page === DEFAULT_PAGE; return ( -
+ +
+ + {pages[page]?.text && !pages[page]?.hideTitle && (

{pages[page].text} diff --git a/grafana-plugin/src/navbar/Header/Header.module.scss b/grafana-plugin/src/navbar/Header/Header.module.scss index 78318cca..bd85bd65 100644 --- a/grafana-plugin/src/navbar/Header/Header.module.scss +++ b/grafana-plugin/src/navbar/Header/Header.module.scss @@ -4,7 +4,7 @@ .header-topnavbar { padding-top: 0; - padding-bottom: 36px; + padding-bottom: 12px; } .navbar-heading { @@ -34,6 +34,7 @@ flex-direction: row; column-gap: 8px; row-gap: 8px; + margin-left: -50px; } .irm-icon { @@ -52,3 +53,12 @@ margin-bottom: 0; } } + +.logo-container, +.page-header__img { + height: 32px; +} + +.page-header__title { + margin-bottom: 8px; +} diff --git a/grafana-plugin/src/navbar/Header/Header.tsx b/grafana-plugin/src/navbar/Header/Header.tsx index 00cae379..c7878ae5 100644 --- a/grafana-plugin/src/navbar/Header/Header.tsx +++ b/grafana-plugin/src/navbar/Header/Header.tsx @@ -23,8 +23,8 @@ const Header = observer(() => {
- - Grafana OnCall + + Grafana OnCall
{renderHeading()}
@@ -41,6 +41,7 @@ const Header = observer(() => {

Grafana OnCall

{APP_SUBTITLE}
+ Date: Mon, 11 Dec 2023 17:17:41 +0300 Subject: [PATCH 6/6] Fix schedules invalid dates issue (#3541) # What this PR does Fix schedules invalid dates issue ## Which issue(s) this PR fixes https://github.com/grafana/support-escalations/issues/8084 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- CHANGELOG.md | 4 ++++ grafana-plugin/src/pages/schedule/Schedule.helpers.ts | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 757a953d..b521acd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Fix schedules invalid dates issue ([#support-escalations/issues/8084](https://github.com/grafana/support-escalations/issues/8084)) + ## v1.3.76 (2023-12-11) ### Fixed diff --git a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts index d1139fb4..ddb2de77 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts +++ b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts @@ -16,10 +16,13 @@ const mondayDayOffset = { }; export const getWeekStartString = () => { - if (!config.bootData.user.weekStart || config.bootData.user.weekStart === 'browser') { + const weekStart = (config.bootData.user.weekStart || '').toLowerCase(); + + if (!weekStart || weekStart === 'browser') { return 'monday'; } - return config.bootData.user.weekStart; + + return weekStart; }; export const getNow = (tz: Timezone) => {