update pagerduty after triggering new alarm

This commit is contained in:
Samira Mokaram
2020-11-20 12:03:27 +01:00
parent bc161fba4e
commit d14f125ed1
10 changed files with 229 additions and 198 deletions
@@ -15,21 +15,47 @@
*/
import React from 'react';
import { List, ListSubheader } from '@material-ui/core';
import { User } from '../types';
import { List, ListSubheader, LinearProgress } from '@material-ui/core';
import { EscalationUsersEmptyState } from './EscalationUsersEmptyState';
import { EscalationUser } from './EscalationUser';
import { useAsync } from 'react-use';
import { pagerDutyApiRef } from '../../api';
import { useApi } from '@backstage/core';
import { Alert } from '@material-ui/lab';
type EscalationPolicyProps = {
users: User[];
type Props = {
policyId: string;
};
export const EscalationPolicy = ({ users }: EscalationPolicyProps) => (
<List dense subheader={<ListSubheader>ON CALL</ListSubheader>}>
{users.length ? (
users.map((user, index) => <EscalationUser key={index} user={user} />)
) : (
<EscalationUsersEmptyState />
)}
</List>
);
export const EscalationPolicy = ({ policyId }: Props) => {
const api = useApi(pagerDutyApiRef);
const { value: users, loading, error } = useAsync(async () => {
const oncalls = await api.getOnCallByPolicyId(policyId);
const users = oncalls.map(oncall => oncall.user);
return users;
});
if (error) {
return (
<Alert severity="error">
Error encountered while fetching information. {error.message}
</Alert>
);
}
if (loading) {
return <LinearProgress />;
}
return users!.length ? (
<List dense subheader={<ListSubheader>ON CALL</ListSubheader>}>
{users!.map((user, index) => (
<EscalationUser key={index} user={user} />
))}
</List>
) : (
<EscalationUsersEmptyState />
);
};
@@ -36,8 +36,13 @@ const useStyles = makeStyles({
},
});
export const EscalationUser = ({ user }: { user: User }) => {
type Props = {
user: User;
};
export const EscalationUser = ({ user }: Props) => {
const classes = useStyles();
return (
<ListItem>
<ListItemIcon>
@@ -31,10 +31,6 @@ import moment from 'moment';
import { Incident } from '../types';
import PagerdutyIcon from '../PagerDutyIcon';
type IncidentListItemProps = {
incident: Incident;
};
const useStyles = makeStyles({
denseListIcon: {
marginRight: 0,
@@ -51,9 +47,15 @@ const useStyles = makeStyles({
},
});
export const IncidentListItem = ({ incident }: IncidentListItemProps) => {
type Props = {
incident: Incident;
};
export const IncidentListItem = ({ incident }: Props) => {
const classes = useStyles();
const user = incident.assignments[0].assignee;
const user = incident.assignments[0]?.assignee;
const createdAt = moment(incident.created_at).fromNow();
return (
<ListItem dense key={incident.id}>
<ListItemIcon className={classes.listItemIcon}>
@@ -68,25 +70,28 @@ export const IncidentListItem = ({ incident }: IncidentListItemProps) => {
</Tooltip>
</ListItemIcon>
<ListItemText
primary={
<Typography className={classes.listItemPrimary}>
{incident.title}
</Typography>
}
primary={incident.title}
primaryTypographyProps={{
variant: 'body1',
className: classes.listItemPrimary,
}}
secondary={
<span style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}>
Created {moment(incident.created_at).fromNow()}, assigned to{' '}
{(incident?.assignments[0]?.assignee?.summary && (
<Link href={`mailto:${user.email}`}>{user.summary}</Link>
)) ||
'nobody'}
</span>
<Typography noWrap variant="body2" color="textSecondary">
Created {createdAt} and assigned to{' '}
<Link
href={user?.html_url ?? '#'}
target="_blank"
rel="noopener noreferrer"
>
{user?.summary ?? 'nobody'}
</Link>
</Typography>
}
/>
<ListItemSecondaryAction>
<Tooltip title="View in PagerDuty" placement="top">
<IconButton
href={user.html_url}
href={incident.html_url}
target="_blank"
rel="noopener noreferrer"
color="primary"
@@ -14,27 +14,58 @@
* limitations under the License.
*/
import React from 'react';
import { List, ListSubheader } from '@material-ui/core';
import { Incident } from '../types';
import React, { useEffect } from 'react';
import { List, ListSubheader, LinearProgress } from '@material-ui/core';
import { IncidentListItem } from './IncidentListItem';
import { IncidentsEmptyState } from './IncidentEmptyState';
import { useAsyncFn } from 'react-use';
import { pagerDutyApiRef } from '../../api';
import { useApi } from '@backstage/core';
import { Alert } from '@material-ui/lab';
type IncidentsProps = {
incidents: Incident[];
type Props = {
serviceId: string;
shouldRefreshIncidents: boolean;
setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void;
};
export const Incidents = ({ incidents }: IncidentsProps) => (
<List
dense
subheader={<ListSubheader>{incidents.length && 'INCIDENTS'}</ListSubheader>}
>
{incidents.length ? (
incidents.map((incident, index) => (
export const Incidents = ({
serviceId,
shouldRefreshIncidents,
setShouldRefreshIncidents,
}: Props) => {
const api = useApi(pagerDutyApiRef);
const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
async () => {
setShouldRefreshIncidents(false);
return await api.getIncidentsByServiceId(serviceId);
},
);
useEffect(() => {
getIncidents();
}, [shouldRefreshIncidents, getIncidents]);
if (error) {
return (
<Alert severity="error">
Error encountered while fetching information. {error.message}
</Alert>
);
}
if (loading) {
return <LinearProgress />;
}
return incidents?.length ? (
<List dense subheader={<ListSubheader>INCIDENTS</ListSubheader>}>
{incidents!.map((incident, index) => (
<IncidentListItem key={incident.id + index} incident={incident} />
))
) : (
<IncidentsEmptyState />
)}
</List>
);
))}
</List>
) : (
<IncidentsEmptyState />
);
};
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import React, { useState } from 'react';
import { useApi, EmptyState } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import {
Button,
Card,
CardContent,
CardHeader,
@@ -26,13 +27,13 @@ import {
} from '@material-ui/core';
import { Incidents } from './Incident';
import { EscalationPolicy } from './Escalation';
import { TriggerButton } from './TriggerButton';
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 ReportProblemIcon from '@material-ui/icons/ReportProblem';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import { TriggerDialog } from './TriggerDialog';
const useStyles = makeStyles(theme => ({
links: {
@@ -42,6 +43,19 @@ const useStyles = makeStyles(theme => ({
gridAutoColumns: 'min-content',
gridGap: theme.spacing(3),
},
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 PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
@@ -56,35 +70,45 @@ type Props = {
export const PagerDutyCard = ({ entity }: Props) => {
const classes = useStyles();
const api = useApi(pagerDutyApiRef);
const [showDialog, setShowDialog] = useState<boolean>(false);
const [shouldRefreshIncidents, setShouldRefreshIncidents] = useState<boolean>(
false,
);
const integrationKey = entity.metadata.annotations![
PAGERDUTY_INTEGRATION_KEY
];
const { value, loading, error } = useAsync(async () => {
const integrationKey = entity.metadata.annotations![
PAGERDUTY_INTEGRATION_KEY
];
const { value: service, loading, error } = useAsync(async () => {
const services = await api.getServiceByIntegrationKey(integrationKey);
const incidents = await api.getIncidentsByServiceId(services[0].id);
const oncalls = await api.getOnCallByPolicyId(
services[0].escalation_policy.id,
);
const users = oncalls.map(oncall => oncall.user);
return {
incidents,
users,
id: services[0].id,
name: services[0].name,
homepageUrl: services[0].html_url,
url: services[0].html_url,
policyId: services[0].escalation_policy.id,
};
});
const handleDialog = () => {
setShowDialog(!showDialog);
};
if (error) {
if (error instanceof UnauthorizedError) {
return (
<EmptyState
missing="info"
title="Missing or invalid PagerDuty Token"
description="The request to fetch data need a valid token. See README for more details."
description="The request to fetch data needs a valid token. See README for more details."
action={
<Button
color="primary"
variant="contained"
href="https://github.com/backstage/backstage/blob/master/plugins/pagerduty/README.md"
>
Read More
</Button>
}
/>
);
}
@@ -102,12 +126,21 @@ export const PagerDutyCard = ({ entity }: Props) => {
const pagerdutyLink = {
title: 'View in PagerDuty',
href: value!.homepageUrl,
href: service!.url,
};
const triggerAlarm = {
title: 'Trigger Alarm',
action: <TriggerButton entity={entity} />,
action: (
<Button
data-testid="trigger-button"
color="secondary"
onClick={handleDialog}
className={classes.triggerAlarm}
>
Trigger Alarm
</Button>
),
};
return (
@@ -123,7 +156,7 @@ export const PagerDutyCard = ({ entity }: Props) => {
/>
<IconLinkVertical
label={triggerAlarm.title}
icon={<ReportProblemIcon />}
icon={<AlarmAddIcon />}
action={triggerAlarm.action}
/>
</nav>
@@ -131,8 +164,19 @@ export const PagerDutyCard = ({ entity }: Props) => {
/>
<Divider />
<CardContent>
<Incidents incidents={value!.incidents} />
<EscalationPolicy users={value!.users} />
<Incidents
serviceId={service!.id}
shouldRefreshIncidents={shouldRefreshIncidents}
setShouldRefreshIncidents={setShouldRefreshIncidents}
/>
<EscalationPolicy policyId={service!.policyId} />
<TriggerDialog
showDialog={showDialog}
handleDialog={handleDialog}
name={entity.metadata.name}
integrationKey={integrationKey}
setShouldRefreshIncidents={setShouldRefreshIncidents}
/>
</CardContent>
</Card>
);
@@ -1,71 +0,0 @@
/*
* 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, { useState } from 'react';
import { Button, makeStyles } from '@material-ui/core';
import { TriggerDialog } from '../TriggerDialog';
import { Entity } from '@backstage/catalog-model';
import { PAGERDUTY_INTEGRATION_KEY } from '../PagerDutyCard';
const useStyles = makeStyles({
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',
},
},
});
type Props = {
entity: Entity;
};
export const TriggerButton = ({ entity }: Props) => {
const [showDialog, setShowDialog] = useState<boolean>(false);
const classes = useStyles();
const handleDialog = () => {
setShowDialog(!showDialog);
};
return (
<>
<Button
color="secondary"
onClick={handleDialog}
className={classes.triggerAlarm}
>
Trigger Alarm
</Button>
{showDialog && (
<TriggerDialog
name={entity.metadata.name}
integrationKey={
entity.metadata.annotations![PAGERDUTY_INTEGRATION_KEY]
}
onClose={handleDialog}
/>
)}
</>
);
};
@@ -1,17 +0,0 @@
/*
* 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 { TriggerButton } from './TriggerButton';
@@ -19,67 +19,61 @@ import {
Dialog,
DialogTitle,
TextField,
makeStyles,
DialogActions,
Button,
DialogContent,
Typography,
CircularProgress,
} from '@material-ui/core';
import {
Progress,
useApi,
alertApiRef,
identityApiRef,
WarningPanel,
} from '@backstage/core';
import { useApi, alertApiRef, identityApiRef } from '@backstage/core';
import { useAsyncFn } from 'react-use';
import { pagerDutyApiRef } from '../../api';
import { Alert, AlertTitle } from '@material-ui/lab';
const useStyles = makeStyles({
warningText: {
border: '1px solid rgba(245, 155, 35, 0.5)',
backgroundColor: 'rgba(245, 155, 35, 0.2)',
padding: '0.5em 1em',
},
});
import { Alert } from '@material-ui/lab';
type Props = {
name: string;
integrationKey: string;
onClose: () => void;
showDialog: boolean;
handleDialog: () => void;
setShouldRefreshIncidents: (shouldRefreshIncidents: boolean) => void;
};
export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
const classes = useStyles();
const [description, setDescription] = useState<string>('');
export const TriggerDialog = ({
name,
integrationKey,
showDialog,
handleDialog,
setShouldRefreshIncidents,
}: Props) => {
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userId = identityApi.getUserId();
const api = useApi(pagerDutyApiRef);
const descriptionChanged = (event: any) => {
setDescription(event.target.value);
};
const [description, setDescription] = useState<string>('');
const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn(
async (desc: string) => {
return await api.triggerAlarm(
async (desc: string) =>
await api.triggerAlarm(
integrationKey,
window.location.toString(),
desc,
userId,
);
},
),
);
const descriptionChanged = (
event: React.ChangeEvent<HTMLTextAreaElement>,
) => {
setDescription(event.target.value);
};
useEffect(() => {
if (value) {
alertApi.post({
message: `Alarm successfully triggered by ${userId}`,
});
onClose();
setShouldRefreshIncidents(true);
handleDialog();
}
if (error) {
@@ -88,16 +82,19 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
severity: 'error',
});
}
}, [value, error, alertApi, onClose, userId]);
}, [value, error, alertApi, handleDialog, userId, setShouldRefreshIncidents]);
if (!showDialog) {
return null;
}
return (
<Dialog maxWidth="md" open onClose={onClose} fullWidth>
<Dialog maxWidth="md" open onClose={handleDialog} fullWidth>
<DialogTitle>
This action will trigger an incident for <strong>"{name}"</strong>.
</DialogTitle>
<DialogContent>
<Alert severity="info">
{/* <AlertTitle>"Note"</AlertTitle> */}
<Typography variant="body1" align="justify">
If the issue you are seeing does not need urgent attention, please
get in touch with the responsible team using their preferred
@@ -141,7 +138,7 @@ export const TriggerDialog = ({ name, integrationKey, onClose }: Props) => {
>
Trigger Incident
</Button>
<Button id="close" color="primary" onClick={onClose}>
<Button id="close" color="primary" onClick={handleDialog}>
Close
</Button>
</DialogActions>
+8 -2
View File
@@ -18,10 +18,10 @@ export type Incident = {
id: string;
title: string;
status: string;
homepageUrl: string;
html_url: string;
assignments: [
{
assignee: User;
assignee: Assignee;
},
];
serviceId: string;
@@ -43,6 +43,12 @@ export type OnCall = {
user: User;
};
export type Assignee = {
id: string;
summary: string;
html_url: string;
};
export type User = {
id: string;
summary: string;
+6 -1
View File
@@ -17,4 +17,9 @@ export { plugin } from './plugin';
export {
isPluginApplicableToEntity,
PagerDutyCard,
} from './components/PagerDutyCard';
} from './components/PagerdutyCard';
export {
PagerDutyClientApi,
pagerDutyApiRef,
UnauthorizedError,
} from './api/client';