Merge pull request #4681 from joaomilho/allow-trigger-button-without-pagerduty-key

Improve PagerDuty plugins
This commit is contained in:
Patrik Oldsberg
2021-03-02 19:15:21 +01:00
committed by GitHub
15 changed files with 375 additions and 181 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ import {
{
isPagerDutyAvailable(entity) && (
<Grid item md={6}>
<PagerDutyCard entity={entity} />
<EntityPagerDutyCard />
</Grid>
);
}
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { render, waitFor, fireEvent, act } from '@testing-library/react';
import { PagerDutyCard } from './PagerDutyCard';
import { PagerDutyCard } from '../PagerDutyCard';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { wrapInTestApp } from '@backstage/test-utils';
@@ -25,8 +25,8 @@ import {
ApiRegistry,
createApiRef,
} from '@backstage/core';
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../api';
import { Service } from './types';
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api';
import { Service } from '../types';
const mockPagerDutyApi: Partial<PagerDutyClient> = {
getServiceByIntegrationKey: async () => [],
@@ -138,7 +138,7 @@ describe('PageDutyCard', () => {
.fn()
.mockImplementationOnce(async () => [service]);
const { getByText, queryByTestId, getByTestId, getByRole } = render(
const { getByText, queryByTestId, getByRole } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
@@ -149,10 +149,10 @@ describe('PageDutyCard', () => {
);
await waitFor(() => !queryByTestId('progress'));
expect(getByText('Service Directory')).toBeInTheDocument();
expect(getByText('Create Incident')).toBeInTheDocument();
const triggerButton = getByTestId('trigger-button');
const triggerLink = getByText('Create Incident');
await act(async () => {
fireEvent.click(triggerButton);
fireEvent.click(triggerLink);
});
expect(getByRole('dialog')).toBeInTheDocument();
});
@@ -14,43 +14,50 @@
* limitations under the License.
*/
import React, { useState, useCallback } from 'react';
import { useApi, Progress, HeaderIconLinkRow } from '@backstage/core';
import {
useApi,
Progress,
HeaderIconLinkRow,
IconLinkVerticalProps,
} from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Card, CardHeader, Divider, CardContent } from '@material-ui/core';
import { Incidents } from './Incident';
import { EscalationPolicy } from './Escalation';
import { Incidents } from '../Incident';
import { EscalationPolicy } from '../Escalation';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { pagerDutyApiRef, UnauthorizedError } from '../api';
import { pagerDutyApiRef, UnauthorizedError } from '../../api';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { MissingTokenError } from './Errors/MissingTokenError';
import { MissingTokenError } from '../Errors/MissingTokenError';
import WebIcon from '@material-ui/icons/Web';
import { PAGERDUTY_INTEGRATION_KEY } from './constants';
import { TriggerButton, useShowDialog } from './TriggerButton';
import { usePagerdutyEntity } from '../../hooks';
import { PAGERDUTY_INTEGRATION_KEY } from '../constants';
import { TriggerDialog } from '../TriggerDialog';
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
export const PagerDutyCard = () => {
const { entity } = useEntity();
const { integrationKey } = usePagerdutyEntity();
const api = useApi(pagerDutyApiRef);
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
const integrationKey = entity.metadata.annotations![
PAGERDUTY_INTEGRATION_KEY
];
const setShowDialog = useShowDialog()[1];
const [dialogShown, setDialogShown] = useState<boolean>(false);
const showDialog = useCallback(() => {
setShowDialog(true);
}, [setShowDialog]);
setDialogShown(true);
}, [setDialogShown]);
const hideDialog = useCallback(() => {
setDialogShown(false);
}, [setDialogShown]);
const handleRefresh = useCallback(() => {
setRefreshIncidents(x => !x);
}, []);
const { value: service, loading, error } = useAsync(async () => {
const services = await api.getServiceByIntegrationKey(integrationKey);
const services = await api.getServiceByIntegrationKey(
integrationKey as string,
);
return {
id: services[0].id,
@@ -76,32 +83,41 @@ export const PagerDutyCard = () => {
return <Progress />;
}
const serviceLink = {
const serviceLink: IconLinkVerticalProps = {
label: 'Service Directory',
href: service!.url,
icon: <WebIcon />,
};
const triggerLink = {
const triggerLink: IconLinkVerticalProps = {
label: 'Create Incident',
action: <TriggerButton design="link" onIncidentCreated={handleRefresh} />,
icon: <AlarmAddIcon onClick={showDialog} />,
onClick: showDialog,
icon: <AlarmAddIcon />,
color: 'secondary',
};
return (
<Card>
<CardHeader
title="PagerDuty"
subheader={<HeaderIconLinkRow links={[serviceLink, triggerLink]} />}
/>
<Divider />
<CardContent>
<Incidents
serviceId={service!.id}
refreshIncidents={refreshIncidents}
<>
<Card data-testid="pagerduty-card">
<CardHeader
title="PagerDuty"
subheader={<HeaderIconLinkRow links={[serviceLink, triggerLink]} />}
/>
<EscalationPolicy policyId={service!.policyId} />
</CardContent>
</Card>
<Divider />
<CardContent>
<Incidents
serviceId={service!.id}
refreshIncidents={refreshIncidents}
/>
<EscalationPolicy policyId={service!.policyId} />
</CardContent>
</Card>
<TriggerDialog
data-testid="trigger-dialog"
showDialog={dialogShown}
handleDialog={hideDialog}
onIncidentCreated={handleRefresh}
/>
</>
);
};
@@ -0,0 +1,142 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
import React from 'react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import {
ApiRegistry,
alertApiRef,
createApiRef,
ApiProvider,
IdentityApi,
identityApiRef,
} from '@backstage/core';
import { pagerDutyApiRef } from '../../api';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { TriggerButton } from './';
describe('TriggerButton', () => {
const mockIdentityApi: Partial<IdentityApi> = {
getUserId: () => 'guest@example.com',
};
const mockTriggerAlarmFn = jest.fn();
const mockPagerDutyApi = {
triggerAlarm: mockTriggerAlarmFn,
};
const apis = ApiRegistry.from([
[
alertApiRef,
createApiRef({
id: 'core.alert',
description: 'Used to report alerts and forward them to the app',
}),
],
[identityApiRef, mockIdentityApi],
[pagerDutyApiRef, mockPagerDutyApi],
]);
it('renders the trigger button, opens and closes dialog', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'pagerduty-test',
annotations: {
'pagerduty.com/integration-key': 'abc123',
},
},
};
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<TriggerButton />
</EntityProvider>
</ApiProvider>,
);
const triggerButton = screen.getByText('Create Incident');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
await act(async () => {
fireEvent.click(triggerButton);
});
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
const closeButton = screen.getByText('Close');
await act(async () => {
fireEvent.click(closeButton);
});
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
it('renders the trigger button with children', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'pagerduty-test',
annotations: {
'pagerduty.com/integration-key': 'abc123',
},
},
};
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<TriggerButton>Send an alert</TriggerButton>
</EntityProvider>
</ApiProvider>,
);
expect(screen.getByText('Send an alert')).toBeInTheDocument();
});
it('renders a disabled trigger button if entity does not include key', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'pagerduty-test',
},
};
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<TriggerButton />
</EntityProvider>
</ApiProvider>,
);
const triggerButton = screen.getByText('Missing integration key');
await act(async () => {
fireEvent.click(triggerButton);
});
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
});
@@ -13,19 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useCallback, PropsWithChildren } from 'react';
import { createGlobalState } from 'react-use';
import React, { useCallback, PropsWithChildren, useState } from 'react';
import { makeStyles, Button } from '@material-ui/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import { BackstageTheme } from '@backstage/theme';
import { usePagerdutyEntity } from '../../hooks';
import { TriggerDialog } from '../TriggerDialog';
import { PAGERDUTY_INTEGRATION_KEY } from '../constants';
export interface TriggerButtonProps {
design: 'link' | 'button';
onIncidentCreated?: () => void;
}
export type TriggerButtonProps = {};
const useStyles = makeStyles<BackstageTheme>(theme => ({
buttonStyle: {
@@ -35,31 +30,14 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
backgroundColor: theme.palette.error.dark,
},
},
triggerAlarm: {
paddingTop: 0,
paddingBottom: 0,
fontSize: '0.7rem',
textTransform: 'uppercase',
fontWeight: 600,
letterSpacing: 1.2,
lineHeight: 1.5,
'&:hover, &:focus, &.focus': {
backgroundColor: 'transparent',
textDecoration: 'none',
},
},
}));
export const useShowDialog = createGlobalState(false);
export function TriggerButton({
design,
onIncidentCreated,
children,
}: PropsWithChildren<TriggerButtonProps>) {
const { buttonStyle, triggerAlarm } = useStyles();
const { entity } = useEntity();
const [dialogShown = false, setDialogShown] = useShowDialog();
const { buttonStyle } = useStyles();
const { integrationKey } = usePagerdutyEntity();
const [dialogShown, setDialogShown] = useState<boolean>(false);
const showDialog = useCallback(() => {
setDialogShown(true);
@@ -68,27 +46,22 @@ export function TriggerButton({
setDialogShown(false);
}, [setDialogShown]);
const integrationKey = entity.metadata.annotations![
PAGERDUTY_INTEGRATION_KEY
];
const disabled = !integrationKey;
return (
<>
<Button
data-testid="trigger-button"
{...(design === 'link' && { color: 'secondary' })}
onClick={showDialog}
className={design === 'link' ? triggerAlarm : buttonStyle}
variant="contained"
className={disabled ? '' : buttonStyle}
disabled={disabled}
>
{children ?? 'Create Incident'}
{integrationKey
? children ?? 'Create Incident'
: 'Missing integration key'}
</Button>
<TriggerDialog
showDialog={dialogShown}
handleDialog={hideDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
onIncidentCreated={onIncidentCreated}
/>
{integrationKey && (
<TriggerDialog showDialog={dialogShown} handleDialog={hideDialog} />
)}
</>
);
}
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { fireEvent, act } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import {
ApiRegistry,
alertApiRef,
@@ -26,6 +26,7 @@ import {
} from '@backstage/core';
import { pagerDutyApiRef } from '../../api';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { TriggerDialog } from './TriggerDialog';
describe('TriggerDialog', () => {
@@ -62,18 +63,16 @@ describe('TriggerDialog', () => {
},
};
const { getByText, getByRole, getByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
const { getByText, getByRole, getByTestId } = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<TriggerDialog
showDialog
handleDialog={() => {}}
name={entity.metadata.name}
integrationKey="abc123"
onIncidentCreated={() => {}}
/>
</ApiProvider>,
),
</EntityProvider>
</ApiProvider>,
);
expect(getByRole('dialog')).toBeInTheDocument();
@@ -29,22 +29,20 @@ import { useApi, alertApiRef, identityApiRef } from '@backstage/core';
import { useAsyncFn } from 'react-use';
import { pagerDutyApiRef } from '../../api';
import { Alert } from '@material-ui/lab';
import { usePagerdutyEntity } from '../../hooks';
type Props = {
name: string;
integrationKey: string;
showDialog: boolean;
handleDialog: () => void;
onIncidentCreated?: () => void;
};
export const TriggerDialog = ({
name,
integrationKey,
showDialog,
handleDialog,
onIncidentCreated: onIncidentCreated,
}: Props) => {
const { name, integrationKey } = usePagerdutyEntity();
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userName = identityApi.getUserId();
@@ -54,7 +52,7 @@ export const TriggerDialog = ({
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
async (descriptions: string) =>
await api.triggerAlarm({
integrationKey,
integrationKey: integrationKey as string,
source: window.location.toString(),
description: descriptions,
userName,
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
import { useEntity } from '@backstage/plugin-catalog-react';
import { PAGERDUTY_INTEGRATION_KEY } from '../components/constants';
export function usePagerdutyEntity() {
const { entity } = useEntity();
const integrationKey =
entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY];
const name = entity.metadata.name;
return { integrationKey, name };
}