From 70903de8792dc3b1470c845994a0180f04b81f68 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 12:04:55 -0500 Subject: [PATCH 01/20] [wip] support 'splunk.com/on-call-routing-key' annotation Extends the Splunk On Call plugin Entity card to infer the corresponding team names from a `splunk.com/on-call-routing-key` annotation. This seeks to enable the use of the Splunk On Call plugin on entity pages for those orgs who associate a component with a routing key rather than a team name. Signed-off-by: Mike Ball --- plugins/splunk-on-call/src/api/client.ts | 11 ++++ plugins/splunk-on-call/src/api/types.ts | 11 ++++ .../src/components/EntitySplunkOnCallCard.tsx | 53 ++++++++++++++++--- .../splunk-on-call/src/components/types.ts | 12 +++++ 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 437c6365ed..6c0ff9bdc4 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -19,6 +19,7 @@ import { OnCall, User, EscalationPolicyInfo, + RoutingKey, Team, } from '../components/types'; import { @@ -30,6 +31,7 @@ import { RequestOptions, ListUserResponse, EscalationPolicyResponse, + ListRoutingKeyResponse, } from './types'; import { createApiRef, @@ -82,6 +84,15 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return teams; } + async getRoutingKeys(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/org/routing-keys`; + const { routingKeys } = await this.getByUrl(url); + + return routingKeys; + } + async getUsers(): Promise { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index 0bdd093ad1..6a414d18bd 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -18,6 +18,7 @@ import { EscalationPolicyInfo, Incident, OnCall, + RoutingKey, Team, User, } from '../components/types'; @@ -65,6 +66,11 @@ export interface SplunkOnCallApi { */ getTeams(): Promise; + /** + * Get a list of routing keys for your organization. + */ + getRoutingKeys(): Promise; + /** * Get a list of escalation policies for your organization. */ @@ -80,6 +86,11 @@ export type ListUserResponse = { _selfUrl?: string; }; +export type ListRoutingKeyResponse = { + routingKeys: RoutingKey[]; + _selfUrl?: string; +}; + export type IncidentsResponse = { incidents: Incident[]; }; diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index bca53a3b0a..ef4ad0388b 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -45,9 +45,18 @@ import { } from '@backstage/core-components'; export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team'; +export const SPLUNK_ON_CALL_ROUTING_KEY = 'splunk.com/on-call-routing-key'; -export const MissingTeamAnnotation = () => ( - +export const MissingAnnotation = () => ( +
+ + The Splunk On Call plugin requires setting either the{' '} + {SPLUNK_ON_CALL_TEAM} or the{' '} + {SPLUNK_ON_CALL_ROUTING_KEY} annotation. + + + +
); export const InvalidTeamAnnotation = ({ teamName }: { teamName: string }) => ( @@ -71,7 +80,8 @@ export const MissingEventsRestEndpoint = () => ( ); export const isSplunkOnCallAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]); + Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) || + Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_ROUTING_KEY]); export const EntitySplunkOnCallCard = () => { const config = useApi(configApiRef); @@ -79,7 +89,12 @@ export const EntitySplunkOnCallCard = () => { const { entity } = useEntity(); const [showDialog, setShowDialog] = useState(false); const [refreshIncidents, setRefreshIncidents] = useState(false); - const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; + const teamAnnotation = entity + ? entity.metadata.annotations![SPLUNK_ON_CALL_TEAM] + : undefined; + const routingKeyAnnotation = entity + ? entity.metadata.annotations![SPLUNK_ON_CALL_ROUTING_KEY] + : undefined; const eventsRestEndpoint = config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; @@ -108,7 +123,20 @@ export const EntitySplunkOnCallCard = () => { {}, ); const teams = await api.getTeams(); - const foundTeam = teams.find(teamValue => teamValue.name === team); + let foundTeam = teams.find(teamValue => teamValue.name === teamAnnotation); + + if (!foundTeam && routingKeyAnnotation) { + const routingKeys = await api.getRoutingKeys(); + const foundRoutingKey = routingKeys.find( + key => key.routingKey === routingKeyAnnotation, + ); + const teamUrlParts = foundRoutingKey + ? foundRoutingKey.targets[0]._teamUrl.split('/') + : []; + const teamSlug = teamUrlParts[teamUrlParts.length - 1]; + foundTeam = teams.find(teamValue => teamValue.slug === teamSlug); + } + return { usersHashMap, foundTeam }; }); @@ -128,13 +156,22 @@ export const EntitySplunkOnCallCard = () => { return ; } + const team = + usersAndTeam?.foundTeam && usersAndTeam?.foundTeam.name + ? usersAndTeam?.foundTeam.name + : ''; + const Content = () => { - if (!team) { - return ; + if (!teamAnnotation && !routingKeyAnnotation) { + return ; } if (!usersAndTeam?.foundTeam) { - return ; + return ( + + ); } if (!eventsRestEndpoint) { diff --git a/plugins/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts index 3c1902e52b..0feefe8b03 100644 --- a/plugins/splunk-on-call/src/components/types.ts +++ b/plugins/splunk-on-call/src/components/types.ts @@ -117,3 +117,15 @@ export type EscalationPolicyTeam = { name: string; slug: string; }; + +export type RoutingKey = { + routingKey: string; + targets: RoutingKeyTarget[]; + isDefault: boolean; +}; + +export type RoutingKeyTarget = { + policyName: string; + policySlug: string; + _teamUrl: string; +}; From 4c61bc0cfa73bdd65467a65a7da1d38bca1245b6 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 16:08:22 -0500 Subject: [PATCH 02/20] render Splunk On-Call card per team In instances where a `splunk.com/on-call-routing-key` is provided and that routing key is associated with multiple teams, the component now renders an individual Splunk On-Call card for each team. Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.tsx | 89 +++++++++++-------- 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index ef4ad0388b..1d3e639910 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -32,8 +32,7 @@ import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError'; import { EscalationPolicy } from './Escalation'; import { Incidents } from './Incident'; import { TriggerDialog } from './TriggerDialog'; -import { User } from './types'; - +import { Team, User } from './types'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { @@ -108,7 +107,7 @@ export const EntitySplunkOnCallCard = () => { }, []); const { - value: usersAndTeam, + value: usersAndTeams, loading, error, } = useAsync(async () => { @@ -123,21 +122,26 @@ export const EntitySplunkOnCallCard = () => { {}, ); const teams = await api.getTeams(); - let foundTeam = teams.find(teamValue => teamValue.name === teamAnnotation); + let foundTeams = [ + teams.find(teamValue => teamValue.name === teamAnnotation), + ].filter(team => team !== undefined); - if (!foundTeam && routingKeyAnnotation) { + if (!foundTeams.length && routingKeyAnnotation) { const routingKeys = await api.getRoutingKeys(); const foundRoutingKey = routingKeys.find( key => key.routingKey === routingKeyAnnotation, ); - const teamUrlParts = foundRoutingKey - ? foundRoutingKey.targets[0]._teamUrl.split('/') + foundTeams = foundRoutingKey + ? foundRoutingKey.targets.map(target => { + const teamUrlParts = target._teamUrl.split('/'); + const teamSlug = teamUrlParts[teamUrlParts.length - 1]; + + return teams.find(teamValue => teamValue.slug === teamSlug); + }) : []; - const teamSlug = teamUrlParts[teamUrlParts.length - 1]; - foundTeam = teams.find(teamValue => teamValue.slug === teamSlug); } - return { usersHashMap, foundTeam }; + return { usersHashMap, foundTeams }; }); if (error instanceof UnauthorizedError) { @@ -156,17 +160,18 @@ export const EntitySplunkOnCallCard = () => { return ; } - const team = - usersAndTeam?.foundTeam && usersAndTeam?.foundTeam.name - ? usersAndTeam?.foundTeam.name - : ''; - - const Content = () => { + const Content = ({ + team, + usersHashMap, + }: { + team: Team | undefined; + usersHashMap: any | undefined; + }) => { if (!teamAnnotation && !routingKeyAnnotation) { return ; } - if (!usersAndTeam?.foundTeam) { + if (!team) { return ( { return ; } + const teamName = team.name || ''; + return ( <> - - {usersAndTeam?.usersHashMap && team && ( - + + {usersHashMap && team && ( + )} { icon: , }; + const teams = usersAndTeams?.foundTeams || []; + return ( - - Team: {team}, - , - ]} - /> - - - - - + <> + {teams.map((team, i) => ( + + + Team: {team && team.name ? team.name : ''} + , + , + ]} + /> + + + + + + ))} + ); }; From 0dfbb30a5db99afd6a209ad76b978eb12a75ddb4 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 16:33:18 -0500 Subject: [PATCH 03/20] add 1em spacing between each Splunk On-Call card Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 1d3e639910..8dbbf6e848 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -22,6 +22,7 @@ import { CardContent, CardHeader, Divider, + makeStyles, Typography, } from '@material-ui/core'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; @@ -82,7 +83,14 @@ export const isSplunkOnCallAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) || Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_ROUTING_KEY]); +const useStyles = makeStyles({ + onCallCard: { + marginBottom: '1em', + }, +}); + export const EntitySplunkOnCallCard = () => { + const classes = useStyles(); const config = useApi(configApiRef); const api = useApi(splunkOnCallApiRef); const { entity } = useEntity(); @@ -219,7 +227,7 @@ export const EntitySplunkOnCallCard = () => { return ( <> {teams.map((team, i) => ( - + Date: Thu, 3 Feb 2022 16:45:42 -0500 Subject: [PATCH 04/20] properly render components Corrects logic ensuring `` and `` render correctly. Previously, neither component would render from within the `` component. Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 8dbbf6e848..5f9db7e415 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -152,6 +152,14 @@ export const EntitySplunkOnCallCard = () => { return { usersHashMap, foundTeams }; }); + if (!teamAnnotation && !routingKeyAnnotation) { + return ; + } + + if (!eventsRestEndpoint) { + return ; + } + if (error instanceof UnauthorizedError) { return ; } @@ -175,10 +183,6 @@ export const EntitySplunkOnCallCard = () => { team: Team | undefined; usersHashMap: any | undefined; }) => { - if (!teamAnnotation && !routingKeyAnnotation) { - return ; - } - if (!team) { return ( { ); } - if (!eventsRestEndpoint) { - return ; - } - const teamName = team.name || ''; return ( From f408cabe8f93bdc828e661c3fb75bde42c309bf4 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 17:09:17 -0500 Subject: [PATCH 05/20] handle invalid Splunk On-Call annotations If a `splunk.com/on-call-team` annotation is provided and the API returns no associated team, render... ``` Splunk On-Call API returned no record of teams associated with the "foo" team name Escalation Policy and incident information unavailable. Splunk On-Call requires a valid team name or routing key. ``` If a `splunk.com/on-call-routing-key` annotation is provided and the API returns no associated team, render... ``` Splunk On-Call API returned no record of teams associated with the "foo" routing key Escalation Policy and incident information unavailable. Splunk On-Call requires a valid team name or routing key. ``` Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.tsx | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 5f9db7e415..559f7c551e 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -59,15 +59,36 @@ export const MissingAnnotation = () => ( ); -export const InvalidTeamAnnotation = ({ teamName }: { teamName: string }) => ( - - - -); +export const InvalidAnnotation = ({ + teamName, + routingKey, +}: { + teamName: string | undefined; + routingKey: string | undefined; +}) => { + let titleSuffix = 'provided annotation'; + + if (teamName) { + titleSuffix = `"${teamName}" team name`; + } + + if (routingKey) { + titleSuffix = `"${routingKey}" routing key`; + } + + return ( + + + + + + + ); +}; export const MissingEventsRestEndpoint = () => ( @@ -181,17 +202,9 @@ export const EntitySplunkOnCallCard = () => { usersHashMap, }: { team: Team | undefined; - usersHashMap: any | undefined; + usersHashMap: any; }) => { - if (!team) { - return ( - - ); - } - - const teamName = team.name || ''; + const teamName = team && team.name ? team.name : ''; return ( <> @@ -224,6 +237,15 @@ export const EntitySplunkOnCallCard = () => { const teams = usersAndTeams?.foundTeams || []; + if (!teams.length) { + return ( + + ); + } + return ( <> {teams.map((team, i) => ( From 7fa7857d4885bea7ef3dcfca9179c9dee1d88f0b Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 18:51:06 -0500 Subject: [PATCH 06/20] correct incorrect team annotation test Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index eb5c99d4e7..da2cfbc2a3 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -170,7 +170,9 @@ describe('SplunkOnCallCard', () => { ); await waitFor(() => !queryByTestId('progress')); expect( - getByText('Could not find team named "test" in the Splunk On-Call API'), + getByText( + 'Splunk On-Call API returned no record of teams associated with the "test" team name', + ), ).toBeInTheDocument(); }); From 4bda7d3045e7d36ce9a9fb87a3a24c74117724a8 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 20:03:43 -0500 Subject: [PATCH 07/20] test warning for incorrect routing key annotation This adds a test asserting that the correct component is rendered when the entity `splunk.com/on-call-routing-key` does not properly map to any associated teams. Signed-off-by: Mike Ball --- plugins/splunk-on-call/src/api/mocks.ts | 13 ++++++ .../EntitySplunkOnCallCard.test.tsx | 41 +++++++++++++++++++ .../src/components/EntitySplunkOnCallCard.tsx | 30 +++++++------- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/plugins/splunk-on-call/src/api/mocks.ts b/plugins/splunk-on-call/src/api/mocks.ts index d5335bfef4..8db06fb9d3 100644 --- a/plugins/splunk-on-call/src/api/mocks.ts +++ b/plugins/splunk-on-call/src/api/mocks.ts @@ -17,6 +17,7 @@ import { EscalationPolicyInfo, Incident, + RoutingKey, Team, User, } from '../components/types'; @@ -88,6 +89,18 @@ export const MOCK_TEAM: Team = { isDefaultTeam: false, }; +export const MOCK_ROUTING_KEY: RoutingKey = { + routingKey: 'test-routing-key', + targets: [ + { + policyName: 'some policy', + policySlug: MOCK_TEAM.slug || '', + _teamUrl: `/api-public/v1/team/${MOCK_TEAM.slug}`, + }, + ], + isDefault: false, +}; + export const MOCK_TEAM_NO_INCIDENTS: Team = { ...MOCK_TEAM, name: 'test-noincidents', diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index da2cfbc2a3..8b58af8e46 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -28,6 +28,7 @@ import { MOCKED_ON_CALL, MOCKED_USER, MOCK_INCIDENT, + MOCK_ROUTING_KEY, MOCK_TEAM, MOCK_TEAM_NO_INCIDENTS, } from '../api/mocks'; @@ -45,6 +46,7 @@ const mockSplunkOnCallApi: Partial = { getIncidents: async () => [MOCK_INCIDENT], getOnCallUsers: async () => MOCKED_ON_CALL, getTeams: async () => [MOCK_TEAM], + getRoutingKeys: async () => [MOCK_ROUTING_KEY], getEscalationPolicies: async () => ESCALATION_POLICIES, }; @@ -71,6 +73,17 @@ const mockEntity = { }, } as Entity; +const mockEntityWithRoutingKeyAnnotation = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'splunkoncall-test', + annotations: { + 'splunk.com/on-call-routing-key': MOCK_ROUTING_KEY.routingKey, + }, + }, +} as Entity; + const mockEntityNoIncidents = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -176,6 +189,34 @@ describe('SplunkOnCallCard', () => { ).toBeInTheDocument(); }); + it('handles warning for incorrect routing key annotation', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + mockSplunkOnCallApi.getRoutingKeys = jest + .fn() + .mockImplementationOnce(async () => [MOCK_ROUTING_KEY]); + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementationOnce(async () => []); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText( + `Splunk On-Call API returned no record of teams associated with the "${MOCK_ROUTING_KEY.routingKey}" routing key`, + ), + ).toBeInTheDocument(); + }); + it('opens the dialog when trigger button is clicked', async () => { mockSplunkOnCallApi.getUsers = jest .fn() diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 559f7c551e..db479f9459 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -161,12 +161,14 @@ export const EntitySplunkOnCallCard = () => { key => key.routingKey === routingKeyAnnotation, ); foundTeams = foundRoutingKey - ? foundRoutingKey.targets.map(target => { - const teamUrlParts = target._teamUrl.split('/'); - const teamSlug = teamUrlParts[teamUrlParts.length - 1]; + ? foundRoutingKey.targets + .map(target => { + const teamUrlParts = target._teamUrl.split('/'); + const teamSlug = teamUrlParts[teamUrlParts.length - 1]; - return teams.find(teamValue => teamValue.slug === teamSlug); - }) + return teams.find(teamValue => teamValue.slug === teamSlug); + }) + .filter(team => team !== undefined) : []; } @@ -197,6 +199,15 @@ export const EntitySplunkOnCallCard = () => { return ; } + if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) { + return ( + + ); + } + const Content = ({ team, usersHashMap, @@ -237,15 +248,6 @@ export const EntitySplunkOnCallCard = () => { const teams = usersAndTeams?.foundTeams || []; - if (!teams.length) { - return ( - - ); - } - return ( <> {teams.map((team, i) => ( From 07ffc7ca9d2a0edeba69c4a86d50afa09e77852d Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 20:19:36 -0500 Subject: [PATCH 08/20] test when no Splunk On Call annotations are provided Signed-off-by: Mike Ball --- .../EntitySplunkOnCallCard.test.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 8b58af8e46..05d9a071c3 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -84,6 +84,15 @@ const mockEntityWithRoutingKeyAnnotation = { }, } as Entity; +const mockEntityNoAnnotation = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'splunkoncall-test', + annotations: {}, + }, +} as Entity; + const mockEntityNoIncidents = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -164,6 +173,20 @@ describe('SplunkOnCallCard', () => { ).toBeInTheDocument(); }); + it('handles warning for missing required annotations', async () => { + const { getAllByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getAllByText('Missing Annotation').length).toEqual(2); + }); + it('handles warning for incorrect team annotation', async () => { mockSplunkOnCallApi.getUsers = jest .fn() From 5669e32ac1ac5494eb4273ae516a673422687096 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Thu, 3 Feb 2022 20:30:09 -0500 Subject: [PATCH 09/20] test component rendering w/ new annotation Test that the Splunk On Call component properly renders when a `splunk.com/on-call-routing-key` annotation is used. Signed-off-by: Mike Ball --- .../EntitySplunkOnCallCard.test.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 05d9a071c3..9c16752421 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -131,6 +131,35 @@ describe('SplunkOnCallCard', () => { expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); + it('handles a "splunk.com/on-call-routing-key" annotation', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + mockSplunkOnCallApi.getRoutingKeys = jest + .fn() + .mockImplementationOnce(async () => [MOCK_ROUTING_KEY]); + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementation(async () => [MOCK_TEAM]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument(); + await waitFor( + () => + expect(getByText(MOCK_INCIDENT.entityDisplayName)).toBeInTheDocument(), + { timeout: 2000 }, + ); + }); + it('Handles custom error for missing token', async () => { mockSplunkOnCallApi.getUsers = jest .fn() From ba1a3e9456363bd59d5e56042385fd26588be876 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 4 Feb 2022 07:51:18 -0500 Subject: [PATCH 10/20] add splunk.com/on-call-routing-key docs to README Signed-off-by: Mike Ball --- plugins/splunk-on-call/README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 52803473b0..ddf31ee49f 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -2,11 +2,11 @@ ## Overview -This plugin displays Splunk On-Call, formerly VictorOps, information about an entity. +This plugin displays Splunk On-Call (formerly VictorOps) information associated with an entity. -There is a way to trigger an new incident directly to specific users or/and specific teams. +It also provides the ability to trigger new incidents directly to specific users or/and specific teams from within Backstage. -This plugin requires that entities are annotated with a team name. See more further down in this document. +This plugin requires that entities feature either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. See below for further details. This plugin provides: @@ -76,12 +76,22 @@ In addition, to make certain API calls (trigger-resolve-acknowledge an incident) ### Adding your team name to the entity annotation -The information displayed for each entity is based on the team name. -If you want to use this plugin for an entity, you need to label it with the below annotation: +The information displayed for each entity is based on either an associated team name or an associated routing key. + +To use this plugin for an entity, the entity must be labeled with either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. + +For example, by specifying a `splunk.com/on-call-team`, the plugin displays Splunk On-Call data associated with the specified team: ```yaml annotations: - splunk.com/on-call-team': + splunk.com/on-call-team: +``` + +Alternatively, by specifying a `splunk.com/on-call-routing-key`, the plugin displays Splunk On-Call data associated with _each_ of the teams associated with the specified routing key: + +```yaml +annotations: + splunk.com/on-call-routing-key: ``` ### Create the Routing Key From c17be55ffbddbd7354556c8f68d49453cf8eca3f Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 4 Feb 2022 07:57:51 -0500 Subject: [PATCH 11/20] add Splunk On-Call plugin changeset Adds a changeset associated with the Splunk On-Call plugin's support for a new `splunk.com/on-call-routing-key` annotation. Signed-off-by: Mike Ball --- .changeset/honest-foxes-scream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/honest-foxes-scream.md diff --git a/.changeset/honest-foxes-scream.md b/.changeset/honest-foxes-scream.md new file mode 100644 index 0000000000..c684c40d72 --- /dev/null +++ b/.changeset/honest-foxes-scream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-splunk-on-call': minor +--- + +Add Splunk On-Call plugin support for a 'splunk.com/on-call-routing-key' annotation. If the 'splunk.com/on-call-routing-key' is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key. From 12182960f421a5ca37ca21fdd15999000f31952c Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 4 Feb 2022 08:21:30 -0500 Subject: [PATCH 12/20] address TypeScript compilation failure This fixes... ``` Run yarn tsc yarn run v1.22.1 $ tsc plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx:158:26 - error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'Matcher'. Type 'undefined' is not assignable to type 'Matcher'. 158 expect(getByText(MOCK_INCIDENT.entityDisplayName)).toBeInTheDocument(), ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 9c16752421..da248ebec0 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -154,8 +154,7 @@ describe('SplunkOnCallCard', () => { await waitFor(() => !queryByTestId('progress')); expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument(); await waitFor( - () => - expect(getByText(MOCK_INCIDENT.entityDisplayName)).toBeInTheDocument(), + () => expect(getByText('test-incident')).toBeInTheDocument(), { timeout: 2000 }, ); }); From 4ddc657bd349e427720e07b3be61884433993a5c Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 4 Feb 2022 08:32:08 -0500 Subject: [PATCH 13/20] add Splunk client `getRoutingKeys` method documentation Signed-off-by: Mike Ball --- plugins/splunk-on-call/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/splunk-on-call/api-report.md b/plugins/splunk-on-call/api-report.md index 19f60ffd13..8e8076d9e8 100644 --- a/plugins/splunk-on-call/api-report.md +++ b/plugins/splunk-on-call/api-report.md @@ -51,6 +51,10 @@ export class SplunkOnCallClient implements SplunkOnCallApi { // // (undocumented) getOnCallUsers(): Promise; + // Warning: (ae-forgotten-export) The symbol "RoutingKey" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getRoutingKeys(): Promise; // Warning: (ae-forgotten-export) The symbol "Team" needs to be exported by the entry point index.d.ts // // (undocumented) From a957b9fc4d5ec80f8da55675881aa1e886bb6812 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 4 Feb 2022 08:38:38 -0500 Subject: [PATCH 14/20] fine-tune Splunk On-Call README language Signed-off-by: Mike Ball --- plugins/splunk-on-call/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index ddf31ee49f..0200cacc50 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -4,14 +4,14 @@ This plugin displays Splunk On-Call (formerly VictorOps) information associated with an entity. -It also provides the ability to trigger new incidents directly to specific users or/and specific teams from within Backstage. +It also provides the ability to trigger new incidents to specific users and/or specific teams from within Backstage. This plugin requires that entities feature either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. See below for further details. This plugin provides: - A list of incidents -- A way to trigger a new incident to specific users or/and teams +- A way to trigger a new incident to specific users and/or teams - A way to acknowledge/resolve an incident - Information details about the persons on-call @@ -47,7 +47,7 @@ const overviewContent = ( ## Client configuration -In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint. +In order to be able to perform certain actions (create-acknowledge-resolve an action), you need to provide a REST Endpoint. 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` From da9d1723f6f35b50b4af5f16fc92442b0724c593 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Fri, 4 Feb 2022 10:04:49 -0500 Subject: [PATCH 15/20] invalid annotation logic better reflects component logic The EntitySplunkOnCallCard gives precedence to a `splunk.com/on-call-team` annotation. Therefore, the InvalidAnnotation component messaging should reflect that precedence, even in instances where _both_ supported annotations are provided and both are deemed invalid. Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index db479f9459..109484ac04 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -68,14 +68,14 @@ export const InvalidAnnotation = ({ }) => { let titleSuffix = 'provided annotation'; - if (teamName) { - titleSuffix = `"${teamName}" team name`; - } - if (routingKey) { titleSuffix = `"${routingKey}" routing key`; } + if (teamName) { + titleSuffix = `"${teamName}" team name`; + } + return ( From 9fa811ddbb6eb440c4a518989fd55b054b120dab Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 7 Feb 2022 10:06:23 -0500 Subject: [PATCH 16/20] Update .changeset/honest-foxes-scream.md Co-authored-by: Johan Haals Signed-off-by: Mike Ball --- .changeset/honest-foxes-scream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/honest-foxes-scream.md b/.changeset/honest-foxes-scream.md index c684c40d72..4737e47cef 100644 --- a/.changeset/honest-foxes-scream.md +++ b/.changeset/honest-foxes-scream.md @@ -2,4 +2,4 @@ '@backstage/plugin-splunk-on-call': minor --- -Add Splunk On-Call plugin support for a 'splunk.com/on-call-routing-key' annotation. If the 'splunk.com/on-call-routing-key' is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key. +Add Splunk On-Call plugin support for a `splunk.com/on-call-routing-key` annotation. If the `splunk.com/on-call-routing-key` is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key. From 1514d8a3137148964ab78907fa0c0739fde8b8ba Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 7 Feb 2022 10:06:30 -0500 Subject: [PATCH 17/20] Update plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx Co-authored-by: Johan Haals Signed-off-by: Mike Ball --- .../splunk-on-call/src/components/EntitySplunkOnCallCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 109484ac04..70aeb98cbd 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -215,7 +215,7 @@ export const EntitySplunkOnCallCard = () => { team: Team | undefined; usersHashMap: any; }) => { - const teamName = team && team.name ? team.name : ''; + const teamName = team?.name ?? ''; return ( <> From 04365cd21355ed000389c764543811e675c3b211 Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 7 Feb 2022 12:12:35 -0500 Subject: [PATCH 18/20] display only 1 `` Per code review feedback, it's arguably unnecessary to display duplicate ``, especially given the preceding contextual sentence noting the supported annotations. Signed-off-by: Mike Ball --- plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index 70aeb98cbd..4a6f983636 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -55,7 +55,6 @@ export const MissingAnnotation = () => ( {SPLUNK_ON_CALL_ROUTING_KEY} annotation. - ); From 9552df7653c0f0b2ef08c961f4ca503a893f055a Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 7 Feb 2022 13:55:01 -0500 Subject: [PATCH 19/20] test reflects occurrence of single 'Missing Annotation' Signed-off-by: Mike Ball --- .../src/components/EntitySplunkOnCallCard.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index da248ebec0..a83db136e4 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -212,7 +212,7 @@ describe('SplunkOnCallCard', () => { ), ); await waitFor(() => !queryByTestId('progress')); - expect(getAllByText('Missing Annotation').length).toEqual(2); + expect(getAllByText('Missing Annotation').length).toEqual(1); }); it('handles warning for incorrect team annotation', async () => { From 5255292a2a0c5e7a84ea59f53c74439f21e4b4ac Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Mon, 7 Feb 2022 17:04:01 -0500 Subject: [PATCH 20/20] update changeset to be 'patch' change Per code review feedback: https://github.com/backstage/backstage/pull/9362#discussion_r800480438 Signed-off-by: Mike Ball --- .changeset/honest-foxes-scream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/honest-foxes-scream.md b/.changeset/honest-foxes-scream.md index 4737e47cef..f2d80b63a9 100644 --- a/.changeset/honest-foxes-scream.md +++ b/.changeset/honest-foxes-scream.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-splunk-on-call': minor +'@backstage/plugin-splunk-on-call': patch --- Add Splunk On-Call plugin support for a `splunk.com/on-call-routing-key` annotation. If the `splunk.com/on-call-routing-key` is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key.