diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx index c417a2f4..4ee3729f 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx @@ -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; +} + interface IntegrationFormProps { id: ApiSchemas['AlertReceiveChannel']['id'] | 'new'; isTableView?: boolean; @@ -174,6 +178,7 @@ export const IntegrationForm = observer( }, []); const labelsRef = useRef(null); + const authSectionRef = useRef(null); const [labelsErrors, setLabelErrors] = useState([]); const isServiceNow = getIsBidirectionalIntegration(data as Partial); @@ -347,7 +352,7 @@ export const IntegrationForm = observer( )} /> - +
-
- -
+
+
+ +
-
- +
+ +
@@ -126,5 +129,15 @@ export const CompleteServiceNowModal: React.FC { return { ...getCommonServiceNowConfigStyles(theme), + + scrollableContainer: css` + max-height: 60vh; + overflow-y: auto; + margin-bottom: 16px; + + @media (max-height: 764px) { + max-height: 40vh; + } + `, }; }; diff --git a/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowAuthSection.tsx b/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowAuthSection.tsx index acd5c0c7..71d7edad 100644 --- a/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowAuthSection.tsx +++ b/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowAuthSection.tsx @@ -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(); +export const ServiceNowAuthSection = observer( + forwardRef(function ServiceNowAuthRef(_props, ref) { + const { getValues } = useFormContext(); - const currentIntegration = useCurrentIntegration(); - const [isAuthTestRunning, setIsAuthTestRunning] = useState(false); - const [authTestResult, setAuthTestResult] = useState(undefined); - const styles = useStyles2(getStyles); + const currentIntegration = useCurrentIntegration(); + const [isAuthTestRunning, setIsAuthTestRunning] = useState(false); + const [authTestResult, setAuthTestResult] = useState(undefined); + const styles = useStyles2(getStyles); - return ( -
- - {authTestResult ? 'Connection OK' : 'Connection failed'}) as unknown as string - } - /> - + useImperativeHandle(ref, () => ({ + testConnection: onAuthTest, + })); - - -
- - - -
-
-
- ); + return ( +
+ + {authTestResult ? 'Connection OK' : 'Connection failed'} + ) as unknown as string + } + /> + - async function onAuthTest() { - const data: OmitReadonlyMembers = { - integration: currentIntegration ? currentIntegration.integration : 'servicenow', - ...getValues(), - }; + + +
+ + + +
+
+
+ ); - setIsAuthTestRunning(true); - const result = await AlertReceiveChannelHelper.testServiceNowAuthentication({ id: currentIntegration?.id, data }); - setAuthTestResult(result); - setIsAuthTestRunning(false); - } -}); + async function onAuthTest(): Promise { + const data: OmitReadonlyMembers = { + 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 { 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 3e128160..c920b009 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 @@ -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 } }, }); diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 25f154cd..89b9f547 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -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; 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 ( diff --git a/grafana-plugin/src/pages/integration/Integration.helper.ts b/grafana-plugin/src/pages/integration/Integration.helper.ts index f44e92e9..042b2606 100644 --- a/grafana-plugin/src/pages/integration/Integration.helper.ts +++ b/grafana-plugin/src/pages/integration/Integration.helper.ts @@ -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) => - integration === ('servicenow' as ApiSchemas['AlertReceiveChannel']['integration']); // TODO: add service now in backend schema as valid value and remove casting + integration === INTEGRATION_SERVICENOW; diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index d109acbb..64c9c587 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -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 drawerConfig.openDrawer('servicenow')} />, + content: ( + drawerConfig.openDrawer(INTEGRATION_SERVICENOW)} + /> + ), }, ]} /> @@ -819,7 +823,7 @@ interface IntegrationActionsProps { drawerConfig: ReturnType>; } -type IntegrationDrawerKey = 'servicenow' | 'completeConfig'; +type IntegrationDrawerKey = typeof INTEGRATION_SERVICENOW | 'completeConfig'; const IntegrationActions: React.FC = ({ alertReceiveChannel, @@ -885,7 +889,7 @@ const IntegrationActions: React.FC = ({ /> )} - {getIsDrawerOpened('servicenow') && } + {getIsDrawerOpened(INTEGRATION_SERVICENOW) && } {isCompleteServiceNowConfigOpen && ( setIsCompleteServiceNowConfigOpen(false)} /> @@ -958,7 +962,7 @@ const IntegrationActions: React.FC = ({ { label: 'ServiceNow configuration', hidden: !getIsBidirectionalIntegration(alertReceiveChannel), - onClick: () => openDrawer('servicenow'), + onClick: () => openDrawer(INTEGRATION_SERVICENOW), }, { onClick: openLabelsForm, diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 6d4fae31..ee691a70 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -85,3 +85,5 @@ export enum OnCallAGStatus { } export const GENERIC_ERROR = 'An error has occurred. Please try again'; + +export const INTEGRATION_SERVICENOW = 'servicenow';