Integration incoming tab (#3752)
# What this PR does ## 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)
This commit is contained in:
parent
c0d9cdfb68
commit
bc8a6cb18c
16 changed files with 276 additions and 122 deletions
|
|
@ -46,6 +46,7 @@
|
|||
--background-canvas: #f4f5f5;
|
||||
--background-primary: #fff;
|
||||
--background-secondary: #f4f5f5;
|
||||
--border-medium-color: rgba(36, 41, 46, 0.3);
|
||||
--border-medium: 1px solid rgba(36, 41, 46, 0.3);
|
||||
--border-strong: 1px solid rgba(36, 41, 46, 0.4);
|
||||
--border-weak: 1px solid rgba(36, 41, 46, 0.12);
|
||||
|
|
@ -94,6 +95,7 @@
|
|||
--background-canvas: #111217;
|
||||
--background-primary: #181b1f;
|
||||
--background-secondary: #22252b;
|
||||
--border-medium-color: rgba(204, 204, 220, 0.15);
|
||||
--border-medium: 1px solid rgba(204, 204, 220, 0.15);
|
||||
--border-strong: 1px solid rgba(204, 204, 220, 0.25);
|
||||
--border-weak: 1px solid rgba(204, 204, 220, 0.07);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
.integrationTree__container {
|
||||
margin-left: 32px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: calc(100% - 20px);
|
||||
border: var(--border-weak);
|
||||
margin-top: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.integrationTree__element {
|
||||
margin-left: 6px;
|
||||
margin-left: 16px;
|
||||
visibility: hidden;
|
||||
overflow-y: hidden;
|
||||
height: 0;
|
||||
|
|
@ -21,26 +12,43 @@
|
|||
&--visible {
|
||||
visibility: visible;
|
||||
height: auto;
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.integrationTree__group {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
|
||||
&:not(:last-child)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border: var(--border-weak);
|
||||
margin-top: 4px;
|
||||
margin-left: -19px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.integrationTree__container--timeline-view .integrationTree__group::before {
|
||||
margin-left: -18px;
|
||||
border: 0 !important;
|
||||
width: 4px;
|
||||
background-color: var(--border-medium-color);
|
||||
}
|
||||
|
||||
.integrationTree__icon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
transform: translateY(50%);
|
||||
left: -30px;
|
||||
color: var(--always-gray);
|
||||
width: 25px;
|
||||
height: 32px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
background-color: var(--primary-background) !important;
|
||||
border: 1px solid var(--primary-background);
|
||||
|
|
@ -50,9 +58,24 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 4px;
|
||||
|
||||
path {
|
||||
// this will overwrite all icons below to be gray
|
||||
fill: var(--always-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.number-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--border-medium-color);
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import cn from 'classnames/bind';
|
|||
import { isArray, isUndefined } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
|
||||
import styles from './IntegrationCollapsibleTreeView.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -13,19 +15,24 @@ export interface IntegrationCollapsibleItem {
|
|||
isHidden?: boolean;
|
||||
customIcon?: IconName;
|
||||
canHoverIcon: boolean;
|
||||
isTextIcon?: boolean;
|
||||
collapsedView: (toggle?: () => void) => React.ReactNode; // needs toggle param for toggling on click
|
||||
expandedView: () => React.ReactNode; // for consistency, this is also a function
|
||||
isCollapsible: boolean;
|
||||
iconText?: string;
|
||||
isExpanded?: boolean;
|
||||
startingElemPosition?: string;
|
||||
onStateChange?(isChecked: boolean): void;
|
||||
}
|
||||
|
||||
interface IntegrationCollapsibleTreeViewProps {
|
||||
startingElemPosition?: string;
|
||||
isRouteView?: boolean;
|
||||
configElements: Array<IntegrationCollapsibleItem | IntegrationCollapsibleItem[]>;
|
||||
}
|
||||
|
||||
const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewProps> = observer((props) => {
|
||||
const { configElements } = props;
|
||||
const { configElements, isRouteView } = props;
|
||||
|
||||
const [expandedList, setExpandedList] = useState(getStartingExpandedState());
|
||||
|
||||
|
|
@ -34,7 +41,7 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
|
|||
}, [configElements]);
|
||||
|
||||
return (
|
||||
<div className={cx('integrationTree__container')}>
|
||||
<div className={cx('integrationTree__container', isRouteView ? 'integrationTree__container--timeline-view' : '')}>
|
||||
{configElements
|
||||
.filter((config) => config) // filter out falsy values
|
||||
.map((item: IntegrationCollapsibleItem | IntegrationCollapsibleItem[], idx) => {
|
||||
|
|
@ -53,6 +60,7 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
|
|||
<IntegrationCollapsibleTreeItem
|
||||
item={item}
|
||||
key={idx}
|
||||
elementPosition={idx + 1} // start from 1 instead of 0
|
||||
onClick={() => expandOrCollapseAtPos(expandedList[idx] as boolean, idx)}
|
||||
isExpanded={expandedList[idx] as boolean}
|
||||
/>
|
||||
|
|
@ -103,19 +111,21 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
|
|||
|
||||
const IntegrationCollapsibleTreeItem: React.FC<{
|
||||
item: IntegrationCollapsibleItem;
|
||||
elementPosition?: number;
|
||||
isExpanded: boolean;
|
||||
onClick: () => void;
|
||||
}> = ({ item, isExpanded, onClick }) => {
|
||||
const iconOnClickFn = !item.isCollapsible ? undefined : onClick;
|
||||
}> = ({ item, elementPosition, isExpanded, onClick }) => {
|
||||
const handleIconClick = !item.isCollapsible ? undefined : onClick;
|
||||
|
||||
return (
|
||||
<div className={cx('integrationTree__group', { 'integrationTree__group--hidden': item.isHidden })}>
|
||||
<div className={cx('integrationTree__icon')}>
|
||||
{item.canHoverIcon ? (
|
||||
<IconButton aria-label="" name={getIconName()} onClick={iconOnClickFn} size="lg" />
|
||||
) : (
|
||||
<Icon name={getIconName()} onClick={iconOnClickFn} size="lg" />
|
||||
)}
|
||||
<div
|
||||
className={cx('integrationTree__icon')}
|
||||
style={{
|
||||
transform: `translateY(${item.startingElemPosition || 0})`,
|
||||
}}
|
||||
>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
<div className={cx('integrationTree__element', { 'integrationTree__element--visible': isExpanded })}>
|
||||
{item.expandedView?.()}
|
||||
|
|
@ -126,6 +136,22 @@ const IntegrationCollapsibleTreeItem: React.FC<{
|
|||
</div>
|
||||
);
|
||||
|
||||
function renderIcon() {
|
||||
if (item.isTextIcon && elementPosition) {
|
||||
return (
|
||||
<Text type="primary" customTag="h6" className={cx('number-icon')}>
|
||||
{elementPosition}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.canHoverIcon) {
|
||||
return <IconButton aria-label="" name={getIconName()} onClick={handleIconClick} size="lg" />;
|
||||
}
|
||||
|
||||
return <Icon name={getIconName()} onClick={handleIconClick} size="lg" />;
|
||||
}
|
||||
|
||||
function getIconName(): IconName {
|
||||
if (item.customIcon) {
|
||||
return item.customIcon;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ interface IntegrationInputFieldProps {
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
const IntegrationInputField: React.FC<IntegrationInputFieldProps> = ({
|
||||
isMasked = true,
|
||||
isMasked = false,
|
||||
value,
|
||||
showEye = true,
|
||||
showCopy = true,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ export interface EscalationPolicyProps extends ElementSortableProps {
|
|||
onDelete: (data: EscalationPolicyType) => void;
|
||||
escalationChoices: any[];
|
||||
number: number;
|
||||
backgroundColor: string;
|
||||
backgroundClassName?: string;
|
||||
backgroundHexNumber?: string;
|
||||
isSlackInstalled: boolean;
|
||||
teamStore: GrafanaTeamStore;
|
||||
outgoingWebhookStore: OutgoingWebhookStore;
|
||||
|
|
@ -57,7 +58,7 @@ export interface EscalationPolicyProps extends ElementSortableProps {
|
|||
|
||||
export class EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
||||
render() {
|
||||
const { data, escalationChoices, number, backgroundColor, isDisabled } = this.props;
|
||||
const { data, escalationChoices, number, isDisabled, backgroundClassName, backgroundHexNumber } = this.props;
|
||||
const { id, step, is_final } = data;
|
||||
|
||||
const escalationOption = escalationChoices.find(
|
||||
|
|
@ -70,7 +71,8 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
contentClassName={cx('root')}
|
||||
number={number}
|
||||
textColor={isDisabled ? getVar('--tag-text-success') : undefined}
|
||||
backgroundColor={backgroundColor}
|
||||
backgroundClassName={backgroundClassName}
|
||||
backgroundHexNumber={backgroundHexNumber}
|
||||
>
|
||||
{!isDisabled && (
|
||||
<WithPermissionControlTooltip disableByPaywall userAction={UserActions.EscalationChainsWrite}>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
|||
const { id, step } = data;
|
||||
|
||||
return (
|
||||
<Timeline.Item className={cx('root')} number={number} backgroundColor={color}>
|
||||
<Timeline.Item className={cx('root')} number={number} backgroundHexNumber={color}>
|
||||
<div className={cx('step')}>
|
||||
{!isDisabled && (
|
||||
<WithPermissionControlTooltip disableByPaywall userAction={userAction}>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ interface TextProps extends HTMLAttributes<HTMLElement> {
|
|||
editModalTitle?: string;
|
||||
maxWidth?: string;
|
||||
clickable?: boolean;
|
||||
customTag?: 'h6' | 'span';
|
||||
}
|
||||
|
||||
interface TextInterface extends React.FC<TextProps> {
|
||||
|
|
@ -56,6 +57,7 @@ const Text: TextInterface = (props) => {
|
|||
style,
|
||||
maxWidth,
|
||||
clickable,
|
||||
customTag,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
|
|
@ -81,8 +83,10 @@ const Text: TextInterface = (props) => {
|
|||
setValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const CustomTag = (customTag || `span`) as unknown as React.ComponentType<any>;
|
||||
|
||||
return (
|
||||
<span
|
||||
<CustomTag
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
'root',
|
||||
|
|
@ -152,7 +156,7 @@ const Text: TextInterface = (props) => {
|
|||
</VerticalGroup>
|
||||
</Modal>
|
||||
)}
|
||||
</span>
|
||||
</CustomTag>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ export interface TimelineItemProps {
|
|||
className?: string;
|
||||
contentClassName?: string;
|
||||
isDisabled?: boolean;
|
||||
backgroundColor?: string;
|
||||
backgroundClassName?: string;
|
||||
backgroundHexNumber?: string;
|
||||
textColor?: string;
|
||||
number?: number;
|
||||
badge?: number;
|
||||
|
|
@ -22,14 +23,18 @@ const TimelineItem: React.FC<TimelineItemProps> = ({
|
|||
contentClassName,
|
||||
children,
|
||||
isDisabled,
|
||||
backgroundColor = '#3274D9',
|
||||
backgroundClassName,
|
||||
backgroundHexNumber,
|
||||
textColor = '#ffffff',
|
||||
number,
|
||||
}) => {
|
||||
return (
|
||||
<li className={cx('item', className)}>
|
||||
{!isDisabled && (
|
||||
<div className={cx('dot')} style={{ backgroundColor, color: textColor }}>
|
||||
<div
|
||||
className={cx('dot', backgroundClassName || '')}
|
||||
style={{ backgroundColor: backgroundHexNumber || '', color: textColor }}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Timeline.Item number={0} backgroundColor={getVar('--tag-secondary')} isDisabled={!showLineNumber}>
|
||||
<Timeline.Item number={0} backgroundHexNumber={getVar('--tag-secondary')} isDisabled={!showLineNumber}>
|
||||
<VerticalGroup>
|
||||
{isSlackInstalled && <SlackConnector channelFilterId={channelFilterId} />}
|
||||
{isTelegramInstalled && <TelegramConnector channelFilterId={channelFilterId} />}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import React, { ReactElement, useCallback, useEffect } from 'react';
|
||||
|
||||
import { LoadingPlaceholder, Select } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { LoadingPlaceholder, Select, useStyles2 } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { get } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import EscalationPolicy from 'components/Policy/EscalationPolicy';
|
||||
import EscalationPolicy, { EscalationPolicyProps } from 'components/Policy/EscalationPolicy';
|
||||
import SortableList from 'components/SortableList/SortableList';
|
||||
import Timeline from 'components/Timeline/Timeline';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
|
|
@ -26,10 +28,19 @@ interface EscalationChainStepsProps {
|
|||
offset?: number;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
background: css`
|
||||
background-color: ${theme.colors.success.main};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
||||
const { id, offset = 0, isDisabled = false, addonBefore } = props;
|
||||
|
||||
const store = useStore();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { escalationPolicyStore } = store;
|
||||
|
||||
|
|
@ -80,13 +91,19 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const extraProps: Partial<EscalationPolicyProps> = {};
|
||||
if (isDisabled) {
|
||||
extraProps.backgroundClassName = styles.background;
|
||||
} else {
|
||||
extraProps.backgroundHexNumber = STEP_COLORS[index] || COLOR_RED;
|
||||
}
|
||||
|
||||
return (
|
||||
<EscalationPolicy
|
||||
index={index} // This in here is a MUST for the SortableElement
|
||||
key={`item-${escalationPolicy.id}`}
|
||||
data={escalationPolicy}
|
||||
number={index + offset + 1}
|
||||
backgroundColor={isDisabled ? getVar('--tag-background-success') : STEP_COLORS[index] || COLOR_RED}
|
||||
escalationChoices={escalationPolicyStore.webEscalationChoices}
|
||||
waitDelays={get(escalationPolicyStore.escalationChoices, 'wait_delay.choices', [])}
|
||||
numMinutesInWindowOptions={escalationPolicyStore.numMinutesInWindowOptions}
|
||||
|
|
@ -97,6 +114,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
|||
scheduleStore={store.scheduleStore}
|
||||
outgoingWebhookStore={store.outgoingWebhookStore}
|
||||
isDisabled={isDisabled}
|
||||
{...extraProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
@ -106,7 +124,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
|||
{!isDisabled && (
|
||||
<Timeline.Item
|
||||
number={(escalationPolicyIds?.length || 0) + offset + 1}
|
||||
backgroundColor={isDisabled ? getVar('--tag-background-success') : getVar('--tag-secondary')}
|
||||
backgroundHexNumber={isDisabled ? getVar('--tag-background-success') : getVar('--tag-secondary')}
|
||||
textColor={isDisabled ? getVar('--tag-text-success') : undefined}
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.EscalationChainsWrite}>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
.input {
|
||||
&--short {
|
||||
width: 500px;
|
||||
}
|
||||
&--long {
|
||||
width: 700px;
|
||||
border: var(--border-weak);
|
||||
|
||||
&--align {
|
||||
width: 728px;
|
||||
}
|
||||
}
|
||||
|
||||
.routing-alert {
|
||||
width: 765px;
|
||||
}
|
||||
|
||||
.integrations-actionsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -31,4 +34,16 @@
|
|||
&:hover {
|
||||
background: var(--cards-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.routing-template-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adjust-element-padding {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.default-route-view {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,22 @@ import { SelectableValue } from '@grafana/data';
|
|||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
InlineLabel,
|
||||
VerticalGroup,
|
||||
Icon,
|
||||
Tooltip,
|
||||
ConfirmModal,
|
||||
LoadingPlaceholder,
|
||||
Select,
|
||||
Alert,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu';
|
||||
import IntegrationCollapsibleTreeView, {
|
||||
IntegrationCollapsibleItem,
|
||||
} from 'components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView';
|
||||
import IntegrationBlock from 'components/Integrations/IntegrationBlock';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
|
|
@ -117,56 +120,32 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
}
|
||||
|
||||
const escChainDisplayName = escalationChainStore.items[channelFilter.escalation_chain]?.name;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationBlock
|
||||
noContent={false}
|
||||
key={channelFilterId}
|
||||
heading={
|
||||
<HorizontalGroup justify={'space-between'}>
|
||||
<HorizontalGroup spacing={'md'}>
|
||||
<TooltipBadge
|
||||
borderType="success"
|
||||
text={CommonIntegrationHelper.getRouteConditionWording(channelFilterIds, routeIndex)}
|
||||
tooltipTitle={CommonIntegrationHelper.getRouteConditionTooltipWording(channelFilterIds, routeIndex)}
|
||||
tooltipContent={undefined}
|
||||
className={cx('u-margin-right-xs')}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<RouteButtonsDisplay
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
onItemMove={onItemMove}
|
||||
setRouteIdForDeletion={() => setState({ routeIdForDeletion: channelFilterId })}
|
||||
openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={
|
||||
<VerticalGroup>
|
||||
const getTreeViewElements = () => {
|
||||
const configs: IntegrationCollapsibleItem[] = [
|
||||
{
|
||||
isHidden: false,
|
||||
isCollapsible: false,
|
||||
isExpanded: false,
|
||||
isTextIcon: true,
|
||||
collapsedView: null,
|
||||
canHoverIcon: false,
|
||||
expandedView: () => (
|
||||
<div className={cx('adjust-element-padding')}>
|
||||
{isDefault ? (
|
||||
<Text type="secondary">
|
||||
All unmatched alerts are directed to this route, grouped using the Grouping Template, sent to
|
||||
messengers, and trigger the escalation chain
|
||||
</Text>
|
||||
) : (
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">
|
||||
If the Routing Template is True, group alerts with the Grouping Template, send them to messengers,
|
||||
and trigger the escalation chain.
|
||||
<div className={cx('default-route-view')}>
|
||||
<Text customTag="h6" type="primary">
|
||||
All unmatched alerts are directed to this route, grouped using the Grouping Template, sent to
|
||||
messengers, and trigger the escalation chain
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<VerticalGroup spacing="sm">
|
||||
<Text customTag="h6" type="primary">
|
||||
Use routing template
|
||||
</Text>
|
||||
|
||||
<HorizontalGroup spacing="xs">
|
||||
<InlineLabel
|
||||
width={20}
|
||||
tooltip="Routing Template should be True for the alert to go to this route."
|
||||
>
|
||||
Routing Template
|
||||
</InlineLabel>
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<div className={cx('input', 'input--align')}>
|
||||
<MonacoEditor
|
||||
value={channelFilterTemplate}
|
||||
disabled={true}
|
||||
|
|
@ -183,25 +162,58 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
|
||||
<div className={cx('routing-alert')}>
|
||||
<Alert
|
||||
severity="info"
|
||||
title={
|
||||
(
|
||||
<Text type="primary">
|
||||
If the Routing template evaluates to True, the alert will be grouped with the Grouping
|
||||
template and proceed to the following steps
|
||||
</Text>
|
||||
) as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
IntegrationHelper.hasChatopsInstalled(store) && {
|
||||
isHidden: false,
|
||||
isCollapsible: false,
|
||||
isTextIcon: true,
|
||||
collapsedView: null,
|
||||
canHoverIcon: false,
|
||||
expandedView: () => (
|
||||
<div className={cx('adjust-element-padding')}>
|
||||
<VerticalGroup spacing="sm">
|
||||
<Text customTag="h6" type="primary">
|
||||
Publish to ChatOps
|
||||
</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
isHidden: false,
|
||||
isCollapsible: false,
|
||||
isExpanded: false,
|
||||
isTextIcon: true,
|
||||
collapsedView: null,
|
||||
canHoverIcon: false,
|
||||
expandedView: () => (
|
||||
<div className={cx('adjust-element-padding')}>
|
||||
<VerticalGroup spacing="sm">
|
||||
<Text customTag="h6" type="primary">
|
||||
Trigger escalation chain
|
||||
</Text>
|
||||
|
||||
{IntegrationHelper.hasChatopsInstalled(store) && (
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type="primary">Publish to ChatOps</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
)}
|
||||
|
||||
<VerticalGroup>
|
||||
<div data-testid="escalation-chain-select">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<InlineLabel
|
||||
width={20}
|
||||
tooltip="The escalation chain determines who and when to notify when an alert group starts."
|
||||
>
|
||||
Escalation chain
|
||||
</InlineLabel>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Select
|
||||
isSearchable
|
||||
|
|
@ -264,7 +276,48 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
<ReadOnlyEscalationChain escalationChainId={channelFilter.escalation_chain} />
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationBlock
|
||||
noContent={false}
|
||||
key={channelFilterId}
|
||||
heading={
|
||||
<HorizontalGroup justify={'space-between'}>
|
||||
<HorizontalGroup spacing={'md'}>
|
||||
<TooltipBadge
|
||||
borderType="success"
|
||||
text={CommonIntegrationHelper.getRouteConditionWording(channelFilterIds, routeIndex)}
|
||||
tooltipTitle={CommonIntegrationHelper.getRouteConditionTooltipWording(channelFilterIds, routeIndex)}
|
||||
tooltipContent={undefined}
|
||||
className={cx('u-margin-right-xs')}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<RouteButtonsDisplay
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
onItemMove={onItemMove}
|
||||
setRouteIdForDeletion={() => setState({ routeIdForDeletion: channelFilterId })}
|
||||
openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={
|
||||
<IntegrationCollapsibleTreeView
|
||||
configElements={getTreeViewElements() as any}
|
||||
isRouteView
|
||||
startingElemPosition="0%"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{routeIdForDeletion && (
|
||||
|
|
|
|||
|
|
@ -150,7 +150,10 @@ const PersonalNotificationSettings = observer((props: PersonalNotificationSettin
|
|||
store={store}
|
||||
/>
|
||||
))}
|
||||
<Timeline.Item number={notificationPolicies.length + 1} backgroundColor={getColor(notificationPolicies.length)}>
|
||||
<Timeline.Item
|
||||
number={notificationPolicies.length + 1}
|
||||
backgroundHexNumber={getColor(notificationPolicies.length)}
|
||||
>
|
||||
<div className={cx('step')}>
|
||||
<WithPermissionControlTooltip userAction={userAction}>
|
||||
<Button icon="plus" variant="secondary" fill="text" onClick={getAddNotificationPolicyHandler()}>
|
||||
|
|
|
|||
|
|
@ -101,15 +101,9 @@ $LARGE-MARGIN: 24px;
|
|||
}
|
||||
|
||||
.routesSection {
|
||||
padding-top: 20px;
|
||||
|
||||
&__heading {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__add {
|
||||
margin-bottom: $LARGE-MARGIN;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-light) {
|
||||
|
|
|
|||
|
|
@ -438,13 +438,14 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
const isAlerting = IntegrationHelper.isSpecificIntegration(alertReceiveChannel, 'grafana_alerting');
|
||||
const isLegacyAlerting = IntegrationHelper.isSpecificIntegration(alertReceiveChannel, 'legacy_grafana_alerting');
|
||||
|
||||
return [
|
||||
const configs: Array<IntegrationCollapsibleItem | IntegrationCollapsibleItem[]> = [
|
||||
(isAlerting || isLegacyAlerting) && {
|
||||
isHidden: isLegacyAlerting || contactPoints === null || contactPoints === undefined,
|
||||
isCollapsible: false,
|
||||
customIcon: 'grafana',
|
||||
canHoverIcon: false,
|
||||
collapsedView: null,
|
||||
startingElemPosition: '50%',
|
||||
expandedView: () => <IntegrationContactPoint id={id} />,
|
||||
},
|
||||
{
|
||||
|
|
@ -452,6 +453,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
customIcon: 'plug',
|
||||
canHoverIcon: false,
|
||||
collapsedView: null,
|
||||
startingElemPosition: '50%',
|
||||
expandedView: () => <IntegrationHowToConnect id={id} />,
|
||||
},
|
||||
{
|
||||
|
|
@ -459,6 +461,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
isExpanded: false,
|
||||
isCollapsible: false,
|
||||
canHoverIcon: false,
|
||||
startingElemPosition: '50%',
|
||||
expandedView: () => (
|
||||
<IntegrationBlock
|
||||
noContent
|
||||
|
|
@ -529,10 +532,11 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
collapsedView: undefined,
|
||||
},
|
||||
{
|
||||
customIcon: 'code-branch',
|
||||
customIcon: 'plus',
|
||||
isCollapsible: false,
|
||||
collapsedView: null,
|
||||
canHoverIcon: false,
|
||||
startingElemPosition: '40px',
|
||||
expandedView: () => (
|
||||
<div className={cx('routesSection')}>
|
||||
<VerticalGroup spacing="md">
|
||||
|
|
@ -545,14 +549,18 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
Add route
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
{this.state.isAddingRoute && <LoadingPlaceholder text="Loading..." />}
|
||||
{this.state.isAddingRoute && (
|
||||
<LoadingPlaceholder text="Loading..." className={cx('loadingPlaceholder')} />
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
this.renderRoutesFn(),
|
||||
].filter((opt) => opt);
|
||||
this.renderRoutesFn() as IntegrationCollapsibleItem[],
|
||||
];
|
||||
|
||||
return configs.filter(Boolean);
|
||||
}
|
||||
|
||||
getRoutingTemplate = (channelFilterId: ChannelFilter['id']) => {
|
||||
|
|
@ -626,6 +634,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
({
|
||||
canHoverIcon: true,
|
||||
isCollapsible: true,
|
||||
startingElemPosition: '50%',
|
||||
isExpanded: openRoutes.indexOf(channelFilterId) > -1,
|
||||
onStateChange: (isChecked: boolean) => {
|
||||
const newOpenRoutes = [...openRoutes];
|
||||
|
|
@ -660,7 +669,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
/>
|
||||
),
|
||||
} as IntegrationCollapsibleItem)
|
||||
);
|
||||
) as IntegrationCollapsibleItem[];
|
||||
};
|
||||
|
||||
handleEditRegexpRouteTemplate = (channelFilterId) => {
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
>
|
||||
<TextEllipsisTooltip placement="top" content={item.verbal_name}>
|
||||
<Text type="link" size="medium">
|
||||
<Emoji className={cx('title', TEXT_ELLIPSIS_CLASS)} text={item.verbal_name} />
|
||||
<Emoji className={cx(TEXT_ELLIPSIS_CLASS)} text={item.verbal_name} />
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
</PluginLink>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue