fix updating incidents and create AboutCard and use it

This commit is contained in:
Samira Mokaram
2020-12-04 09:28:48 +01:00
parent 2b027c9138
commit 3dee3093de
10 changed files with 254 additions and 94 deletions
@@ -0,0 +1,33 @@
/*
* 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 { Card, CardContent, CardHeader, Divider } from '@material-ui/core';
import { SubHeader } from './SubHeader';
import { SubHeaderLink } from '../types';
type Props = {
title: string;
links: SubHeaderLink[];
content: React.ReactNode;
};
export const AboutCard = ({ title, links, content }: Props) => (
<Card>
<CardHeader title={title} subheader={<SubHeader links={links} />} />
<Divider />
<CardContent>{content}</CardContent>
</Card>
);
@@ -0,0 +1,50 @@
/*
* 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 { VerticalIcon } from './VerticalIcon';
import { SubHeaderLink } from '../types';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
links: {
margin: theme.spacing(2, 0),
display: 'grid',
gridAutoFlow: 'column',
gridAutoColumns: 'min-content',
gridGap: theme.spacing(3),
},
}));
type Props = {
links: SubHeaderLink[];
};
export const SubHeader = ({ links }: Props) => {
const classes = useStyles();
return (
<nav className={classes.links}>
{links.map(link => (
<VerticalIcon
key={link.title}
label={link.title}
href={link.href ?? '#'}
action={link.action}
icon={link.icon}
/>
))}
</nav>
);
};
@@ -0,0 +1,85 @@
/*
* 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 classnames from 'classnames';
import { makeStyles, Link } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
import { Link as RouterLink } from 'react-router-dom';
export type VerticalIconProps = {
icon?: React.ReactNode;
href?: string;
title?: string;
label: string;
action?: React.ReactNode;
};
const useIconStyles = makeStyles(theme => ({
link: {
display: 'grid',
justifyItems: 'center',
gridGap: 4,
textAlign: 'center',
},
label: {
fontSize: '0.7rem',
textTransform: 'uppercase',
fontWeight: 600,
letterSpacing: 1.2,
},
linkStyle: {
color: theme.palette.secondary.main,
},
}));
export function VerticalIcon({
icon = <LinkIcon />,
href = '#',
action,
...props
}: VerticalIconProps) {
const classes = useIconStyles();
if (action) {
return (
<Link
className={classnames(classes.link, classes.linkStyle)}
href={href}
{...props}
>
{icon}
{action}
</Link>
);
}
// Absolute links should not be using RouterLink
if (href?.startsWith('//') || href?.includes('://')) {
return (
<Link className={classes.link} href={href} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
</Link>
);
}
return (
<Link className={classes.link} to={href} component={RouterLink} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
</Link>
);
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { AboutCard } from './AboutCard';
// export { SubHeader } from './SubHeader';
// export { VerticalIcon } from './VerticalIcon';
@@ -25,16 +25,14 @@ import { Alert } from '@material-ui/lab';
type Props = {
serviceId: string;
refreshIncidents: Boolean;
refreshIncidents: boolean;
};
export const Incidents = ({ serviceId, refreshIncidents }: Props) => {
const api = useApi(pagerDutyApiRef);
const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
async () => {
return await api.getIncidentsByServiceId(serviceId);
},
async () => await api.getIncidentsByServiceId(serviceId),
);
useEffect(() => {
@@ -85,11 +85,9 @@ describe('PageDutyCard', () => {
),
);
await waitFor(() => !queryByTestId('progress'));
expect(getByText('View in PagerDuty')).toBeInTheDocument();
expect(getByText('Trigger Alarm')).toBeInTheDocument();
expect(
getByText('Nice! No incidents are assigned to you!'),
).toBeInTheDocument();
expect(getByText('Service Directory')).toBeInTheDocument();
expect(getByText('Create Incident')).toBeInTheDocument();
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
expect(getByText('Empty escalation policy')).toBeInTheDocument();
});
@@ -141,8 +139,8 @@ describe('PageDutyCard', () => {
),
);
await waitFor(() => !queryByTestId('progress'));
expect(getByText('View in PagerDuty')).toBeInTheDocument();
expect(getByText('Trigger Alarm')).toBeInTheDocument();
expect(getByText('Service Directory')).toBeInTheDocument();
expect(getByText('Create Incident')).toBeInTheDocument();
const triggerButton = getByTestId('trigger-button');
await act(async () => {
fireEvent.click(triggerButton);
@@ -13,36 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { useApi, Progress } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import {
Button,
Card,
CardContent,
CardHeader,
Divider,
makeStyles,
} from '@material-ui/core';
import { Button, makeStyles } from '@material-ui/core';
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 { IconLinkVertical } from '@backstage/plugin-catalog';
import { PagerDutyIcon } from './PagerDutyIcon';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { TriggerDialog } from './TriggerDialog';
import { MissingTokenError } from './MissingTokenError';
import { MissingTokenError } from './Errors/MissingTokenError';
import WebIcon from '@material-ui/icons/Web';
import { AboutCard } from './About/AboutCard';
const useStyles = makeStyles(theme => ({
links: {
margin: theme.spacing(2, 0),
display: 'grid',
gridAutoFlow: 'column',
gridAutoColumns: 'min-content',
gridGap: theme.spacing(3),
},
const useStyles = makeStyles({
triggerAlarm: {
paddingTop: 0,
paddingBottom: 0,
@@ -56,7 +42,7 @@ const useStyles = makeStyles(theme => ({
textDecoration: 'none',
},
},
}));
});
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -76,6 +62,14 @@ export const PagerDutyCard = ({ entity }: Props) => {
PAGERDUTY_INTEGRATION_KEY
];
const handleRefresh = useCallback(() => {
setRefreshIncidents(x => !x);
}, []);
const handleDialog = useCallback(() => {
setShowDialog(x => !x);
}, []);
const { value: service, loading, error } = useAsync(async () => {
const services = await api.getServiceByIntegrationKey(integrationKey);
@@ -87,10 +81,6 @@ export const PagerDutyCard = ({ entity }: Props) => {
};
});
const handleDialog = () => {
setShowDialog(!showDialog);
};
if (error instanceof UnauthorizedError) {
return <MissingTokenError />;
}
@@ -107,13 +97,14 @@ export const PagerDutyCard = ({ entity }: Props) => {
return <Progress />;
}
const pagerdutyLink = {
title: 'View in PagerDuty',
const serviceLink = {
title: 'Service Directory',
href: service!.url,
icon: <WebIcon />,
};
const triggerAlarm = {
title: 'Trigger Alarm',
const triggerLink = {
title: 'Create Incident',
action: (
<Button
data-testid="trigger-button"
@@ -121,49 +112,32 @@ export const PagerDutyCard = ({ entity }: Props) => {
onClick={handleDialog}
className={classes.triggerAlarm}
>
Trigger Alarm
Create Incident
</Button>
),
};
const onTriggerRefresh = () => {
setRefreshIncidents(true);
icon: <AlarmAddIcon onClick={handleDialog} />,
};
return (
<Card>
<CardHeader
title="PagerDuty"
subheader={
<nav className={classes.links}>
<IconLinkVertical
label={pagerdutyLink.title}
href={pagerdutyLink.href}
icon={<PagerDutyIcon viewBox="0 0 100 100" />}
/>
<IconLinkVertical
label={triggerAlarm.title}
icon={<AlarmAddIcon />}
action={triggerAlarm.action}
/>
</nav>
}
/>
<Divider />
<CardContent>
<Incidents
serviceId={service!.id}
refreshIncidents={refreshIncidents}
/>
<EscalationPolicy policyId={service!.policyId} />
<TriggerDialog
showDialog={showDialog}
handleDialog={handleDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
onTriggerRefresh={onTriggerRefresh}
/>
</CardContent>
</Card>
<AboutCard
title="PagerDuty"
links={[serviceLink, triggerLink]}
content={
<>
<Incidents
serviceId={service!.id}
refreshIncidents={refreshIncidents}
/>
<EscalationPolicy policyId={service!.policyId} />
<TriggerDialog
showDialog={showDialog}
handleDialog={handleDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
onIncidentCreated={handleRefresh}
/>
</>
}
/>
);
};
@@ -70,7 +70,7 @@ describe('TriggerDialog', () => {
handleDialog={() => {}}
name={entity.metadata.name}
integrationKey="abc123"
onTriggerRefresh={() => {}}
onIncidentCreated={() => {}}
/>
</ApiProvider>,
),
@@ -35,7 +35,7 @@ type Props = {
integrationKey: string;
showDialog: boolean;
handleDialog: () => void;
onTriggerRefresh: () => void;
onIncidentCreated: () => void;
};
export const TriggerDialog = ({
@@ -43,7 +43,7 @@ export const TriggerDialog = ({
integrationKey,
showDialog,
handleDialog,
onTriggerRefresh,
onIncidentCreated: onIncidentCreated,
}: Props) => {
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
@@ -72,24 +72,20 @@ export const TriggerDialog = ({
alertApi.post({
message: `Alarm successfully triggered by ${userName}`,
});
onTriggerRefresh();
onIncidentCreated();
handleDialog();
}
}, [value, alertApi, handleDialog, userName, onIncidentCreated]);
if (error) {
alertApi.post({
message: `Failed to trigger alarm. ${error.message}`,
severity: 'error',
});
}
}, [value, error, alertApi, handleDialog, userName, onTriggerRefresh]);
if (!showDialog) {
return null;
if (error) {
alertApi.post({
message: `Failed to trigger alarm. ${error.message}`,
severity: 'error',
});
}
return (
<Dialog maxWidth="md" open onClose={handleDialog} fullWidth>
<Dialog maxWidth="md" open={showDialog} onClose={handleDialog} fullWidth>
<DialogTitle>
This action will trigger an incident for <strong>"{name}"</strong>.
</DialogTitle>
@@ -56,3 +56,10 @@ export type User = {
html_url: string;
name: string;
};
export type SubHeaderLink = {
title: string;
href?: string;
icon: React.ReactNode;
action?: React.ReactNode;
};