Brojd/connect integration to snow (#3968)
# What this PR does https://github.com/grafana/oncall/assets/12073649/0dad62c2-d722-4f5b-aee6-549dc97902cd ## 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)
This commit is contained in:
parent
f886fee93c
commit
7794246efb
16 changed files with 188 additions and 60 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import semver from 'semver';
|
||||
|
||||
import { test, expect } from '../fixtures';
|
||||
import { resolveFiringAlert } from '../utils/alertGroup';
|
||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect, Page, Locator } from '../fixtures';
|
||||
import { verifyThatAlertGroupIsRoutedCorrectlyButNotEscalated } from '../utils/alertGroup';
|
||||
import { EscalationStep, createEscalationChain } from '../utils/escalationChain';
|
||||
import { clickButton, generateRandomValue, selectDropdownValue } from '../utils/forms';
|
||||
import { generateRandomValue, selectDropdownValue } from '../utils/forms';
|
||||
import {
|
||||
assignEscalationChainToIntegration,
|
||||
createIntegration,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import styles from './GTable.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export interface Props<RecordType = unknown> extends TableProps<RecordType> {
|
||||
export interface GTableProps<RecordType = unknown> extends TableProps<RecordType> {
|
||||
pagination?: {
|
||||
page: number;
|
||||
total: number;
|
||||
|
|
@ -31,7 +31,7 @@ export interface Props<RecordType = unknown> extends TableProps<RecordType> {
|
|||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props: Props<RT>): ReactElement => {
|
||||
export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props: GTableProps<RT>): ReactElement => {
|
||||
const {
|
||||
columns: columnsProp,
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -39,5 +39,6 @@ export const getStyles = () => ({
|
|||
}),
|
||||
disabledBadge: css({
|
||||
wordBreak: 'keep-all',
|
||||
marginLeft: '8px',
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ export const AlertReceiveChannelCard = observer((props: AlertReceiveChannelCardP
|
|||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
alertReceiveChannel
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class AlertReceiveChannelHelper {
|
|||
: undefined;
|
||||
}
|
||||
|
||||
static getIntegration(
|
||||
static getIntegrationSelectOption(
|
||||
store: AlertReceiveChannelStore,
|
||||
alertReceiveChannel: Partial<ApiSchemas['AlertReceiveChannel'] | ApiSchemas['FastAlertReceiveChannel']>
|
||||
): SelectOption {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
|||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { Heartbeat } from 'models/heartbeat/heartbeat.types';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
|
@ -12,7 +13,7 @@ import { operations } from 'network/oncall-api/autogenerated-api.types';
|
|||
import { onCallApi } from 'network/oncall-api/http-client';
|
||||
import { move } from 'state/helpers';
|
||||
import { RootBaseStore } from 'state/rootBaseStore/RootBaseStore';
|
||||
import { WithGlobalNotification } from 'utils/decorators';
|
||||
import { AutoLoadingState, WithGlobalNotification } from 'utils/decorators';
|
||||
import { OmitReadonlyMembers } from 'utils/types';
|
||||
|
||||
import { AlertReceiveChannelCounters, ContactPoint } from './alert_receive_channel.types';
|
||||
|
|
@ -124,20 +125,23 @@ export class AlertReceiveChannelStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
@AutoLoadingState(ActionKey.FETCH_INTEGRATIONS)
|
||||
async fetchPaginatedItems({
|
||||
filters,
|
||||
page = 1,
|
||||
shouldFetchCounters = false,
|
||||
invalidateFn = undefined,
|
||||
perpage,
|
||||
}: {
|
||||
filters: operations['alert_receive_channels_list']['parameters']['query'];
|
||||
page: number;
|
||||
shouldFetchCounters: boolean;
|
||||
invalidateFn: () => boolean;
|
||||
page?: number;
|
||||
shouldFetchCounters?: boolean;
|
||||
invalidateFn?: () => boolean;
|
||||
perpage?: number;
|
||||
}) {
|
||||
const {
|
||||
data: { count, results, page_size },
|
||||
} = await onCallApi().GET('/alert_receive_channels/', { params: { query: { ...filters, page } } });
|
||||
} = await onCallApi().GET('/alert_receive_channels/', { params: { query: { ...filters, page, perpage } } });
|
||||
|
||||
if (invalidateFn?.()) {
|
||||
return undefined;
|
||||
|
|
@ -173,6 +177,10 @@ export class AlertReceiveChannelStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
resetPaginatedResults() {
|
||||
this.paginatedSearchResult = {};
|
||||
}
|
||||
|
||||
populateHearbeats(alertReceiveChannels: Array<ApiSchemas['AlertReceiveChannelPolymorphic']>) {
|
||||
const heartbeats = alertReceiveChannels.reduce(
|
||||
(acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ export enum ActionKey {
|
|||
FETCH_INCIDENTS_POLLING = 'FETCH_INCIDENTS_POLLING',
|
||||
FETCH_INCIDENTS_AND_STATS = 'FETCH_INCIDENTS_AND_STATS',
|
||||
UPDATE_FILTERS_AND_FETCH_INCIDENTS = 'UPDATE_FILTERS_AND_FETCH_INCIDENTS',
|
||||
FETCH_INTEGRATIONS = 'FETCH_INTEGRATIONS',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
} = this.props;
|
||||
const { alerts } = store.alertGroupStore;
|
||||
const incident = alerts.get(id);
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
store.alertReceiveChannelStore,
|
||||
incident.alert_receive_channel
|
||||
);
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
record.alert_receive_channel
|
||||
);
|
||||
|
|
|
|||
|
|
@ -152,7 +152,10 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
);
|
||||
}
|
||||
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
alertReceiveChannel
|
||||
);
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
const isLegacyIntegration = integration && (integration?.value as string).toLowerCase().startsWith('legacy_');
|
||||
const contactPoints = alertReceiveChannelStore.connectedContactPoints?.[alertReceiveChannel.id];
|
||||
|
|
|
|||
|
|
@ -1,15 +1,65 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Icon, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
import cn from 'classnames';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { useCommonStyles, useIsLoading } from 'utils/hooks';
|
||||
|
||||
import ConnectedIntegrationsTable from './ConnectedIntegrationsTable';
|
||||
import { getStyles } from './OutgoingTab.styles';
|
||||
|
||||
export const ConnectIntegrationModal = ({ onDismiss }: { onDismiss: () => void }) => {
|
||||
const DEBOUNCE_MS = 500;
|
||||
|
||||
export const ConnectIntegrationModal = observer(({ onDismiss }: { onDismiss: () => void }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const isLoading = useIsLoading(ActionKey.FETCH_INTEGRATIONS);
|
||||
const commonStyles = useCommonStyles();
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useState<Array<ApiSchemas['AlertReceiveChannel']>>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { count, results, page_size } = AlertReceiveChannelHelper.getPaginatedSearchResult(alertReceiveChannelStore);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
return alertReceiveChannelStore.resetPaginatedResults;
|
||||
}, [page]);
|
||||
|
||||
const fetchItems = async (search?: string) => {
|
||||
await alertReceiveChannelStore.fetchPaginatedItems({
|
||||
filters: { search },
|
||||
perpage: 10,
|
||||
page,
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = (integration: ApiSchemas['AlertReceiveChannel'], checked) => {
|
||||
if (checked) {
|
||||
setSelectedIntegrations((integrations) => [...integrations, integration]);
|
||||
} else {
|
||||
setSelectedIntegrations((integrations) => integrations.filter(({ id }) => id !== integration.id));
|
||||
}
|
||||
};
|
||||
|
||||
const onConnect = () => {};
|
||||
|
||||
const debouncedSearch = debounce(fetchItems, DEBOUNCE_MS);
|
||||
|
||||
const onSearchInputChange = (searchTerm: string) => {
|
||||
debouncedSearch(searchTerm);
|
||||
};
|
||||
|
||||
const onChangePage = (page: number) => {
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
|
|
@ -17,13 +67,37 @@ export const ConnectIntegrationModal = ({ onDismiss }: { onDismiss: () => void }
|
|||
closeOnBackdropClick={false}
|
||||
closeOnEscape
|
||||
onDismiss={onDismiss}
|
||||
contentClassName={styles.connectIntegrationModalContent}
|
||||
>
|
||||
<Input
|
||||
className={styles.searchIntegrationsInput}
|
||||
suffix={<Icon name="search" />}
|
||||
placeholder="Search integrations..."
|
||||
onChange={(e) => onSearchInputChange(e.currentTarget.value)}
|
||||
/>
|
||||
<ConnectedIntegrationsTable />
|
||||
<ConnectedIntegrationsTable
|
||||
selectable
|
||||
onChange={onChange}
|
||||
tableProps={{
|
||||
data: results,
|
||||
pagination: {
|
||||
page,
|
||||
total: results ? Math.ceil((count || 0) / page_size) : 0,
|
||||
onChange: onChangePage,
|
||||
},
|
||||
emptyText: isLoading ? 'Loading...' : 'No integrations found',
|
||||
}}
|
||||
/>
|
||||
<div className={cn(commonStyles.bottomDrawerButtons, styles.connectIntegrationModalButtons)}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onDismiss}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onConnect} disabled={!selectedIntegrations?.length}>
|
||||
Connect
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,57 +1,78 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { HorizontalGroup, Tooltip, Icon, useStyles2, IconButton, Switch } from '@grafana/ui';
|
||||
import { HorizontalGroup, Tooltip, Icon, useStyles2, IconButton, Switch, Checkbox } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { GTable } from 'components/GTable/GTable';
|
||||
import { GTable, GTableProps } from 'components/GTable/GTable';
|
||||
import { IntegrationLogoWithTitle } from 'components/IntegrationLogo/IntegrationLogoWithTitle';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import { getStyles } from './OutgoingTab.styles';
|
||||
|
||||
interface ConnectedIntegrationsTableProps {
|
||||
allowDelete?: boolean;
|
||||
allowBacksync?: boolean;
|
||||
selectable?: boolean;
|
||||
onChange?: (integration: ApiSchemas['AlertReceiveChannel'], checked: boolean) => void;
|
||||
tableProps: GTableProps;
|
||||
}
|
||||
|
||||
const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = (props) => {
|
||||
const FAKE_INTEGRATIONS = [{ a: 'a' }];
|
||||
const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = observer(
|
||||
({ selectable, allowDelete, onChange, tableProps, allowBacksync }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
|
||||
return (
|
||||
<GTable
|
||||
emptyText={FAKE_INTEGRATIONS ? 'No integrations found' : 'Loading...'}
|
||||
rowKey="id"
|
||||
columns={getColumns(props)}
|
||||
data={FAKE_INTEGRATIONS}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const columns = [
|
||||
...(selectable
|
||||
? [
|
||||
{
|
||||
width: '5%',
|
||||
render: (integration: ApiSchemas['AlertReceiveChannel']) => (
|
||||
<Checkbox onChange={(event) => onChange(integration, event.currentTarget.checked)} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
width: '45%',
|
||||
title: <Text type="secondary">Integration name</Text>,
|
||||
dataIndex: 'verbal_name',
|
||||
render: (name: string) => name,
|
||||
},
|
||||
{
|
||||
width: '55%',
|
||||
title: <Text type="secondary">Type</Text>,
|
||||
render: (integration: ApiSchemas['AlertReceiveChannel']) => (
|
||||
<IntegrationLogoWithTitle
|
||||
integration={AlertReceiveChannelHelper.getIntegrationSelectOption(alertReceiveChannelStore, integration)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(allowBacksync
|
||||
? [
|
||||
{
|
||||
title: (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Backsync</Text>
|
||||
<Tooltip content={<>Switch on to start sending data from other integrations</>}>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
),
|
||||
render: BacksyncSwitcher,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
render: () => <ActionsColumn allowDelete={allowDelete} />,
|
||||
},
|
||||
];
|
||||
|
||||
const getColumns = ({ allowDelete }: ConnectedIntegrationsTableProps) => [
|
||||
{
|
||||
width: '45%',
|
||||
title: <Text type="secondary">Integration name</Text>,
|
||||
dataIndex: 'trigger_type_name',
|
||||
render: () => <>Some integration name</>,
|
||||
},
|
||||
{
|
||||
width: '55%',
|
||||
title: <Text type="secondary">Type</Text>,
|
||||
render: () => <IntegrationLogoWithTitle integration={{ value: 'elastalert', display_name: 'ElastAlerts' }} />,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Backsync</Text>
|
||||
<Tooltip content={<>Switch on to start sending data from other integrations</>}>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
),
|
||||
render: BacksyncSwitcher,
|
||||
},
|
||||
{
|
||||
render: () => <ActionsColumn allowDelete={allowDelete} />,
|
||||
},
|
||||
];
|
||||
return <GTable rowKey="id" columns={columns} {...tableProps} />;
|
||||
}
|
||||
);
|
||||
|
||||
const BacksyncSwitcher = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const OtherIntegrations = observer(() => {
|
|||
</Button>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={<ConnectedIntegrationsTable allowDelete />}
|
||||
content={<ConnectedIntegrationsTable allowDelete allowBacksync tableProps={{ data: [] }} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||
backsyncColumn: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
'& label': {
|
||||
position: 'relative',
|
||||
},
|
||||
}),
|
||||
triggerTemplateWrapper: css({
|
||||
position: 'relative',
|
||||
|
|
@ -77,4 +80,10 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||
tabsWrapper: css({
|
||||
padding: '16px 16px 0 8px',
|
||||
}),
|
||||
connectIntegrationModalContent: css({
|
||||
paddingBottom: 0,
|
||||
}),
|
||||
connectIntegrationModalButtons: css({
|
||||
marginTop: '50px',
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.store.alertReceiveChannelStore.resetPaginatedResults();
|
||||
}
|
||||
|
||||
parseQueryParams = async () => {
|
||||
const {
|
||||
store,
|
||||
|
|
@ -353,7 +357,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
|
||||
renderDatasource(item: ApiSchemas['AlertReceiveChannel'], alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
alertReceiveChannel
|
||||
);
|
||||
const isLegacyIntegration = (integration?.value as string)?.toLowerCase().startsWith('legacy_');
|
||||
|
||||
if (isLegacyIntegration) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue