From 7794246efbfe2e28b3bddc4513e09736144ebaf8 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Wed, 28 Feb 2024 13:19:18 +0100 Subject: [PATCH] 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) --- .../e2e-tests/insights/insights.test.ts | 1 + .../integrations/maintenanceMode.test.ts | 2 +- .../src/components/GTable/GTable.tsx | 4 +- .../src/components/Webhooks/WebhookName.tsx | 1 + .../AlertReceiveChannelCard.tsx | 5 +- .../alert_receive_channel.helpers.ts | 2 +- .../alert_receive_channel.ts | 18 +++- .../src/models/loader/action-keys.ts | 1 + .../src/pages/incident/Incident.tsx | 2 +- .../src/pages/incidents/Incidents.tsx | 2 +- .../src/pages/integration/Integration.tsx | 5 +- .../OutgoingTab/ConnectIntegrationModal.tsx | 84 ++++++++++++++- .../ConnectedIntegrationsTable.tsx | 101 +++++++++++------- .../OutgoingTab/OtherIntegrations.tsx | 2 +- .../OutgoingTab/OutgoingTab.styles.ts | 9 ++ .../src/pages/integrations/Integrations.tsx | 9 +- 16 files changed, 188 insertions(+), 60 deletions(-) diff --git a/grafana-plugin/e2e-tests/insights/insights.test.ts b/grafana-plugin/e2e-tests/insights/insights.test.ts index e9655de4..db2d9dd8 100644 --- a/grafana-plugin/e2e-tests/insights/insights.test.ts +++ b/grafana-plugin/e2e-tests/insights/insights.test.ts @@ -1,4 +1,5 @@ import semver from 'semver'; + import { test, expect } from '../fixtures'; import { resolveFiringAlert } from '../utils/alertGroup'; import { createEscalationChain, EscalationStep } from '../utils/escalationChain'; diff --git a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts index b78e3d22..306fa4d9 100644 --- a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts +++ b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts @@ -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, diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index 212b26e8..02240fc3 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -10,7 +10,7 @@ import styles from './GTable.module.css'; const cx = cn.bind(styles); -export interface Props extends TableProps { +export interface GTableProps extends TableProps { pagination?: { page: number; total: number; @@ -31,7 +31,7 @@ export interface Props extends TableProps { showHeader?: boolean; } -export const GTable = (props: Props): ReactElement => { +export const GTable = (props: GTableProps): ReactElement => { const { columns: columnsProp, data, diff --git a/grafana-plugin/src/components/Webhooks/WebhookName.tsx b/grafana-plugin/src/components/Webhooks/WebhookName.tsx index 494e6a24..b503ee14 100644 --- a/grafana-plugin/src/components/Webhooks/WebhookName.tsx +++ b/grafana-plugin/src/components/Webhooks/WebhookName.tsx @@ -39,5 +39,6 @@ export const getStyles = () => ({ }), disabledBadge: css({ wordBreak: 'keep-all', + marginLeft: '8px', }), }); diff --git a/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx b/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx index 23f27641..e91b5ebb 100644 --- a/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx +++ b/grafana-plugin/src/containers/AlertReceiveChannelCard/AlertReceiveChannelCard.tsx @@ -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 (
diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts index 932e21ad..99b9dac0 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.helpers.ts @@ -43,7 +43,7 @@ export class AlertReceiveChannelHelper { : undefined; } - static getIntegration( + static getIntegrationSelectOption( store: AlertReceiveChannelStore, alertReceiveChannel: Partial ): SelectOption { diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 999ba481..9b75571c 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -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) { const heartbeats = alertReceiveChannels.reduce( (acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => { diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts index 10505e2a..9699dee8 100644 --- a/grafana-plugin/src/models/loader/action-keys.ts +++ b/grafana-plugin/src/models/loader/action-keys.ts @@ -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', } diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index e37c4937..ad97c8b4 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -273,7 +273,7 @@ class _IncidentPage extends React.Component 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>([]); + 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 ( void } closeOnBackdropClick={false} closeOnEscape onDismiss={onDismiss} + contentClassName={styles.connectIntegrationModalContent} > } placeholder="Search integrations..." + onChange={(e) => onSearchInputChange(e.currentTarget.value)} /> - + +
+ + + + +
); -}; +}); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx index 63efa48c..675e6170 100644 --- a/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx +++ b/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx @@ -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 = (props) => { - const FAKE_INTEGRATIONS = [{ a: 'a' }]; +const ConnectedIntegrationsTable: FC = observer( + ({ selectable, allowDelete, onChange, tableProps, allowBacksync }) => { + const { alertReceiveChannelStore } = useStore(); - return ( - - ); -}; + const columns = [ + ...(selectable + ? [ + { + width: '5%', + render: (integration: ApiSchemas['AlertReceiveChannel']) => ( + onChange(integration, event.currentTarget.checked)} /> + ), + }, + ] + : []), + { + width: '45%', + title: Integration name, + dataIndex: 'verbal_name', + render: (name: string) => name, + }, + { + width: '55%', + title: Type, + render: (integration: ApiSchemas['AlertReceiveChannel']) => ( + + ), + }, + ...(allowBacksync + ? [ + { + title: ( + + Backsync + Switch on to start sending data from other integrations}> + + + + ), + render: BacksyncSwitcher, + }, + ] + : []), + { + render: () => , + }, + ]; -const getColumns = ({ allowDelete }: ConnectedIntegrationsTableProps) => [ - { - width: '45%', - title: Integration name, - dataIndex: 'trigger_type_name', - render: () => <>Some integration name, - }, - { - width: '55%', - title: Type, - render: () => , - }, - { - title: ( - - Backsync - Switch on to start sending data from other integrations}> - - - - ), - render: BacksyncSwitcher, - }, - { - render: () => , - }, -]; + return ; + } +); const BacksyncSwitcher = () => { const styles = useStyles2(getStyles); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx index 6fcf1c87..02cef51e 100644 --- a/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx @@ -24,7 +24,7 @@ export const OtherIntegrations = observer(() => { } - content={} + content={} /> ); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts index d4739707..7d6d30e0 100644 --- a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts @@ -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', + }), }); diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 2e09ed0c..a26720b1 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -122,6 +122,10 @@ class _IntegrationsPage extends React.Component { const { store, @@ -353,7 +357,10 @@ class _IntegrationsPage extends React.Component