diff --git a/.changeset/funny-singers-serve.md b/.changeset/funny-singers-serve.md new file mode 100644 index 0000000000..5812bdf5e3 --- /dev/null +++ b/.changeset/funny-singers-serve.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-splunk-on-call': minor +--- + +Use the routing key if it's available instead of team name when triggering incidents. + +BREAKING CHANGE: +Before, the team name was used even if the routing key (with or without team) was used. +Now, the routing key defined for the component will be used instead of the team name. diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 0688fb8237..051d9de030 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -13,11 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { + alertApiRef, + ConfigApi, + configApiRef, +} from '@backstage/core-plugin-api'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; import { splunkOnCallApiRef, SplunkOnCallClient, @@ -33,13 +39,7 @@ import { MOCK_TEAM_NO_INCIDENTS, } from '../api/mocks'; import { EntitySplunkOnCallCard } from './EntitySplunkOnCallCard'; - -import { - alertApiRef, - ConfigApi, - configApiRef, -} from '@backstage/core-plugin-api'; -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { expectTriggeredIncident } from './TriggerDialog/testUtils'; const mockSplunkOnCallApi: Partial = { getUsers: async () => [], @@ -167,8 +167,10 @@ describe('SplunkOnCallCard', () => { mockSplunkOnCallApi.getTeams = jest .fn() .mockImplementation(async () => [MOCK_TEAM]); + const mockTriggerAlarmFn = jest.fn(); + mockSplunkOnCallApi.incidentAction = mockTriggerAlarmFn; - const { getByText, queryByTestId } = render( + const { getByRole, getByTestId, getByText, queryByTestId } = render( wrapInTestApp( @@ -179,10 +181,23 @@ describe('SplunkOnCallCard', () => { ); await waitFor(() => !queryByTestId('progress')); expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); await waitFor( () => expect(getByText('test-incident')).toBeInTheDocument(), { timeout: 2000 }, ); + + const createIncidentButton = await getByText('Create Incident'); + await act(async () => { + fireEvent.click(createIncidentButton); + }); + expect(getByRole('dialog')).toBeInTheDocument(); + + await expectTriggeredIncident( + 'test-routing-key', + getByTestId, + mockTriggerAlarmFn, + ); }); it('Handles custom error for missing token', async () => { diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index d635f7328f..26521ea9ee 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -29,11 +29,11 @@ import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import WebIcon from '@material-ui/icons/Web'; import { Alert } from '@material-ui/lab'; import { splunkOnCallApiRef, UnauthorizedError } from '../api'; -import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError'; +import { MissingApiKeyOrApiIdError } from './Errors'; import { EscalationPolicy } from './Escalation'; import { Incidents } from './Incident'; import { TriggerDialog } from './TriggerDialog'; -import { Team, User } from './types'; +import { RoutingKey, Team, User } from './types'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { @@ -143,7 +143,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { }, []); const { - value: usersAndTeams, + value: entityData, loading, error, } = useAsync(async () => { @@ -162,11 +162,15 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { teams.find(teamValue => teamValue.name === teamAnnotation), ].filter(team => team !== undefined); - if (!foundTeams.length && routingKeyAnnotation) { + let foundRoutingKey: RoutingKey | undefined; + if (routingKeyAnnotation) { const routingKeys = await api.getRoutingKeys(); - const foundRoutingKey = routingKeys.find( + foundRoutingKey = routingKeys.find( key => key.routingKey === routingKeyAnnotation, ); + } + + if (!foundTeams.length) { foundTeams = foundRoutingKey ? foundRoutingKey.targets .map(target => { @@ -179,7 +183,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { : []; } - return { usersHashMap, foundTeams }; + return { usersHashMap, foundTeams, foundRoutingKey }; }); if (!teamAnnotation && !routingKeyAnnotation) { @@ -206,7 +210,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { return ; } - if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) { + if (!entityData?.foundTeams || !entityData?.foundTeams.length) { return ( { const Content = ({ team, + routingKey, usersHashMap, }: { team: Team | undefined; + routingKey: RoutingKey | undefined; usersHashMap: any; }) => { const teamName = team?.name ?? ''; @@ -235,7 +241,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { )} { icon: , }; - const teams = usersAndTeams?.foundTeams || []; + const teams = entityData?.foundTeams || []; return ( <> @@ -277,7 +283,11 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { /> - + ))} 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 0b64a137af..3bfbddb0e4 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { render, fireEvent, act } from '@testing-library/react'; -import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; -import { splunkOnCallApiRef } from '../../api'; -import { TriggerDialog } from './TriggerDialog'; - import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef } from '@backstage/core-plugin-api'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { splunkOnCallApiRef } from '../../api'; +import { TriggerDialog } from './TriggerDialog'; +import { expectTriggeredIncident } from './testUtils'; describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); @@ -38,7 +38,7 @@ describe('TriggerDialog', () => { wrapInTestApp( {}} onIncidentCreated={() => {}} @@ -53,34 +53,6 @@ describe('TriggerDialog', () => { exact: false, }), ).toBeInTheDocument(); - const incidentType = getByTestId('trigger-incident-type'); - const incidentId = getByTestId('trigger-incident-id'); - const incidentDisplayName = getByTestId('trigger-incident-displayName'); - const incidentMessage = getByTestId('trigger-incident-message'); - - await act(async () => { - 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 incident creation button - const triggerButton = getByTestId('trigger-button'); - await act(async () => { - fireEvent.click(triggerButton); - }); - expect(mockTriggerAlarmFn).toHaveBeenCalled(); - expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ - incidentType: 'CRITICAL', - incidentId: 'incident-id', - routingKey: 'Example', - incidentDisplayName: 'incident-display-name', - incidentMessage: 'incident-message', - }); + await expectTriggeredIncident('Example', getByTestId, mockTriggerAlarmFn); }); }); diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx index 82dd2da74d..dd8f0b1909 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx @@ -35,11 +35,11 @@ import { import useAsyncFn from 'react-use/lib/useAsyncFn'; import { splunkOnCallApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; -import { TriggerAlarmRequest } from '../../api/types'; +import { TriggerAlarmRequest } from '../../api'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; type Props = { - team: string; + routingKey: string; showDialog: boolean; handleDialog: () => void; onIncidentCreated: () => void; @@ -76,7 +76,7 @@ const useStyles = makeStyles((theme: Theme) => ); export const TriggerDialog = ({ - team, + routingKey, showDialog, handleDialog, onIncidentCreated: onIncidentCreated, @@ -221,7 +221,7 @@ export const TriggerDialog = ({ id="details" multiline fullWidth - rows="2" + minRows="2" margin="normal" label="Incident message" variant="outlined" @@ -242,7 +242,7 @@ export const TriggerDialog = ({ variant="contained" onClick={() => handleTriggerAlarm({ - routingKey: team, + routingKey, incidentType, incidentDisplayName, incidentMessage, diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts new file mode 100644 index 0000000000..764ac94e2c --- /dev/null +++ b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// eslint-disable-next-line import/no-extraneous-dependencies +import { + act, + fireEvent, + Matcher, + MatcherOptions, +} from '@testing-library/react'; + +export async function expectTriggeredIncident( + routingKey: string, + getByTestId: ( + id: Matcher, + options?: MatcherOptions | undefined, + ) => HTMLElement, + mockTriggerAlarmFn: any, +): Promise { + const incidentType = getByTestId('trigger-incident-type'); + const incidentId = getByTestId('trigger-incident-id'); + const incidentDisplayName = getByTestId('trigger-incident-displayName'); + const incidentMessage = getByTestId('trigger-incident-message'); + + await act(async () => { + 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 incident creation button + const triggerButton = getByTestId('trigger-button'); + await act(async () => { + fireEvent.click(triggerButton); + }); + + expect(mockTriggerAlarmFn).toHaveBeenCalled(); + expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ + incidentType: 'CRITICAL', + incidentId: 'incident-id', + routingKey: routingKey, + incidentDisplayName: 'incident-display-name', + incidentMessage: 'incident-message', + }); +}