= {
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,26 @@ 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 mockEntityNoAnnotation = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'splunkoncall-test',
+ annotations: {},
+ },
+} as Entity;
+
const mockEntityNoIncidents = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -109,6 +131,34 @@ 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('test-incident')).toBeInTheDocument(),
+ { timeout: 2000 },
+ );
+ });
+
it('Handles custom error for missing token', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
@@ -151,6 +201,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(1);
+ });
+
it('handles warning for incorrect team annotation', async () => {
mockSplunkOnCallApi.getUsers = jest
.fn()
@@ -170,7 +234,37 @@ 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();
+ });
+
+ 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();
});
diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx
index bca53a3b0a..4a6f983636 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';
@@ -32,8 +33,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 {
@@ -45,20 +45,49 @@ 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 }) => (
-
-
-
-);
+export const InvalidAnnotation = ({
+ teamName,
+ routingKey,
+}: {
+ teamName: string | undefined;
+ routingKey: string | undefined;
+}) => {
+ let titleSuffix = 'provided annotation';
+
+ if (routingKey) {
+ titleSuffix = `"${routingKey}" routing key`;
+ }
+
+ if (teamName) {
+ titleSuffix = `"${teamName}" team name`;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
export const MissingEventsRestEndpoint = () => (
@@ -71,15 +100,28 @@ 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]);
+
+const useStyles = makeStyles({
+ onCallCard: {
+ marginBottom: '1em',
+ },
+});
export const EntitySplunkOnCallCard = () => {
+ const classes = useStyles();
const config = useApi(configApiRef);
const api = useApi(splunkOnCallApiRef);
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;
@@ -93,7 +135,7 @@ export const EntitySplunkOnCallCard = () => {
}, []);
const {
- value: usersAndTeam,
+ value: usersAndTeams,
loading,
error,
} = useAsync(async () => {
@@ -108,10 +150,38 @@ export const EntitySplunkOnCallCard = () => {
{},
);
const teams = await api.getTeams();
- const foundTeam = teams.find(teamValue => teamValue.name === team);
- return { usersHashMap, foundTeam };
+ let foundTeams = [
+ teams.find(teamValue => teamValue.name === teamAnnotation),
+ ].filter(team => team !== undefined);
+
+ if (!foundTeams.length && routingKeyAnnotation) {
+ const routingKeys = await api.getRoutingKeys();
+ const foundRoutingKey = routingKeys.find(
+ key => key.routingKey === routingKeyAnnotation,
+ );
+ foundTeams = foundRoutingKey
+ ? foundRoutingKey.targets
+ .map(target => {
+ const teamUrlParts = target._teamUrl.split('/');
+ const teamSlug = teamUrlParts[teamUrlParts.length - 1];
+
+ return teams.find(teamValue => teamValue.slug === teamSlug);
+ })
+ .filter(team => team !== undefined)
+ : [];
+ }
+
+ return { usersHashMap, foundTeams };
});
+ if (!teamAnnotation && !routingKeyAnnotation) {
+ return ;
+ }
+
+ if (!eventsRestEndpoint) {
+ return ;
+ }
+
if (error instanceof UnauthorizedError) {
return ;
}
@@ -128,27 +198,32 @@ export const EntitySplunkOnCallCard = () => {
return ;
}
- const Content = () => {
- if (!team) {
- return ;
- }
+ if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) {
+ return (
+
+ );
+ }
- if (!usersAndTeam?.foundTeam) {
- return ;
- }
-
- if (!eventsRestEndpoint) {
- return ;
- }
+ const Content = ({
+ team,
+ usersHashMap,
+ }: {
+ team: Team | undefined;
+ usersHashMap: any;
+ }) => {
+ 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 : ''}
+ ,
+ ,
+ ]}
+ />
+
+
+
+
+
+ ))}
+ >
);
};
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;
+};
diff --git a/yarn.lock b/yarn.lock
index 92a7e80f97..a5cc9933e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4653,13 +4653,13 @@
react-use "^17.2.4"
"@roadiehq/backstage-plugin-github-pull-requests@^1.3.2":
- version "1.3.4"
- resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.3.4.tgz#3e6be41fe81909b28a3d517aadcc7ae6b70d7d7a"
- integrity sha512-F1y3CmMZOiNiam3zt6TBaJ5+NqAhPwS3HkWbUsMvJq1PKAzYdSeDBPhoTBwhYOu3uV36EtKjfog4r4wVHOf7EA==
+ version "1.3.7"
+ resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.3.7.tgz#d3c884c9b85f88c5f851f383596a616598c56076"
+ integrity sha512-a3PBLQ6SRtEDGNIekcGT8v4Aaac3BoEww/UU9uaQuE7bCEVolx2MeOVbSxb1uCWr6C6uGeDF5Kw4S4hPJJcnNg==
dependencies:
"@backstage/catalog-model" "^0.9.7"
"@backstage/core-components" "^0.8.0"
- "@backstage/core-plugin-api" "^0.4.0"
+ "@backstage/core-plugin-api" "^0.6.0"
"@backstage/plugin-catalog-react" "^0.6.5"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"