diff --git a/src/assets/style/utils.css b/src/assets/style/utils.css index d60a67422c..38795f9ae2 100644 --- a/src/assets/style/utils.css +++ b/src/assets/style/utils.css @@ -138,14 +138,6 @@ padding-bottom: 24px; } -/* ----- - * Icons - */ - -.icon-exclamation { - color: var(--error-text-color); -} - .loadingPlaceholder { margin-bottom: 0; margin-right: 4px; diff --git a/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.styles.ts b/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.styles.ts index edeed6f3a7..d1986cd750 100644 --- a/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.styles.ts +++ b/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.styles.ts @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { COLORS } from 'styles/utils.styles'; +import { Colors } from 'styles/utils.styles'; export const getIntegrationCollapsibleTreeStyles = (theme: GrafanaTheme2) => { return { @@ -54,7 +54,7 @@ export const getIntegrationCollapsibleTreeStyles = (theme: GrafanaTheme2) => { position: absolute; top: 0; left: -30px; - color: ${COLORS.ALWAYS_GREY}; + color: ${Colors.ALWAYS_GREY}; width: 28px; height: 28px; text-align: center; @@ -70,7 +70,7 @@ export const getIntegrationCollapsibleTreeStyles = (theme: GrafanaTheme2) => { path { // this will overwrite all icons below to be gray - fill: ${COLORS.ALWAYS_GREY}; + fill: ${Colors.ALWAYS_GREY}; } `, diff --git a/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx b/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx index faf746c3de..541d974e0b 100644 --- a/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx +++ b/src/components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal.tsx @@ -6,7 +6,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'; import Emoji from 'react-emoji-render'; import { debounce } from 'throttle-debounce'; -import { MonacoEditor, MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor'; +import { MonacoEditor, MonacoLanguage } from 'components/MonacoEditor/MonacoEditor'; import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; import { PluginLink } from 'components/PluginLink/PluginLink'; import { Text } from 'components/Text/Text'; @@ -74,7 +74,7 @@ export const IntegrationSendDemoAlertModal: React.FC = ({ labels, onClic } /> ) : null; + +interface LabelBadgesProps { + labels: Array; + maxCount?: number; +} + +export const LabelBadges: React.FC = ({ labels = [], maxCount = 3 }) => { + const renderer = (values: LabelBadgesProps['labels']) => { + return ( + + {values.map((label) => ( + + ))} + + ); + }; + + return ( + + {renderer(labels.slice(0, maxCount))} + + maxCount}> + +
{labels.length > maxCount ? `+ ${labels.length - maxCount}` : ``}
+
+
+
+ ); +}; diff --git a/src/components/MonacoEditor/MonacoEditor.tsx b/src/components/MonacoEditor/MonacoEditor.tsx index 1cb7e81f1c..b108eb6b86 100644 --- a/src/components/MonacoEditor/MonacoEditor.tsx +++ b/src/components/MonacoEditor/MonacoEditor.tsx @@ -17,7 +17,7 @@ interface MonacoEditorProps { data: any; showLineNumbers?: boolean; useAutoCompleteList?: boolean; - language?: MONACO_LANGUAGE; + language?: MonacoLanguage; onChange?: (value: string) => void; loading?: boolean; monacoOptions?: any; @@ -26,7 +26,7 @@ interface MonacoEditorProps { codeEditorProps?: Partial>; } -export enum MONACO_LANGUAGE { +export enum MonacoLanguage { json = 'json', jinja2 = 'jinja2', } @@ -46,7 +46,7 @@ export const MonacoEditor: FC = (props) => { onChange, disabled, data, - language = MONACO_LANGUAGE.jinja2, + language = MonacoLanguage.jinja2, useAutoCompleteList = true, focus = true, height = '130px', @@ -79,7 +79,7 @@ export const MonacoEditor: FC = (props) => { editor.focus(); } - if (language === MONACO_LANGUAGE.jinja2) { + if (language === MonacoLanguage.jinja2) { const jinja2Lang = monaco.languages.getLanguages().find((l: { id: string }) => l.id === 'jinja2'); if (!jinja2Lang) { monaco.languages.register({ id: 'jinja2' }); diff --git a/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.styles.ts b/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.styles.ts index c6ab992635..879b106df8 100644 --- a/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.styles.ts +++ b/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.styles.ts @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { COLORS } from 'styles/utils.styles'; +import { Colors } from 'styles/utils.styles'; const radius = '2px' as string; @@ -30,7 +30,7 @@ export const getScheduleQualityProgressBarStyles = (theme: GrafanaTheme2) => { `, row: css` - background-color: ${COLORS.GRAY_8}; + background-color: ${Colors.GRAY_8}; &:first-child, &:first-child > .bar { diff --git a/src/components/Text/Text.styles.ts b/src/components/Text/Text.styles.ts index b91e87078b..220c4303be 100644 --- a/src/components/Text/Text.styles.ts +++ b/src/components/Text/Text.styles.ts @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { COLORS } from 'styles/utils.styles'; +import { Colors } from 'styles/utils.styles'; export const getTextStyles = (theme: GrafanaTheme2) => { return { @@ -38,7 +38,7 @@ export const getTextStyles = (theme: GrafanaTheme2) => { } &--success { - color: ${COLORS.GREEN_5}; + color: ${Colors.GREEN_5}; } &--strong { diff --git a/src/components/UserGroups/UserGroups.styles.ts b/src/components/UserGroups/UserGroups.styles.ts index 81a82c513f..4032faa29c 100644 --- a/src/components/UserGroups/UserGroups.styles.ts +++ b/src/components/UserGroups/UserGroups.styles.ts @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { COLORS } from 'styles/utils.styles'; +import { Colors } from 'styles/utils.styles'; export const getUserGroupStyles = (theme: GrafanaTheme2) => { return { @@ -59,7 +59,7 @@ export const getUserGroupStyles = (theme: GrafanaTheme2) => { icon: css` display: block; - color: ${COLORS.ALWAYS_GREY}; + color: ${Colors.ALWAYS_GREY}; &:hover { color: #fff; diff --git a/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 7ef9376226..7ca7d07ddc 100644 --- a/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -159,7 +159,7 @@ export const ColumnsModal: React.FC = observer( {values.slice(0, 2).map((val) => ( ))} -
{values.length > 2 ? `+ ${values.length - 2}` : ``}
+
{values.length > 2 ? `+ ${values.length - 2}` : ``}
); } diff --git a/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts b/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts index c6bc99cc3f..eee2f3aa14 100644 --- a/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts +++ b/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts @@ -21,9 +21,6 @@ export const getColumnsSelectorWrapperStyles = (theme: GrafanaTheme2) => { removalModal: css` max-width: 500px; `, - totalValuesCount: css` - margin-left: 16px; - `, valuesBlock: css` margin-bottom: 12px; `, diff --git a/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss b/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss index 4f4686cfb9..ebd2a9cf25 100644 --- a/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss +++ b/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss @@ -10,7 +10,7 @@ display: flex; white-space: nowrap; flex-direction: row; - gap: 4px; + gap: 8px; } &__item--large { diff --git a/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx b/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx index 6642c88f3c..63e402ed1e 100644 --- a/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx +++ b/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx @@ -7,9 +7,9 @@ import { observer } from 'mobx-react'; import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; import { PluginLink } from 'components/PluginLink/PluginLink'; import { Text } from 'components/Text/Text'; -import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; import styles from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss'; import { RouteButtonsDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay'; +import { RouteHeading } from 'containers/IntegrationContainers/RouteHeading'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper'; @@ -70,34 +70,13 @@ export const CollapsedIntegrationRouteDisplay: React.FC -
- - {routeWording === 'Default' && Unmatched alerts routed to default route} - {routeWording !== 'Default' && - (channelFilter.filtering_term ? ( - - {channelFilter.filtering_term} - - ) : ( - <> -
- -
- - Routing template not set - - - ))} -
+
= observer( ({ alertReceiveChannelId, @@ -83,7 +104,12 @@ export const ExpandedIntegrationRouteDisplay: React.FC(undefined); + const [labels, setLabels] = useState>([]); + const [labelErrors, setLabelErrors] = useState([]); const [{ isEscalationCollapsed, isRefreshingEscalationChains, routeIdForDeletion }, setState] = useReducer( (state: ExpandedIntegrationRouteDisplayState, newState: Partial) => ({ @@ -105,7 +131,21 @@ export const ExpandedIntegrationRouteDisplay: React.FC { + if (channelFilter && !labels?.length) { + setLabels(channelFilter.filtering_labels); + } + + if (channelFilter && !routingOption) { + setRoutingOption( + (channelFilter.filtering_term_type === FilteringTermType.labels + ? QueryBuilderOptions[0] + : QueryBuilderOptions[1] + ).value + ); + } + }, [channelFilter]); + if (!channelFilter) { return null; } @@ -121,6 +161,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC; } + const hasLabels = store.hasFeature(AppFeature.Labels); const escChainDisplayName = escalationChainStore.items[channelFilter.escalation_chain]?.name; const getTreeViewElements = () => { const configs: IntegrationCollapsibleItem[] = [ @@ -143,41 +184,73 @@ export const ExpandedIntegrationRouteDisplay: React.FC - Use routing template + {hasLabels ? 'Alerts matched by' : 'Use routing template'} - -
- + +
+ +
+ + + + + + + + Alerts by default will not match this route if no labels are being provided + + ) as unknown as string + } + /> + + + +
+ + + + + +
+ +
+
-
@@ -287,23 +360,24 @@ export const ExpandedIntegrationRouteDisplay: React.FC - - - - +
+ + +
setState({ routeIdForDeletion: channelFilterId })} openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)} /> - - +
+
} content={ @@ -337,6 +411,52 @@ export const ExpandedIntegrationRouteDisplay: React.FC ); + async function onLabelsChange(labels: Array) { + setLabelErrors([]); + setLabels(labels); + + if (!areLabelsValid(labels)) { + return; + } + + await alertReceiveChannelStore.saveChannelFilter(channelFilterId, { + filtering_labels: labels, + filtering_term_type: FilteringTermType.labels, + }); + } + + async function onRouteOptionChange(option: string) { + if (option === RoutingOption.TEMPLATE) { + await alertReceiveChannelStore.saveChannelFilter(channelFilterId, { + filtering_term: channelFilter.filtering_term || '', + filtering_term_type: FilteringTermType.jinja2, + }); + } + + if (option === RoutingOption.LABELS) { + await alertReceiveChannelStore.saveChannelFilter(channelFilterId, { + filtering_labels: channelFilter.filtering_labels || [], + filtering_term_type: FilteringTermType.labels, + }); + } + + setRoutingOption(option); + } + + function shouldShowLabelAlert() { + if (!labels?.length) { + return true; + } + if (labels.length === 1) { + return !areLabelsValid(labels); + } + return false; + } + + function areLabelsValid(labels: Array): boolean { + return labels.every((v) => v.key?.id !== undefined && v.value?.id !== undefined); + } + async function onRouteDeleteConfirm() { setState({ routeIdForDeletion: undefined }); onRouteDelete(routeIdForDeletion); diff --git a/src/containers/IntegrationContainers/RouteHeading.tsx b/src/containers/IntegrationContainers/RouteHeading.tsx new file mode 100644 index 0000000000..f55317cee5 --- /dev/null +++ b/src/containers/IntegrationContainers/RouteHeading.tsx @@ -0,0 +1,103 @@ +import React from 'react'; + +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Icon, useStyles2 } from '@grafana/ui'; + +import { LabelBadges } from 'components/LabelsTooltipBadge/LabelsTooltipBadge'; +import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; +import { Text } from 'components/Text/Text'; +import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; +import { ChannelFilter, FilteringTermType } from 'models/channel_filter/channel_filter.types'; +import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper'; +import { AppFeature } from 'state/features'; +import { useStore } from 'state/useStore'; + +interface RouteHeadingProps { + className: string; + routeWording: string; + routeIndex: number; + channelFilter: ChannelFilter; + channelFilterIds: Array; +} + +export const RouteHeading: React.FC = ({ + className, + routeWording, + channelFilterIds, + channelFilter, + routeIndex, +}) => { + const styles = useStyles2(getStyles); + + return ( +
+ + + {routeWording === 'Default' && Unmatched alerts routed to default route} + {routeWording !== 'Default' && } +
+ ); +}; + +const RouteHeadingDisplay: React.FC<{ channelFilter: ChannelFilter }> = ({ channelFilter }) => { + const store = useStore(); + const styles = useStyles2(getStyles); + const hasLabels = store.hasFeature(AppFeature.Labels); + + if (channelFilter?.filtering_term || channelFilter?.filtering_labels) { + return ( + <> + + + {channelFilter.filtering_term} + + + + + + + + ); + } + + return ( + <> +
+ +
+ Routing template not set + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + badge: css` + margin-right: 4px; + `, + + iconExclamation: css` + color: ${theme.colors.error.main}; + `, + + routeHeading: css` + max-width: 80%; + display: block; + text-overflow: ellipsis; + overflow: hidden; + `, + }; +}; diff --git a/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx index df04222b10..bee1cb6227 100644 --- a/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx +++ b/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -16,7 +16,7 @@ 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 { MonacoEditor, MonacoLanguage } from 'components/MonacoEditor/MonacoEditor'; import { PluginLink } from 'components/PluginLink/PluginLink'; import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { Text } from 'components/Text/Text'; @@ -178,7 +178,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps height="200px" data={{}} showLineNumbers={false} - language={MONACO_LANGUAGE.jinja2} + language={MonacoLanguage.jinja2} onChange={(value) => { setAlertGroupLabels({ ...alertGroupLabels, template: value }); }} diff --git a/src/containers/IntegrationTemplate/IntegrationTemplate.tsx b/src/containers/IntegrationTemplate/IntegrationTemplate.tsx index be11565390..a3adcd8cd5 100644 --- a/src/containers/IntegrationTemplate/IntegrationTemplate.tsx +++ b/src/containers/IntegrationTemplate/IntegrationTemplate.tsx @@ -17,8 +17,9 @@ import { } from 'components/CheatSheet/CheatSheet.config'; import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; import { Text } from 'components/Text/Text'; +import { TemplatePage } from 'containers/TemplatePreview/TemplatePreview'; import { TemplateResult } from 'containers/TemplateResult/TemplateResult'; -import { TemplatesAlertGroupsList, TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList'; +import { TemplatesAlertGroupsList } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; @@ -192,7 +193,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
{ return { gray: css` - color: ${COLORS.ALWAYS_GREY}; + color: ${Colors.ALWAYS_GREY}; `, }; }; diff --git a/src/containers/RouteLabelsDisplay/RouteLabelsDisplay.tsx b/src/containers/RouteLabelsDisplay/RouteLabelsDisplay.tsx new file mode 100644 index 0000000000..17743e8ea9 --- /dev/null +++ b/src/containers/RouteLabelsDisplay/RouteLabelsDisplay.tsx @@ -0,0 +1,98 @@ +import React from 'react'; + +import { ServiceLabels } from '@grafana/labels'; +import { Button, VerticalGroup } from '@grafana/ui'; + +import { splitToGroups } from 'models/label/label.helpers'; +import { components } from 'network/oncall-api/autogenerated-api.types'; +import { useStore } from 'state/useStore'; +import { GENERIC_ERROR } from 'utils/consts'; +import { openErrorNotification } from 'utils/utils'; + +interface RouteLabelsDisplayProps { + labels: Array; + labelErrors: any; + onChange: (value: Array) => void; +} + +const INPUT_WIDTH = 280; +const DUPLICATE_ERROR = 'Duplicate values are not allowed'; + +export const RouteLabelsDisplay: React.FC = ({ labels, labelErrors, onChange }) => { + const { labelsStore } = useStore(); + + const onLabelAdd = () => { + onChange([ + ...labels, + { + key: { id: undefined, name: undefined, prescribed: false }, + value: { id: undefined, name: undefined, prescribed: false }, + }, + ]); + }; + + const onLoadKeys = async (search?: string) => { + let result = undefined; + + try { + result = await labelsStore.loadKeys(search); + } catch (error) { + openErrorNotification('There was an error processing your request. Please try again'); + } + + return splitToGroups(result); + }; + + const onLoadValuesForKey = async (key: string, search?: string) => { + let result = undefined; + + try { + const { values } = await labelsStore.loadValuesForKey(key, search); + result = values; + } catch (error) { + openErrorNotification('There was an error processing your request. Please try again'); + } + + return splitToGroups(result); + }; + + return ( + + { + if (res?.response?.status === 409) { + openErrorNotification(DUPLICATE_ERROR); + } else { + openErrorNotification(GENERIC_ERROR); + } + }} + renderValue={(option, index, renderValueDefault) => renderValueDefault(option, index)} + onDataUpdate={(value) => { + onChange([...value]); + }} + getIsKeyEditable={(option) => !option.prescribed} + getIsValueEditable={(option) => !option.prescribed} + /> + + + + ); +}; + +const getIsAddBtnDisabled = (labels: Array = []) => { + const lastItem = labels.at(-1); + return lastItem && (lastItem.key?.id === undefined || lastItem.value?.id === undefined); +}; diff --git a/src/containers/ScheduleSlot/ScheduleSlot.tsx b/src/containers/ScheduleSlot/ScheduleSlot.tsx index 81161a8372..a585f5ee49 100644 --- a/src/containers/ScheduleSlot/ScheduleSlot.tsx +++ b/src/containers/ScheduleSlot/ScheduleSlot.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Button, HorizontalGroup, Icon, Tooltip, useStyles2, VerticalGroup } from '@grafana/ui'; import dayjs from 'dayjs'; import { observer } from 'mobx-react'; -import { COLORS, getLabelCss } from 'styles/utils.styles'; +import { Colors, getLabelCss } from 'styles/utils.styles'; import NonExistentUserName from 'components/NonExistentUserName/NonExistentUserName'; import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; @@ -535,7 +535,7 @@ const getStyles = (theme: GrafanaTheme2) => { return { root: css` height: 28px; - background: ${COLORS.GRAY_8}; + background: ${Colors.GRAY_8}; border-radius: 2px; position: relative; display: flex; diff --git a/src/containers/TemplatePreview/TemplatePreview.tsx b/src/containers/TemplatePreview/TemplatePreview.tsx index 20abf9bebb..93113f5b68 100644 --- a/src/containers/TemplatePreview/TemplatePreview.tsx +++ b/src/containers/TemplatePreview/TemplatePreview.tsx @@ -27,14 +27,14 @@ interface TemplatePreviewProps { alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id']; alertGroupId?: ApiSchemas['AlertGroup']['pk']; outgoingWebhookId?: ApiSchemas['Webhook']['id']; - templatePage: TEMPLATE_PAGE; + templatePage: TemplatePage; } interface ConditionalResult { isResult?: boolean; value?: string; } -export enum TEMPLATE_PAGE { +export enum TemplatePage { Integrations, Webhooks, } @@ -62,7 +62,7 @@ export const TemplatePreview = observer((props: TemplatePreviewProps) => { const handleTemplateBodyChange = useDebouncedCallback(async () => { try { - const data = await (templatePage === TEMPLATE_PAGE.Webhooks + const data = await (templatePage === TemplatePage.Webhooks ? outgoingWebhookStore.renderPreview(outgoingWebhookId, templateName, templateBody, payload) : alertGroupId ? AlertGroupHelper.renderPreview(alertGroupId, templateName, templateBody) diff --git a/src/containers/TemplateResult/TemplateResult.tsx b/src/containers/TemplateResult/TemplateResult.tsx index 5cd6f088f6..ddbf24c9ec 100644 --- a/src/containers/TemplateResult/TemplateResult.tsx +++ b/src/containers/TemplateResult/TemplateResult.tsx @@ -7,7 +7,7 @@ import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesF import { Block } from 'components/GBlock/Block'; import { Text } from 'components/Text/Text'; import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss'; -import { TemplatePreview, TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview'; +import { TemplatePreview, TemplatePage } from 'containers/TemplatePreview/TemplatePreview'; import { ApiSchemas } from 'network/oncall-api/api.types'; const cx = cn.bind(styles); @@ -23,7 +23,7 @@ interface ResultProps { error?: string; onSaveAndFollowLink?: (link: string) => void; templateIsRoute?: boolean; - templatePage?: TEMPLATE_PAGE; + templatePage?: TemplatePage; } export const TemplateResult = (props: ResultProps) => { @@ -37,7 +37,7 @@ export const TemplateResult = (props: ResultProps) => { error, isAlertGroupExisting, onSaveAndFollowLink, - templatePage = TEMPLATE_PAGE.Integrations, + templatePage = TemplatePage.Integrations, } = props; return ( @@ -91,7 +91,7 @@ export const TemplateResult = (props: ResultProps) => {
- ← Select {templatePage === TEMPLATE_PAGE.Webhooks ? 'event' : 'alert group'} or "Use custom payload" + ← Select {templatePage === TemplatePage.Webhooks ? 'event' : 'alert group'} or "Use custom payload"
diff --git a/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index 0b1d0e554e..93f4ec8252 100644 --- a/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -4,10 +4,11 @@ import { Button, HorizontalGroup, Icon, IconButton, Badge, LoadingPlaceholder } import cn from 'classnames/bind'; import { debounce } from 'lodash-es'; -import { MonacoEditor, MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor'; +import { MonacoEditor, MonacoLanguage } from 'components/MonacoEditor/MonacoEditor'; import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; import { Text } from 'components/Text/Text'; import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; +import { TemplatePage } from 'containers/TemplatePreview/TemplatePreview'; import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates'; import { AlertGroupHelper } from 'models/alertgroup/alertgroup.helpers'; import { OutgoingWebhookResponse } from 'models/outgoing_webhook/outgoing_webhook.types'; @@ -18,13 +19,8 @@ import styles from './TemplatesAlertGroupsList.module.css'; const cx = cn.bind(styles); -export enum TEMPLATE_PAGE { - Integrations, - Webhooks, -} - interface TemplatesAlertGroupsListProps { - templatePage: TEMPLATE_PAGE; + templatePage: TemplatePage; templates: AlertTemplatesDTO[]; alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id']; outgoingwebhookId?: ApiSchemas['Webhook']['id']; @@ -58,12 +54,12 @@ export const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) = useEffect(() => { (async () => { - if (templatePage === TEMPLATE_PAGE.Webhooks) { + if (templatePage === TemplatePage.Webhooks) { if (outgoingwebhookId !== 'new') { const res = await store.outgoingWebhookStore.getLastResponses(outgoingwebhookId); setOutgoingWebhookLastResponses(res); } - } else if (templatePage === TEMPLATE_PAGE.Integrations) { + } else if (templatePage === TemplatePage.Integrations) { const result = await AlertGroupHelper.getAlertGroupsForIntegration(alertReceiveChannelId); setAlertGroupsList(result.slice(0, 30)); onLoadAlertGroupsList(result.length > 0); @@ -140,7 +136,7 @@ export const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) = value={null} disabled={true} useAutoCompleteList={false} - language={MONACO_LANGUAGE.json} + language={MonacoLanguage.json} data={templates} monacoOptions={{ ...MONACO_EDITABLE_CONFIG, @@ -168,7 +164,7 @@ export const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) =
- {templatePage === TEMPLATE_PAGE.Webhooks ? renderOutgoingWebhookLastResponses() : renderAlertGroupList()} + {templatePage === TemplatePage.Webhooks ? renderOutgoingWebhookLastResponses() : renderAlertGroupList()}
)} @@ -263,7 +259,7 @@ export const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) = onChange={getChangeHandler()} showLineNumbers useAutoCompleteList={false} - language={MONACO_LANGUAGE.json} + language={MonacoLanguage.json} monacoOptions={MONACO_EDITABLE_CONFIG} />
@@ -301,7 +297,7 @@ export const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) = height="100%" onChange={getChangeHandler()} useAutoCompleteList={false} - language={MONACO_LANGUAGE.json} + language={MonacoLanguage.json} monacoOptions={{ ...MONACO_EDITABLE_CONFIG, readOnly: true, diff --git a/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx b/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx index 36a7d98b21..ce14b3c319 100644 --- a/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx +++ b/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx @@ -9,8 +9,9 @@ import { genericTemplateCheatSheet, webhookPayloadCheatSheet } from 'components/ import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; import { Text } from 'components/Text/Text'; import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss'; +import { TemplatePage } from 'containers/TemplatePreview/TemplatePreview'; import { TemplateResult } from 'containers/TemplateResult/TemplateResult'; -import { TemplatesAlertGroupsList, TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList'; +import { TemplatesAlertGroupsList } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { ApiSchemas } from 'network/oncall-api/api.types'; import { UserActions } from 'utils/authorization/authorization'; @@ -117,7 +118,7 @@ export const WebhooksTemplateEditor: React.FC = ({
= ({ )} { if (!key) { return { key: undefined, values: [] }; } diff --git a/src/models/channel_filter/channel_filter.types.ts b/src/models/channel_filter/channel_filter.types.ts index ea8bf9f30a..f55651ae81 100644 --- a/src/models/channel_filter/channel_filter.types.ts +++ b/src/models/channel_filter/channel_filter.types.ts @@ -2,10 +2,12 @@ import { EscalationChain } from 'models/escalation_chain/escalation_chain.types' import { SlackChannel } from 'models/slack_channel/slack_channel.types'; import { TelegramChannel, TelegramChannelDetails } from 'models/telegram_channel/telegram_channel.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; +import { components } from 'network/oncall-api/autogenerated-api.types'; export enum FilteringTermType { regex, jinja2, + labels, } export interface ChannelFilter { @@ -15,6 +17,7 @@ export interface ChannelFilter { slack_channel?: SlackChannel; telegram_channel?: TelegramChannel['id']; telegram_channel_details?: TelegramChannelDetails; + filtering_labels?: Array; created_at: string; filtering_term: string; filtering_term_as_jinja2: string; diff --git a/src/models/label/label.helpers.ts b/src/models/label/label.helpers.ts index a0c8f1d2cb..fb2d9a23e2 100644 --- a/src/models/label/label.helpers.ts +++ b/src/models/label/label.helpers.ts @@ -1,6 +1,15 @@ import { ApiSchemas } from 'network/oncall-api/api.types'; -export const splitToGroups = (labels: Array | Array) => { +export interface SplitGroupsResult { + name: string; + id: string; + expanded: boolean; + options: Array | Array; +} + +export const splitToGroups = ( + labels: Array | Array +): SplitGroupsResult[] => { return labels?.reduce( (memo, option) => { memo.find(({ name }) => name === (option.prescribed ? 'System' : 'User added')).options.push(option); diff --git a/src/models/label/label.ts b/src/models/label/label.ts index fabef1de11..0ca89ce35d 100644 --- a/src/models/label/label.ts +++ b/src/models/label/label.ts @@ -3,6 +3,7 @@ import { action, makeObservable } from 'mobx'; import { BaseStore } from 'models/base_store'; import { makeRequest } from 'network/network'; import { ApiSchemas } from 'network/oncall-api/api.types'; +import { components } from 'network/oncall-api/autogenerated-api.types'; import { onCallApi } from 'network/oncall-api/http-client'; import { RootStore } from 'state/rootStore'; import { WithGlobalNotification } from 'utils/decorators'; @@ -17,7 +18,7 @@ export class LabelStore extends BaseStore { } @action.bound - public async loadKeys(search = '') { + public async loadKeys(search = ''): Promise> { const { data } = await onCallApi().GET('/labels/keys/', undefined); const filtered = data.filter((k) => k.name.toLowerCase().includes(search.toLowerCase())); @@ -26,11 +27,10 @@ export class LabelStore extends BaseStore { } @action.bound - async loadValuesForKey(key: ApiSchemas['LabelKey']['id'], search = '') { - if (!key) { - return []; - } - + async loadValuesForKey( + key: ApiSchemas['LabelKey']['id'], + search = '' + ): Promise { const result = await makeRequest(`${this.path}id/${key}`, { params: { search }, }); diff --git a/src/pages/incident/Incident.tsx b/src/pages/incident/Incident.tsx index 6c4a60c7ce..3176ca45c2 100644 --- a/src/pages/incident/Incident.tsx +++ b/src/pages/incident/Incident.tsx @@ -27,7 +27,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'; import Emoji from 'react-emoji-render'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import reactStringReplace from 'react-string-replace'; -import { COLORS, getLabelBackgroundTextColorObject } from 'styles/utils.styles'; +import { Colors, getLabelBackgroundTextColorObject } from 'styles/utils.styles'; import { OnCallPluginExtensionPoints } from 'types'; import errorSVG from 'assets/img/error.svg'; @@ -987,7 +987,7 @@ const getStyles = (theme: GrafanaTheme2) => { incidentsContent: css` > div:not(:last-child) { - border-bottom: 1px solid ${COLORS.BORDER}; + border-bottom: 1px solid ${Colors.BORDER}; padding-bottom: 16px; } diff --git a/src/pages/integration/CommonIntegration.helper.ts b/src/pages/integration/CommonIntegration.helper.ts index ac09abc8b7..f00177a834 100644 --- a/src/pages/integration/CommonIntegration.helper.ts +++ b/src/pages/integration/CommonIntegration.helper.ts @@ -1,4 +1,4 @@ -import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { ChannelFilter, FilteringTermType } from 'models/channel_filter/channel_filter.types'; export const CommonIntegrationHelper = { getRouteConditionWording(channelFilters: Array, routeIndex: number): 'Default' | 'Else' | 'If' { @@ -10,12 +10,22 @@ export const CommonIntegrationHelper = { return routeIndex ? 'Else' : 'If'; }, - getRouteConditionTooltipWording(channelFilters: Array, routeIndex: number) { + getRouteConditionTooltipWording( + channelFilters: Array, + routeIndex: number, + filteringTermType: FilteringTermType + ) { const totalCount = Object.keys(channelFilters).length; if (routeIndex === totalCount - 1) { - return 'If the alert payload does not match to the previous routes, it will go to this default route.'; + return 'If the alert payload does not match any of the previous routes, it will stick to the default route.'; } - return 'If Routing Template is True for incoming alert payload, it will be go only to this route. Alert will be grouped based on Grouping Template and escalated'; + + if (filteringTermType === FilteringTermType.labels) { + return 'Alerts will be grouped if they match these labels and then escalated'; + } + + // Templating + return 'Alerts will be grouped based on the evaluation of the route template and then escalated'; }, }; diff --git a/src/pages/integration/Integration.module.scss b/src/pages/integration/Integration.module.scss index c94139a357..8584a2294f 100644 --- a/src/pages/integration/Integration.module.scss +++ b/src/pages/integration/Integration.module.scss @@ -238,3 +238,18 @@ $LARGE-MARGIN: 24px; display: inline-flex; gap: 4px; } + +/* ----- + * Icons + */ + +.icon-exclamation { + color: var(--error-text-color); +} + +.route-heading { + max-width: 80%; + display: block; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/src/pages/integrations/Integrations.styles.tsx b/src/pages/integrations/Integrations.styles.tsx index 031a0150a3..fdd31f9da5 100644 --- a/src/pages/integrations/Integrations.styles.tsx +++ b/src/pages/integrations/Integrations.styles.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { COLORS } from 'styles/utils.styles'; +import { Colors } from 'styles/utils.styles'; export const getIntegrationsStyles = (theme: GrafanaTheme2) => { return { @@ -56,7 +56,7 @@ export const getIntegrationsStyles = (theme: GrafanaTheme2) => { flex-direction: row; &:hover { - background: ${theme.isLight ? 'rgba(244, 245, 245)' : COLORS.GRAY_9}; + background: ${theme.isLight ? 'rgba(244, 245, 245)' : Colors.GRAY_9}; } `, diff --git a/src/styles/utils.styles.ts b/src/styles/utils.styles.ts index 13a1786af6..997ddc6760 100644 --- a/src/styles/utils.styles.ts +++ b/src/styles/utils.styles.ts @@ -116,7 +116,7 @@ export const bem = (...args: string[]) => return (out += '-'); }, ''); -export enum COLORS { +export enum Colors { ALWAYS_GREY = '#ccccdc', GRAY_8 = '#595959', GRAY_9 = '#434343',