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:
Rares Mardare 2024-02-01 13:21:52 +02:00 committed by GitHub
parent c0d9cdfb68
commit bc8a6cb18c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 276 additions and 122 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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}>

View file

@ -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}>

View file

@ -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>
);
};

View file

@ -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>
)}

View file

@ -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} />}

View file

@ -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}>

View file

@ -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;
}

View file

@ -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 && (

View file

@ -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()}>

View file

@ -101,15 +101,9 @@ $LARGE-MARGIN: 24px;
}
.routesSection {
padding-top: 20px;
&__heading {
font-size: 16px;
}
&__add {
margin-bottom: $LARGE-MARGIN;
}
}
:global(.theme-light) {

View file

@ -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) => {

View file

@ -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>