Merge pull request #14463 from Bonial-International-GmbH/pjungermann/splunk/use-routing-key

feat(splunk-on-call): use routing key instead of team name for triggered incidents
This commit is contained in:
Fredrik Adelöw
2022-11-07 13:40:45 +01:00
committed by GitHub
6 changed files with 129 additions and 61 deletions
@@ -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<SplunkOnCallClient> = {
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(
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntityWithRoutingKeyAnnotation}>
@@ -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 () => {
@@ -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 <Progress />;
}
if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) {
if (!entityData?.foundTeams || !entityData?.foundTeams.length) {
return (
<InvalidAnnotation
teamName={teamAnnotation}
@@ -217,9 +221,11 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => {
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) => {
<EscalationPolicy team={teamName} users={usersHashMap} />
)}
<TriggerDialog
team={teamName}
routingKey={routingKey?.routingKey ?? teamName}
showDialog={showDialog}
handleDialog={handleDialog}
onIncidentCreated={handleRefresh}
@@ -257,7 +263,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => {
icon: <WebIcon />,
};
const teams = usersAndTeams?.foundTeams || [];
const teams = entityData?.foundTeams || [];
return (
<>
@@ -277,7 +283,11 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => {
/>
<Divider />
<CardContent>
<Content team={team} usersHashMap={usersAndTeams?.usersHashMap} />
<Content
team={team}
routingKey={entityData?.foundRoutingKey}
usersHashMap={entityData?.usersHashMap}
/>
</CardContent>
</Card>
))}
@@ -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(
<ApiProvider apis={apis}>
<TriggerDialog
team="Example"
routingKey="Example"
showDialog
handleDialog={() => {}}
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);
});
});
@@ -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,
@@ -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<void> {
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',
});
}