SNOW final tweaks (#4254)
- Show `external_url` for an AG that has its integration connected to ServiceNow - Addressed some of @raphael-batte 's remarks - Prevent SNOW creation if connection to servicenow instance fails - Skip error handling that showed 500 or 401: Unauthorized on some calls to test_connection and instead rely purely on displaying `OK` or `Error`
This commit is contained in:
parent
baef4e2642
commit
55b154841f
8 changed files with 102 additions and 59 deletions
|
|
@ -38,7 +38,7 @@ import { IntegrationHelper, getIsBidirectionalIntegration } from 'pages/integrat
|
|||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { PLUGIN_ROOT, generateAssignToTeamInputDescription, DOCS_ROOT } from 'utils/consts';
|
||||
import { PLUGIN_ROOT, generateAssignToTeamInputDescription, DOCS_ROOT, INTEGRATION_SERVICENOW } from 'utils/consts';
|
||||
import { useIsLoading } from 'utils/hooks';
|
||||
import { OmitReadonlyMembers } from 'utils/types';
|
||||
|
||||
|
|
@ -58,6 +58,10 @@ export interface IntegrationFormFields {
|
|||
additional_settings: ApiSchemas['AlertReceiveChannel']['additional_settings'];
|
||||
}
|
||||
|
||||
interface AuthSection {
|
||||
testConnection(): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface IntegrationFormProps {
|
||||
id: ApiSchemas['AlertReceiveChannel']['id'] | 'new';
|
||||
isTableView?: boolean;
|
||||
|
|
@ -174,6 +178,7 @@ export const IntegrationForm = observer(
|
|||
}, []);
|
||||
|
||||
const labelsRef = useRef(null);
|
||||
const authSectionRef = useRef<AuthSection>(null);
|
||||
|
||||
const [labelsErrors, setLabelErrors] = useState([]);
|
||||
const isServiceNow = getIsBidirectionalIntegration(data as Partial<ApiSchemas['AlertReceiveChannel']>);
|
||||
|
|
@ -347,7 +352,7 @@ export const IntegrationForm = observer(
|
|||
)}
|
||||
/>
|
||||
|
||||
<ServiceNowAuthSection />
|
||||
<ServiceNowAuthSection ref={authSectionRef} />
|
||||
|
||||
<Controller
|
||||
name={'create_default_webhooks'}
|
||||
|
|
@ -414,14 +419,21 @@ export const IntegrationForm = observer(
|
|||
labels: labels ? [...labels] : undefined,
|
||||
};
|
||||
|
||||
if (formData.integration !== 'servicenow') {
|
||||
const isServiceNow = formData.integration === INTEGRATION_SERVICENOW;
|
||||
|
||||
if (!isServiceNow) {
|
||||
delete data.additional_settings;
|
||||
}
|
||||
|
||||
const isCreate = id === 'new';
|
||||
if (isServiceNow && isNew) {
|
||||
const testResult = await authSectionRef?.current?.testConnection();
|
||||
if (!testResult) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCreate) {
|
||||
if (isNew) {
|
||||
await createNewIntegration();
|
||||
} else {
|
||||
await alertReceiveChannelStore.update({ id, data, skipErrorHandling: true });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
|
@ -51,12 +52,14 @@ export const CompleteServiceNowModal: React.FC<CompleteServiceNowConfigModalProp
|
|||
>
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className={styles.border}>
|
||||
<ServiceNowStatusSection />
|
||||
</div>
|
||||
<div className={styles.scrollableContainer}>
|
||||
<div className={styles.border}>
|
||||
<ServiceNowStatusSection />
|
||||
</div>
|
||||
|
||||
<div className={styles.border}>
|
||||
<ServiceNowTokenSection />
|
||||
<div className={styles.border}>
|
||||
<ServiceNowTokenSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -126,5 +129,15 @@ export const CompleteServiceNowModal: React.FC<CompleteServiceNowConfigModalProp
|
|||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
...getCommonServiceNowConfigStyles(theme),
|
||||
|
||||
scrollableContainer: css`
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-height: 764px) {
|
||||
max-height: 40vh;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, HorizontalGroup, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
|
|
@ -11,55 +11,66 @@ import { IntegrationFormFields } from 'containers/IntegrationForm/IntegrationFor
|
|||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useCurrentIntegration } from 'pages/integration/OutgoingTab/OutgoingTab.hooks';
|
||||
import { INTEGRATION_SERVICENOW } from 'utils/consts';
|
||||
import { OmitReadonlyMembers } from 'utils/types';
|
||||
|
||||
import { getCommonServiceNowConfigStyles } from './ServiceNow.styles';
|
||||
import { ServiceNowFormFields } from './ServiceNowStatusSection';
|
||||
|
||||
export const ServiceNowAuthSection: React.FC = observer(() => {
|
||||
const { getValues } = useFormContext<ServiceNowFormFields | IntegrationFormFields>();
|
||||
export const ServiceNowAuthSection = observer(
|
||||
forwardRef(function ServiceNowAuthRef(_props, ref) {
|
||||
const { getValues } = useFormContext<ServiceNowFormFields | IntegrationFormFields>();
|
||||
|
||||
const currentIntegration = useCurrentIntegration();
|
||||
const [isAuthTestRunning, setIsAuthTestRunning] = useState(false);
|
||||
const [authTestResult, setAuthTestResult] = useState<boolean>(undefined);
|
||||
const styles = useStyles2(getStyles);
|
||||
const currentIntegration = useCurrentIntegration();
|
||||
const [isAuthTestRunning, setIsAuthTestRunning] = useState(false);
|
||||
const [authTestResult, setAuthTestResult] = useState<boolean>(undefined);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RenderConditionally shouldRender={!isAuthTestRunning && authTestResult !== undefined}>
|
||||
<Alert
|
||||
severity={authTestResult ? 'success' : 'error'}
|
||||
title={
|
||||
(<Text type="primary">{authTestResult ? 'Connection OK' : 'Connection failed'}</Text>) as unknown as string
|
||||
}
|
||||
/>
|
||||
</RenderConditionally>
|
||||
useImperativeHandle(ref, () => ({
|
||||
testConnection: onAuthTest,
|
||||
}));
|
||||
|
||||
<HorizontalGroup>
|
||||
<Button className={''} variant="secondary" onClick={onAuthTest} disabled={isAuthTestRunning}>
|
||||
Test Connection
|
||||
</Button>
|
||||
<div>
|
||||
<RenderConditionally shouldRender={isAuthTestRunning}>
|
||||
<LoadingPlaceholder text="Loading..." className={styles.loader} />
|
||||
</RenderConditionally>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<RenderConditionally shouldRender={!isAuthTestRunning && authTestResult !== undefined}>
|
||||
<Alert
|
||||
severity={authTestResult ? 'success' : 'error'}
|
||||
title={
|
||||
(
|
||||
<Text type="primary">{authTestResult ? 'Connection OK' : 'Connection failed'}</Text>
|
||||
) as unknown as string
|
||||
}
|
||||
/>
|
||||
</RenderConditionally>
|
||||
|
||||
async function onAuthTest() {
|
||||
const data: OmitReadonlyMembers<ApiSchemas['AlertReceiveChannel']> = {
|
||||
integration: currentIntegration ? currentIntegration.integration : 'servicenow',
|
||||
...getValues(),
|
||||
};
|
||||
<HorizontalGroup>
|
||||
<Button className={''} variant="secondary" onClick={onAuthTest} disabled={isAuthTestRunning}>
|
||||
Test Connection
|
||||
</Button>
|
||||
<div>
|
||||
<RenderConditionally shouldRender={isAuthTestRunning}>
|
||||
<LoadingPlaceholder text="Loading..." className={styles.loader} />
|
||||
</RenderConditionally>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
setIsAuthTestRunning(true);
|
||||
const result = await AlertReceiveChannelHelper.testServiceNowAuthentication({ id: currentIntegration?.id, data });
|
||||
setAuthTestResult(result);
|
||||
setIsAuthTestRunning(false);
|
||||
}
|
||||
});
|
||||
async function onAuthTest(): Promise<boolean> {
|
||||
const data: OmitReadonlyMembers<ApiSchemas['AlertReceiveChannel']> = {
|
||||
integration: currentIntegration ? currentIntegration.integration : INTEGRATION_SERVICENOW,
|
||||
...getValues(),
|
||||
};
|
||||
|
||||
setIsAuthTestRunning(true);
|
||||
const result = await AlertReceiveChannelHelper.testServiceNowAuthentication({ id: currentIntegration?.id, data });
|
||||
setAuthTestResult(result);
|
||||
setIsAuthTestRunning(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class AlertReceiveChannelHelper {
|
|||
? '/alert_receive_channels/{id}/test_connection/'
|
||||
: '/alert_receive_channels/test_connection/';
|
||||
|
||||
const result = await onCallApi({ skipErrorHandling: false }).POST(endpoint, {
|
||||
const result = await onCallApi({ skipErrorHandling: true }).POST(endpoint, {
|
||||
body: data as ApiSchemas['AlertReceiveChannelUpdate'],
|
||||
params: { path: { id } },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ import { PageProps, WithStoreProps } from 'state/types';
|
|||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
import { INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts';
|
||||
import { sanitize } from 'utils/sanitize';
|
||||
import { parseURL } from 'utils/url';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
|
@ -278,7 +278,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
const showLinkTo = !incident.dependent_alert_groups.length && !incident.root_alert_group && !incident.resolved;
|
||||
const integrationNameWithEmojies = <Emoji text={incident.alert_receive_channel.verbal_name} />;
|
||||
const sourceLink = incident?.render_for_web?.source_link;
|
||||
const isServiceNow = incident?.alert_receive_channel?.integration === 'servicenow';
|
||||
const isServiceNow = Boolean(incident?.external_urls?.find((el) => el.integration_type === INTEGRATION_SERVICENOW));
|
||||
|
||||
return (
|
||||
<Block className={cx('block')}>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
|||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
import { INTEGRATION_SERVICENOW } from 'utils/consts';
|
||||
|
||||
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './IntegrationCommon.config';
|
||||
|
||||
|
|
@ -122,4 +123,4 @@ export const IntegrationHelper = {
|
|||
};
|
||||
|
||||
export const getIsBidirectionalIntegration = ({ integration }: Partial<ApiSchemas['AlertReceiveChannel']>) =>
|
||||
integration === ('servicenow' as ApiSchemas['AlertReceiveChannel']['integration']); // TODO: add service now in backend schema as valid value and remove casting
|
||||
integration === INTEGRATION_SERVICENOW;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ import { useStore } from 'state/useStore';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { LocationHelper } from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { GENERIC_ERROR, PLUGIN_ROOT } from 'utils/consts';
|
||||
import { GENERIC_ERROR, INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts';
|
||||
import { withDrawer } from 'utils/hoc';
|
||||
import { useDrawer } from 'utils/hooks';
|
||||
import { getItem, setItem } from 'utils/localStorage';
|
||||
|
|
@ -275,7 +275,11 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
{ label: 'Incoming', content: incomingPart },
|
||||
{
|
||||
label: 'Outgoing',
|
||||
content: <OutgoingTab openSnowConfigurationDrawer={() => drawerConfig.openDrawer('servicenow')} />,
|
||||
content: (
|
||||
<OutgoingTab
|
||||
openSnowConfigurationDrawer={() => drawerConfig.openDrawer(INTEGRATION_SERVICENOW)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -819,7 +823,7 @@ interface IntegrationActionsProps {
|
|||
drawerConfig: ReturnType<typeof useDrawer<IntegrationDrawerKey>>;
|
||||
}
|
||||
|
||||
type IntegrationDrawerKey = 'servicenow' | 'completeConfig';
|
||||
type IntegrationDrawerKey = typeof INTEGRATION_SERVICENOW | 'completeConfig';
|
||||
|
||||
const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
||||
alertReceiveChannel,
|
||||
|
|
@ -885,7 +889,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{getIsDrawerOpened('servicenow') && <ServiceNowConfigDrawer onHide={closeDrawer} />}
|
||||
{getIsDrawerOpened(INTEGRATION_SERVICENOW) && <ServiceNowConfigDrawer onHide={closeDrawer} />}
|
||||
|
||||
{isCompleteServiceNowConfigOpen && (
|
||||
<CompleteServiceNowModal onHide={() => setIsCompleteServiceNowConfigOpen(false)} />
|
||||
|
|
@ -958,7 +962,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
{
|
||||
label: 'ServiceNow configuration',
|
||||
hidden: !getIsBidirectionalIntegration(alertReceiveChannel),
|
||||
onClick: () => openDrawer('servicenow'),
|
||||
onClick: () => openDrawer(INTEGRATION_SERVICENOW),
|
||||
},
|
||||
{
|
||||
onClick: openLabelsForm,
|
||||
|
|
|
|||
|
|
@ -85,3 +85,5 @@ export enum OnCallAGStatus {
|
|||
}
|
||||
|
||||
export const GENERIC_ERROR = 'An error has occurred. Please try again';
|
||||
|
||||
export const INTEGRATION_SERVICENOW = 'servicenow';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue