Disable integration backsync conditionally (#4084)

# What this PR does

Disable integration backsync conditionally if token has not been
generated

## Which issue(s) this PR closes

Closes https://github.com/grafana/oncall-private/issues/2599

<!--
*Note*: if you have more than one GitHub issue that this PR closes, be
sure to preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
This commit is contained in:
Dominik Broj 2024-03-20 11:15:43 +01:00 committed by GitHub
parent 04604caa62
commit 5bcb438012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 223 additions and 8 deletions

View file

@ -183,4 +183,15 @@ export class AlertReceiveChannelHelper {
data,
});
}
static async checkIfTokenExists(integrationId: string) {
try {
await onCallApi({ skipErrorHandling: true }).GET('/alert_receive_channels/{id}/api_token/', {
params: { path: { id: integrationId } },
});
return true;
} catch (_e) {
return false;
}
}
}

View file

@ -34,11 +34,19 @@ export class AlertReceiveChannelConnectedChannelsStore {
}
@AutoLoadingState(ActionKey.FETCH_INTEGRATIONS_AVAILABLE_FOR_CONNECTION)
async fetchItemsAvailableForConnection({ search, page }: { search?: string; page: number }) {
async fetchItemsAvailableForConnection({
search,
page,
currentIntegrationId,
}: {
search?: string;
page: number;
currentIntegrationId: string;
}) {
await this.rootStore.alertReceiveChannelStore.fetchPaginatedItems({
filters: {
search,
id_ne: this.itemsAsList.map(({ alert_receive_channel: { id } }) => id),
id_ne: [...this.itemsAsList.map(({ alert_receive_channel: { id } }) => id), currentIntegrationId],
},
perpage: 10,
page,

View file

@ -39,6 +39,24 @@ export interface paths {
patch: operations['alert_receive_channels_partial_update'];
trace?: never;
};
'/alert_receive_channels/{id}/api_token/': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Internal API endpoints for alert receive channels (integrations). */
get: operations['alert_receive_channels_api_token_retrieve'];
put?: never;
/** @description Internal API endpoints for alert receive channels (integrations). */
post: operations['alert_receive_channels_api_token_create'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/alert_receive_channels/{id}/change_team/': {
parameters: {
query?: never;
@ -245,6 +263,23 @@ export interface paths {
patch?: never;
trace?: never;
};
'/alert_receive_channels/{id}/status_options/': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Internal API endpoints for alert receive channels (integrations). */
get: operations['alert_receive_channels_status_options_list'];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/alert_receive_channels/{id}/stop_maintenance/': {
parameters: {
query?: never;
@ -366,6 +401,23 @@ export interface paths {
patch?: never;
trace?: never;
};
'/alert_receive_channels/test_connection/': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** @description Internal API endpoints for alert receive channels (integrations). */
post: operations['alert_receive_channels_test_connection_create'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
'/alert_receive_channels/validate_name/': {
parameters: {
query?: never;
@ -1448,6 +1500,10 @@ export interface components {
readonly alertmanager_v2_migrated_at: string | null;
additional_settings?: components['schemas']['AdditionalSettingsField'] | null;
};
AlertReceiveChannelBacksyncStatusOptions: {
value: string;
display_name: string;
};
AlertReceiveChannelConnectContactPoint: {
datasource_uid: string;
contact_point_name: string;
@ -1746,6 +1802,9 @@ export interface components {
readonly status: boolean;
readonly instruction: string;
};
IntegrationTokenPostResponse: {
token: string;
};
Key: {
id: string;
name: string;
@ -2440,6 +2499,49 @@ export interface operations {
};
};
};
alert_receive_channels_api_token_retrieve: {
parameters: {
query?: never;
header?: never;
path: {
/** @description A string identifying this alert receive channel. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description No response body */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
alert_receive_channels_api_token_create: {
parameters: {
query?: never;
header?: never;
path: {
/** @description A string identifying this alert receive channel. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['IntegrationTokenPostResponse'];
};
};
};
};
alert_receive_channels_change_team_update: {
parameters: {
query: {
@ -2661,7 +2763,7 @@ export interface operations {
};
responses: {
/** @description No response body */
200: {
201: {
headers: {
[name: string]: unknown;
};
@ -2799,6 +2901,28 @@ export interface operations {
};
};
};
alert_receive_channels_status_options_list: {
parameters: {
query?: never;
header?: never;
path: {
/** @description A string identifying this alert receive channel. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['AlertReceiveChannelBacksyncStatusOptions'][];
};
};
};
};
alert_receive_channels_stop_maintenance_create: {
parameters: {
query?: never;
@ -3004,6 +3128,30 @@ export interface operations {
};
};
};
alert_receive_channels_test_connection_create: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
'application/json': components['schemas']['AlertReceiveChannel'];
'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannel'];
'multipart/form-data': components['schemas']['AlertReceiveChannel'];
};
};
responses: {
/** @description No response body */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
alert_receive_channels_validate_name_retrieve: {
parameters: {
query: {

View file

@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
import { useCurrentIntegration } from './OutgoingTab/OutgoingTab.hooks';
export const useIntegrationTokenCheck = () => {
const [tokenExists, setTokenExists] = useState(true);
const { id } = useCurrentIntegration();
useEffect(() => {
const checkToken = async () => {
const tokenExists = await AlertReceiveChannelHelper.checkIfTokenExists(id);
setTokenExists(tokenExists);
};
checkToken();
}, [id]);
return tokenExists;
};

View file

@ -38,6 +38,7 @@ export const ConnectIntegrationModal = observer(({ onDismiss }: { onDismiss: ()
await alertReceiveChannelConnectedChannelsStore.fetchItemsAvailableForConnection({
page,
search,
currentIntegrationId: currentIntegration.id,
});
};

View file

@ -1,6 +1,16 @@
import React, { FC } from 'react';
import { HorizontalGroup, Tooltip, Icon, useStyles2, IconButton, Switch, Checkbox, ConfirmModal } from '@grafana/ui';
import {
HorizontalGroup,
Tooltip,
Icon,
useStyles2,
IconButton,
Switch,
Checkbox,
ConfirmModal,
useTheme2,
} from '@grafana/ui';
import { observer } from 'mobx-react';
import Emoji from 'react-emoji-render';
@ -9,6 +19,7 @@ import { IntegrationLogoWithTitle } from 'components/IntegrationLogo/Integration
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 { useIntegrationTokenCheck } from 'pages/integration/Integration.hooks';
import { useStore } from 'state/useStore';
import { PLUGIN_ROOT } from 'utils/consts';
import { useConfirmModal } from 'utils/hooks';
@ -33,6 +44,8 @@ interface ConnectedIntegrationsTableProps {
const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = observer(
({ selectable, allowDelete, onChange, onBacksyncChange, tableProps, defaultBacksyncedIds = [], allowBacksync }) => {
const { alertReceiveChannelStore } = useStore();
const { colors } = useTheme2();
const tokenExists = useIntegrationTokenCheck();
const columns = [
...(selectable
@ -70,15 +83,22 @@ const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = observer
title: (
<HorizontalGroup>
<Text type="secondary">Backsync</Text>
<Tooltip content={<>Switch on to start sending data from other integrations</>}>
<Icon name={'info-circle'} />
</Tooltip>
{tokenExists ? (
<Tooltip content={<>Switch on to start sending data from other integrations</>}>
{<Icon name={'info-circle'} />}
</Tooltip>
) : (
<Tooltip content={<>Token must be generated to enable backsync</>}>
{<Icon name={'info-circle'} color={colors.error.shade} />}
</Tooltip>
)}
</HorizontalGroup>
),
render: (connectedIntegration: ConnectedIntegration) => (
<BacksyncSwitcher
defaultChecked={defaultBacksyncedIds.includes(connectedIntegration.id)}
onChange={(checked: boolean) => onBacksyncChange(connectedIntegration.id, checked)}
disabled={!tokenExists}
/>
),
},
@ -98,15 +118,21 @@ const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = observer
const BacksyncSwitcher = ({
onChange,
defaultChecked,
disabled,
}: {
onChange: (checked: boolean) => void;
defaultChecked?: boolean;
disabled?: boolean;
}) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.backsyncColumn}>
<Switch defaultChecked={defaultChecked} onChange={({ currentTarget }) => onChange(currentTarget.checked)} />
<Switch
disabled={disabled}
defaultChecked={defaultChecked}
onChange={({ currentTarget }) => onChange(currentTarget.checked)}
/>
</div>
);
};