From fde006b434ca9523a06fb91d41753d555e97dda1 Mon Sep 17 00:00:00 2001 From: Remi Date: Thu, 25 Feb 2021 22:56:01 +0100 Subject: [PATCH 1/9] feat(splunk-on-call-plugin): add REST endpoint Signed-off-by: Remi --- plugins/splunk-on-call/README.md | 1 + plugins/splunk-on-call/src/api/client.ts | 77 ++---- plugins/splunk-on-call/src/api/types.ts | 46 ++-- .../components/Incident/IncidentListItem.tsx | 63 +++-- .../src/components/Incident/Incidents.tsx | 5 + .../src/components/SplunkOnCallCard.tsx | 23 +- .../TriggerDialog/TriggerDialog.tsx | 255 +++++------------- .../splunk-on-call/src/components/types.ts | 12 +- 8 files changed, 177 insertions(+), 305 deletions(-) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 73ee05c181..89fd45adc0 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -56,6 +56,7 @@ In `app-config.yaml`: ```yaml splunkOnCall: username: + eventsRestEndpoint: ``` The user supplied must be a valid Splunk On-Call user and a member of your organization. diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 501dad80a2..616ebb7023 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -31,7 +31,6 @@ import { RequestOptions, ListUserResponse, EscalationPolicyResponse, - PatchIncidentRequest, } from './types'; export class UnauthorizedError extends Error {} @@ -45,8 +44,11 @@ export class SplunkOnCallClient implements SplunkOnCallApi { static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { const usernameFromConfig: string | null = configApi.getOptionalString('splunkOnCall.username') || null; + const eventsRestEndpoint: string | null = + configApi.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; return new SplunkOnCallClient({ username: usernameFromConfig, + eventsRestEndpoint, discoveryApi, }); } @@ -80,50 +82,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return teams; } - async acknowledgeIncident({ - incidentNames, - }: PatchIncidentRequest): Promise { - const options = { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName: this.config.username, - incidentNames, - }), - }; - - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents/ack`; - - return this.request(url, options); - } - - async resolveIncident({ - incidentNames, - }: PatchIncidentRequest): Promise { - const options = { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName: this.config.username, - incidentNames, - }), - }; - - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents/resolve`; - - return this.request(url, options); - } - async getUsers(): Promise { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', @@ -142,19 +100,22 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return policies; } - async triggerAlarm({ - summary, - details, - userName, - targets, - isMultiResponder, + async incidentAction({ + routingKey, + incidentType, + incidentId, + incidentDisplayName, + incidentMessage, + incidentStartTime, }: TriggerAlarmRequest): Promise { const body = JSON.stringify({ - summary, - details, - userName: this.config.username || userName, - targets, - isMultiResponder, + message_type: incidentType, + ...(incidentId ? { entity_id: incidentId } : {}), + ...(incidentDisplayName + ? { entity_display_name: incidentDisplayName } + : {}), + ...(incidentMessage ? { state_message: incidentMessage } : {}), + ...(incidentStartTime ? { state_start_time: incidentStartTime } : {}), }); const options = { @@ -166,9 +127,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { body, }; - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents`; + const url = `${this.config.eventsRestEndpoint}/${routingKey}`; return this.request(url, options); } diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index 500f2a6291..b4007a9051 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -23,22 +23,20 @@ import { } from '../components/types'; import { DiscoveryApi } from '@backstage/core'; -export enum TargetType { - UserValue = 'User', - EscalationPolicyValue = 'EscalationPolicy', -} - -export type IncidentTarget = { - type: TargetType; - slug: string; -}; +export type MessageType = + | 'CRITICAL' + | 'WARNING' + | 'ACKNOWLEDGEMENT' + | 'INFO' + | 'RECOVERY'; export type TriggerAlarmRequest = { - targets: IncidentTarget[]; - details: string; - summary: string; - userName: string; - isMultiResponder?: boolean; + routingKey?: string; + incidentType: MessageType; + incidentId?: string; + incidentDisplayName?: string; + incidentMessage?: string; + incidentStartTime?: number; }; export interface SplunkOnCallApi { @@ -53,19 +51,9 @@ export interface SplunkOnCallApi { getOnCallUsers(): Promise; /** - * Triggers an incident to specific users and/or specific teams. + * Triggers-Resolves-Acknowledge an incident. */ - triggerAlarm(request: TriggerAlarmRequest): Promise; - - /** - * Resolves an incident. - */ - resolveIncident(request: PatchIncidentRequest): Promise; - - /** - * Acknowledge an incident. - */ - acknowledgeIncident(request: PatchIncidentRequest): Promise; + incidentAction(request: TriggerAlarmRequest): Promise; /** * Get a list of users for your organization. @@ -83,11 +71,6 @@ export interface SplunkOnCallApi { getEscalationPolicies(): Promise; } -export type PatchIncidentRequest = { - incidentNames: string[]; - message?: string; -}; - export type EscalationPolicyResponse = { policies: EscalationPolicyInfo[]; }; @@ -107,6 +90,7 @@ export type OnCallsResponse = { export type ClientApiConfig = { username: string | null; + eventsRestEndpoint: string | null; discoveryApi: DiscoveryApi; }; diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx index 1bf233de86..7bd8081226 100644 --- a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx +++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx @@ -39,7 +39,7 @@ import { Incident, IncidentPhase } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import { splunkOnCallApiRef } from '../../api/client'; import { useAsyncFn } from 'react-use'; -import { PatchIncidentRequest } from '../../api/types'; +import { TriggerAlarmRequest } from '../../api/types'; const useStyles = makeStyles({ denseListIcon: { @@ -61,6 +61,7 @@ const useStyles = makeStyles({ }); type Props = { + team: string; incident: Incident; onIncidentAction: () => void; }; @@ -93,20 +94,24 @@ const incidentPhaseTooltip = (currentPhase: IncidentPhase) => { const IncidentAction = ({ currentPhase, - incidentNames, + incidentId, resolveAction, acknowledgeAction, }: { currentPhase: string; - incidentNames: string[]; - resolveAction: (args: PatchIncidentRequest) => void; - acknowledgeAction: (args: PatchIncidentRequest) => void; + incidentId: string; + resolveAction: (args: TriggerAlarmRequest) => void; + acknowledgeAction: (args: TriggerAlarmRequest) => void; }) => { switch (currentPhase) { case 'UNACKED': return ( - acknowledgeAction({ incidentNames })}> + + acknowledgeAction({ incidentId, incidentType: 'ACKNOWLEDGEMENT' }) + } + > @@ -114,7 +119,11 @@ const IncidentAction = ({ case 'ACKED': return ( - resolveAction({ incidentNames })}> + + resolveAction({ incidentId, incidentType: 'RECOVERY' }) + } + > @@ -124,7 +133,11 @@ const IncidentAction = ({ } }; -export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { +export const IncidentListItem = ({ + incident, + onIncidentAction, + team, +}: Props) => { const classes = useStyles(); const duration = new Date().getTime() - new Date(incident.startTime!).getTime(); @@ -136,17 +149,26 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-'); - const user = hasBeenManuallyTriggered - ? incident.monitorName?.replace('vouser-', '') - : incident.monitorName; + const source = () => { + if (hasBeenManuallyTriggered) { + return incident.monitorName?.replace('vouser-', ''); + } + if (incident.monitorType === 'API') { + return '{ REST }'; + } + + return incident.monitorName; + }; const [ { value: resolveValue, error: resolveError }, handleResolveIncident, ] = useAsyncFn( - async ({ incidentNames }: PatchIncidentRequest) => - await api.resolveIncident({ - incidentNames, + async ({ incidentId, incidentType }: TriggerAlarmRequest) => + await api.incidentAction({ + routingKey: team, + incidentType, + incidentId, }), ); @@ -154,9 +176,11 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { { value: acknowledgeValue, error: acknowledgeError }, handleAcknowledgeIncident, ] = useAsyncFn( - async ({ incidentNames }: PatchIncidentRequest) => - await api.acknowledgeIncident({ - incidentNames, + async ({ incidentId, incidentType }: TriggerAlarmRequest) => + await api.incidentAction({ + routingKey: team, + incidentType, + incidentId, }), ); @@ -211,7 +235,8 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { }} secondary={ - Created {createdAt} {user && `by ${user}`} + #{incident.incidentNumber} - Created {createdAt}{' '} + {source() && `by ${source()}`} } /> @@ -220,7 +245,7 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx index e1a79314b9..507020f0a5 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -33,6 +33,10 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( async () => { + // For some reason the changes applied to incidents (trigger-resolve-acknowledge) + // may take some time to actually be applied after receiving the response from the server. + // The timeout compensates for this latency. + await new Promise(resolve => setTimeout(resolve, 2000)); const allIncidents = await api.getIncidents(); const teams = await api.getTeams(); const teamSlug = teams.find(teamValue => teamValue.name === team)?.slug; @@ -71,6 +75,7 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { getIncidents()} key={index} + team={team} incident={incident} /> ))} diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx index 84d94d60d3..e2cc642c6a 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx @@ -74,6 +74,16 @@ export const MissingUsername = () => ( ); +export const MissingEventsRestEndpoint = () => ( + + + +); + export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]); @@ -89,7 +99,10 @@ export const SplunkOnCallCard = ({ entity }: Props) => { const [refreshIncidents, setRefreshIncidents] = useState(false); const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; - const username = config.getOptionalString('splunkOnCall.username'); + const username = config.getOptionalString('splunkOnCall.username') || null; + + const eventsRestEndpoint = + config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; const handleRefresh = useCallback(() => { setRefreshIncidents(x => !x); @@ -140,15 +153,19 @@ export const SplunkOnCallCard = ({ entity }: Props) => { return ; } + if (!eventsRestEndpoint) { + return ; + } + return ( <> {users?.usersHashMap && team && ( )} - {users && incidentCreator && ( + {incidentCreator && ( void; @@ -79,7 +69,7 @@ const useStyles = makeStyles((theme: Theme) => ); export const TriggerDialog = ({ - users, + team, incidentCreator, showDialog, handleDialog, @@ -89,72 +79,46 @@ export const TriggerDialog = ({ const api = useApi(splunkOnCallApiRef); const classes = useStyles(); - const [userTargets, setUserTargets] = useState([]); - const [policyTargets, setPolicyTargets] = useState([]); - const [detailsValue, setDetails] = useState(''); - const [summaryValue, setSummary] = useState(''); - const [isMultiResponderValue, setIsMultiResponder] = useState('1'); + const [incidentType, setIncidentType] = useState(''); + const [incidentId, setIncidentId] = useState(); + const [incidentDisplayName, setIncidentDisplayName] = useState(''); + const [incidentMessage, setIncidentMessage] = useState(''); + const [incidentStartTime, setIncidentStartTime] = useState(); const [ { value, loading: triggerLoading, error: triggerError }, handleTriggerAlarm, ] = useAsyncFn( - async ( - summary: string, - details: string, - userName: string, - targets: IncidentTarget[], - isMultiResponder: boolean, - ) => - await api.triggerAlarm({ - summary, - details, - userName, - targets, - isMultiResponder, - }), + async (params: TriggerAlarmRequest) => await api.incidentAction(params), ); - const { - value: policies, - loading: policiesLoaading, - error: policiesError, - } = useAsync(async () => { - const allPolicies = await api.getEscalationPolicies(); - return allPolicies; - }); - - const handleUserTargets = (event: React.ChangeEvent<{ value: unknown }>) => { - setUserTargets(event.target.value as string[]); + const handleIncidentType = (event: React.ChangeEvent<{ value: unknown }>) => { + setIncidentType(event.target.value as string); }; - const handlePolicyTargets = ( - event: React.ChangeEvent<{ value: unknown }>, + const handleIncidentId = (event: React.ChangeEvent) => { + setIncidentId(event.target.value as string); + }; + + const handleIncidentDisplayName = ( + event: React.ChangeEvent, ) => { - setPolicyTargets(event.target.value as string[]); + setIncidentDisplayName(event.target.value); }; - const detailsChanged = (event: React.ChangeEvent) => { - setDetails(event.target.value); - }; - - const summaryChanged = (event: React.ChangeEvent) => { - setSummary(event.target.value); - }; - - const isMultiResponderChanged = ( - event: React.ChangeEvent<{ value: unknown }>, + const handleIncidentMessage = ( + event: React.ChangeEvent, ) => { - setIsMultiResponder(event.target.value as string); + setIncidentMessage(event.target.value); }; - const targets = (): IncidentTarget[] => [ - ...userTargets.map(user => ({ slug: user, type: TargetType.UserValue })), - ...policyTargets.map(user => ({ - slug: user, - type: TargetType.EscalationPolicyValue, - })), - ]; + const handleIncidentStartTime = ( + event: React.ChangeEvent, + ) => { + const dateTime = new Date(event.target.value).getTime(); + const dateTimeInSeconds = Math.floor(dateTime / 1000); + setIncidentStartTime(dateTimeInSeconds); + }; useEffect(() => { if (value) { @@ -203,132 +167,58 @@ export const TriggerDialog = ({ automatically be amended to the alarm so that the receiver can reach out to you if necessary. -
- - Select the targets - -
- - Select Users - - - - Select Teams / Policies - - -
-
- - Acknowledge Behavior - + Incident type + + @@ -337,20 +227,21 @@ export const TriggerDialog = ({ id="trigger" color="secondary" disabled={ - !detailsValue || - !summaryValue || - (!userTargets.length && !policyTargets.length) || + !incidentType.length || + !incidentDisplayName || + !incidentMessage || triggerLoading } variant="contained" onClick={() => - handleTriggerAlarm( - summaryValue, - detailsValue, - incidentCreator.username!, - targets(), - !!Number(isMultiResponderValue), - ) + handleTriggerAlarm({ + routingKey: team, + incidentType, + incidentDisplayName, + incidentMessage, + ...(incidentId ? { incidentId } : {}), + ...(incidentStartTime ? { incidentStartTime } : {}), + } as TriggerAlarmRequest) } endIcon={triggerLoading && } > diff --git a/plugins/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts index de5718cb28..9b2af1cddd 100644 --- a/plugins/splunk-on-call/src/components/types.ts +++ b/plugins/splunk-on-call/src/components/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { IncidentTarget } from '../api/types'; - export type Team = { name?: string; slug?: string; @@ -68,14 +66,6 @@ export type User = { _selfUrl?: string; }; -export type CreateIncidentRequest = { - summary: string; - details: string; - userName: string; - targets: IncidentTarget; - isMultiResponder: boolean; -}; - export type IncidentPhase = 'UNACKED' | 'ACKED' | 'RESOLVED'; export type Incident = { @@ -88,7 +78,7 @@ export type Incident = { alertCount?: number; lastAlertTime?: string; lastAlertId?: string; - entityId?: string; + entityId: string; host?: string; service?: string; pagedUsers?: string[]; From 98fb840b4daabe57c5d9c43a637c73c1e56fd678 Mon Sep 17 00:00:00 2001 From: Remi Date: Thu, 25 Feb 2021 22:56:19 +0100 Subject: [PATCH 2/9] feat(splunk-on-call-plugin): REST endpoint tests Signed-off-by: Remi --- .../components/Incident/Incidents.test.tsx | 31 +++++++--- .../src/components/SplunkOnCallCard.test.tsx | 5 +- .../TriggerDialog/TriggerDialog.test.tsx | 58 ++++++------------- 3 files changed, 44 insertions(+), 50 deletions(-) diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx index d1f973735c..d0171df985 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx @@ -62,7 +62,10 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + await waitFor( + () => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(), + { timeout: 2000 }, + ); }); it('Renders all incidents', async () => { @@ -87,11 +90,15 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect( - getByText('user', { - exact: false, - }), - ).toBeInTheDocument(); + await waitFor( + () => + expect( + getByText('user', { + exact: false, + }), + ).toBeInTheDocument(), + { timeout: 2000 }, + ); expect(getByText('test-incident')).toBeInTheDocument(); expect(getByTitle('Acknowledged')).toBeInTheDocument(); expect(getByLabelText('Status warning')).toBeInTheDocument(); @@ -113,8 +120,14 @@ describe('Incidents', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect( - getByText('Error encountered while fetching information. Error occurred'), - ).toBeInTheDocument(); + await waitFor( + () => + expect( + getByText( + 'Error encountered while fetching information. Error occurred', + ), + ).toBeInTheDocument(), + { timeout: 2000 }, + ); }); }); diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx index b1280ab21c..3418ed3398 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx @@ -91,7 +91,10 @@ describe('SplunkOnCallCard', () => { ); await waitFor(() => !queryByTestId('progress')); expect(getByText('Create Incident')).toBeInTheDocument(); - expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + await waitFor( + () => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(), + { timeout: 2000 }, + ); expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx index 741a15fb41..5308148afa 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -24,13 +24,12 @@ import { } from '@backstage/core'; import { splunkOnCallApiRef } from '../../api'; import { TriggerDialog } from './TriggerDialog'; -import { ESCALATION_POLICIES, MOCKED_USER } from '../../api/mocks'; +import { MOCKED_USER } from '../../api/mocks'; describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); const mockSplunkOnCallApi = { - triggerAlarm: mockTriggerAlarmFn, - getEscalationPolicies: async () => ESCALATION_POLICIES, + incidentAction: mockTriggerAlarmFn, }; const apis = ApiRegistry.from([ @@ -45,14 +44,13 @@ describe('TriggerDialog', () => { ]); it('open the dialog and trigger an alarm', async () => { - const { getByText, getByRole, getAllByRole, getByTestId } = render( + const { getByText, getByRole, getByTestId } = render( wrapInTestApp( {}} - users={[MOCKED_USER]} onIncidentCreated={() => {}} /> , @@ -65,36 +63,20 @@ describe('TriggerDialog', () => { exact: false, }), ).toBeInTheDocument(); - const summary = getByTestId('trigger-summary-input'); - const body = getByTestId('trigger-body-input'); - const behavior = getByTestId('trigger-select-behavior'); - const description = 'Test Trigger Alarm'; - await act(async () => { - fireEvent.change(summary, { target: { value: description } }); - fireEvent.change(body, { target: { value: description } }); - fireEvent.change(behavior, { target: { value: '0' } }); - fireEvent.mouseDown(getAllByRole('button')[0]); - }); + const incidentType = getByTestId('trigger-incident-type'); + const incidentId = getByTestId('trigger-incident-id'); + const incidentDisplayName = getByTestId('trigger-incident-displayName'); + const incidentMessage = getByTestId('trigger-incident-message'); - // Trigger user targets select - const options = getAllByRole('option'); await act(async () => { - fireEvent.click(options[0]); - fireEvent.keyDown(options[0], { - key: 'Escape', - code: 'Escape', - keyCode: 27, - charCode: 27, + fireEvent.change(incidentType, { target: { value: 'CRITICAL' } }); + fireEvent.change(incidentId, { target: { value: 'incident-id' } }); + fireEvent.change(incidentDisplayName, { + target: { value: 'incident-display-name' }, + }); + fireEvent.change(incidentMessage, { + target: { value: 'incident-message' }, }); - }); - - // Trigger policy targets select - await act(async () => { - fireEvent.mouseDown(getAllByRole('button')[1]); - }); - const policiesOptions = getAllByRole('option'); - await act(async () => { - fireEvent.click(policiesOptions[0]); }); // Trigger incident creation button @@ -104,14 +86,10 @@ describe('TriggerDialog', () => { }); expect(mockTriggerAlarmFn).toHaveBeenCalled(); expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ - summary: description, - details: description, - userName: 'test_user', - targets: [ - { slug: 'test_user', type: 'User' }, - { slug: 'team-zEalMCgwYSA0Lt40', type: 'EscalationPolicy' }, - ], - isMultiResponder: false, + incidentType: 'CRITICAL', + incidentId: 'incident-id', + incidentDisplayName: 'incident-display-name', + incidentMessage: 'incident-message', }); }); }); From 14af7ac400bef36e7088b8dba949c28c8acd028f Mon Sep 17 00:00:00 2001 From: Remi Date: Thu, 25 Feb 2021 23:22:53 +0100 Subject: [PATCH 3/9] feat(splunk-on-call-plugin): update documentation Signed-off-by: Remi --- plugins/splunk-on-call/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 89fd45adc0..5276d6d2df 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -51,6 +51,9 @@ import { In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action. The user supplied must be a valid Splunk On-Call user and a member of your organization. +In addition, you also need to enable the REST Endpoint integration on https://portal.victorops.com/ by going to Integrations > 3rd Party Integrations > REST – Generic. +You can now copy the URL to notify: `/$routing_key` + In `app-config.yaml`: ```yaml @@ -88,6 +91,14 @@ annotations: splunk.com/on-call-team': ``` +### Create the Routing Key + +To be able to use the REST Endpoint seen above, you must have created a routing key with the **same name** as the provided team. + +You can create a new routing key on https://portal.victorops.com/ by going to Settings > Routing Keys. + +You can read [Create & Manage Alert Routing Keys](https://help.victorops.com/knowledge-base/routing-keys/#routing-key-tips-tricks) for further information. + ## Providing the API key and API id In order for the client to make requests to the [Splunk On-Call API](https://portal.victorops.com/public/api-docs.html#/) it needs an [API ID and an API Key](https://help.victorops.com/knowledge-base/api/). From 274b00ed52c4d0d8f037a601141adf0b14fa60b6 Mon Sep 17 00:00:00 2001 From: Remi Date: Fri, 26 Feb 2021 12:32:00 +0100 Subject: [PATCH 4/9] feat(splunk-on-call-plugin): update style + fetch Signed-off-by: Remi --- plugins/splunk-on-call/README.md | 8 +-- plugins/splunk-on-call/src/api/client.ts | 3 - plugins/splunk-on-call/src/api/types.ts | 1 - .../Escalation/EscalationPolicy.tsx | 46 ++++++++++--- .../src/components/Incident/Incidents.tsx | 60 ++++++++++++----- .../src/components/SplunkOnCallCard.tsx | 47 +++++-------- .../TriggerDialog/TriggerDialog.tsx | 67 ++++++++++--------- 7 files changed, 138 insertions(+), 94 deletions(-) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 5276d6d2df..78313d3329 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -48,22 +48,18 @@ import { ## Client configuration -In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action. -The user supplied must be a valid Splunk On-Call user and a member of your organization. +In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint. -In addition, you also need to enable the REST Endpoint integration on https://portal.victorops.com/ by going to Integrations > 3rd Party Integrations > REST – Generic. +To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST – Generic. You can now copy the URL to notify: `/$routing_key` In `app-config.yaml`: ```yaml splunkOnCall: - username: eventsRestEndpoint: ``` -The user supplied must be a valid Splunk On-Call user and a member of your organization. - In order to make the API calls, you need to provide a new proxy config which will redirect to the Splunk On-Call API endpoint and add authentication information in the headers: ```yaml diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 616ebb7023..045ea04d63 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -42,12 +42,9 @@ export const splunkOnCallApiRef = createApiRef({ export class SplunkOnCallClient implements SplunkOnCallApi { static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { - const usernameFromConfig: string | null = - configApi.getOptionalString('splunkOnCall.username') || null; const eventsRestEndpoint: string | null = configApi.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; return new SplunkOnCallClient({ - username: usernameFromConfig, eventsRestEndpoint, discoveryApi, }); diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index b4007a9051..b42ab6a287 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -89,7 +89,6 @@ export type OnCallsResponse = { }; export type ClientApiConfig = { - username: string | null; eventsRestEndpoint: string | null; discoveryApi: DiscoveryApi; }; diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx index 9538a0e73b..4234ba1621 100644 --- a/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx @@ -15,7 +15,13 @@ */ import React from 'react'; -import { List, ListSubheader } from '@material-ui/core'; +import { + createStyles, + List, + ListSubheader, + makeStyles, + Theme, +} from '@material-ui/core'; import { EscalationUsersEmptyState } from './EscalationUsersEmptyState'; import { EscalationUser } from './EscalationUser'; import { useAsync } from 'react-use'; @@ -24,12 +30,28 @@ import { useApi, Progress } from '@backstage/core'; import { Alert } from '@material-ui/lab'; import { User } from '../types'; +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + maxHeight: '400px', + overflow: 'auto', + }, + subheader: { + backgroundColor: 'white', + }, + progress: { + margin: `0 ${theme.spacing(2)}px`, + }, + }), +); + type Props = { users: { [key: string]: User }; team: string; }; export const EscalationPolicy = ({ users, team }: Props) => { + const classes = useStyles(); const api = useApi(splunkOnCallApiRef); const { value: userNames, loading, error } = useAsync(async () => { @@ -54,24 +76,30 @@ export const EscalationPolicy = ({ users, team }: Props) => { ); } - if (loading) { - return ; - } - - if (!userNames?.length) { + if (!loading && !userNames?.length) { return ; } return ( - ON CALL}> - {userNames && + ON CALL + } + > + {loading ? ( + + ) : ( + userNames && userNames.map( (userName, index) => userName && userName in users && ( ), - )} + ) + )} ); }; diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx index 507020f0a5..189367542d 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -15,7 +15,13 @@ */ import React, { useEffect } from 'react'; -import { List, ListSubheader } from '@material-ui/core'; +import { + createStyles, + List, + ListSubheader, + makeStyles, + Theme, +} from '@material-ui/core'; import { IncidentListItem } from './IncidentListItem'; import { IncidentsEmptyState } from './IncidentEmptyState'; import { useAsyncFn } from 'react-use'; @@ -23,12 +29,28 @@ import { splunkOnCallApiRef } from '../../api'; import { useApi, Progress } from '@backstage/core'; import { Alert } from '@material-ui/lab'; +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + maxHeight: '400px', + overflow: 'auto', + }, + subheader: { + backgroundColor: 'white', + }, + progress: { + margin: `0 ${theme.spacing(2)}px`, + }, + }), +); + type Props = { refreshIncidents: boolean; team: string; }; export const Incidents = ({ refreshIncidents, team }: Props) => { + const classes = useStyles(); const api = useApi(splunkOnCallApiRef); const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( @@ -61,24 +83,32 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { ); } - if (loading) { - return ; - } - - if (!incidents?.length) { + if (!loading && !incidents?.length) { return ; } return ( - INCIDENTS}> - {incidents!.map((incident, index) => ( - getIncidents()} - key={index} - team={team} - incident={incident} - /> - ))} + + CRITICAL INCIDENTS + + } + > + {loading ? ( + + ) : ( + incidents!.map((incident, index) => ( + getIncidents()} + key={index} + team={team} + incident={incident} + /> + )) + )} ); }; diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx index e2cc642c6a..8a7364fea9 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx @@ -34,6 +34,7 @@ import { } from '@material-ui/core'; import { Incidents } from './Incident'; import { EscalationPolicy } from './Escalation'; +import WebIcon from '@material-ui/icons/Web'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { splunkOnCallApiRef, UnauthorizedError } from '../api'; @@ -64,16 +65,6 @@ export const MissingTeamAnnotation = () => ( ); -export const MissingUsername = () => ( - - - -); - export const MissingEventsRestEndpoint = () => ( { const [refreshIncidents, setRefreshIncidents] = useState(false); const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; - const username = config.getOptionalString('splunkOnCall.username') || null; - const eventsRestEndpoint = config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; @@ -126,9 +115,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => { return { usersHashMap, userList: allUsers }; }); - const incidentCreator = - username && users?.userList.find(user => user.username === username); - if (error instanceof UnauthorizedError) { return ; } @@ -149,9 +135,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => { if (!team) { return ; } - if (!username || !incidentCreator) { - return ; - } if (!eventsRestEndpoint) { return ; @@ -163,15 +146,12 @@ export const SplunkOnCallCard = ({ entity }: Props) => { {users?.usersHashMap && team && ( )} - {incidentCreator && ( - - )} + ); }; @@ -191,15 +171,22 @@ export const SplunkOnCallCard = ({ entity }: Props) => { icon: , }; + const serviceLink = { + label: 'Portal', + href: 'https://portal.victorops.com/', + icon: , + }; + return ( Team: {team}, - username && ( - - ), + , ]} /> diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx index a43dc548dd..2872c80ba0 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx @@ -36,12 +36,10 @@ import { useApi, alertApiRef } from '@backstage/core'; import { useAsyncFn } from 'react-use'; import { splunkOnCallApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; -import { User } from '../types'; import { TriggerAlarmRequest } from '../../api/types'; type Props = { team: string; - incidentCreator: User; showDialog: boolean; handleDialog: () => void; onIncidentCreated: () => void; @@ -58,8 +56,17 @@ const useStyles = makeStyles((theme: Theme) => }, formControl: { margin: theme.spacing(1), + display: 'flex', + flexDirection: 'row', + alignItems: 'center', minWidth: `calc(100% - ${theme.spacing(2)}px)`, }, + formHeader: { + width: '50%', + }, + incidentType: { + width: '90%', + }, targets: { display: 'flex', flexDirection: 'column', @@ -70,7 +77,6 @@ const useStyles = makeStyles((theme: Theme) => export const TriggerDialog = ({ team, - incidentCreator, showDialog, handleDialog, onIncidentCreated: onIncidentCreated, @@ -142,10 +148,7 @@ export const TriggerDialog = ({ This action will trigger an incident - Created by:{' '} - - {incidentCreator?.firstName} {incidentCreator?.lastName} - + Created by: {`{ REST } Endpoint`} @@ -163,22 +166,35 @@ export const TriggerDialog = ({ align="justify" > Please describe the problem you want to report. Be as descriptive as - possible. Your signed in user and a reference to the current page will - automatically be amended to the alarm so that the receiver can reach - out to you if necessary. + possible.
+ Note that only the Incident type, Incident display name{' '} + and the Incident message fields are required.
- Incident type - +
+ Incident type + +
+
-