feat(splunk-on-call-plugin): add Incident

This commit is contained in:
Remi
2021-02-04 23:42:27 +01:00
parent 3e58185346
commit 71c4582783
4 changed files with 363 additions and 0 deletions
@@ -0,0 +1,36 @@
/*
* 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 { Grid, Typography } from '@material-ui/core';
import EmptyStateImage from '../../assets/emptystate.svg';
export const IncidentsEmptyState = () => {
return (
<Grid container justify="center" direction="column" alignItems="center">
<Grid item xs={12}>
<Typography variant="h5">Nice! No incidents found!</Typography>
</Grid>
<Grid item xs={12}>
<img
src={EmptyStateImage}
alt="EmptyState"
data-testid="emptyStateImg"
/>
</Grid>
</Grid>
);
};
@@ -0,0 +1,242 @@
/*
* 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, { useEffect } from 'react';
import {
ListItem,
ListItemIcon,
ListItemSecondaryAction,
Tooltip,
ListItemText,
makeStyles,
IconButton,
Typography,
withStyles,
} from '@material-ui/core';
import DoneIcon from '@material-ui/icons/Done';
import DoneAllIcon from '@material-ui/icons/DoneAll';
import {
StatusError,
StatusWarning,
StatusOK,
identityApiRef,
useApi,
alertApiRef,
} from '@backstage/core';
import { formatDistanceToNowStrict } from 'date-fns';
import { Incident } from '../types';
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
import { splunkOnCallApiRef } from '../../api/client';
import { useAsyncFn } from 'react-use';
import { PatchIncidentRequest } from '../../api/types';
const useStyles = makeStyles({
denseListIcon: {
marginRight: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
listItemPrimary: {
fontWeight: 'bold',
},
listItemIcon: {
minWidth: '1em',
},
secondaryAction: {
paddingRight: 48,
},
});
const ListItemWithWiderSecondaryAction = withStyles({
secondaryAction: {
paddingRight: 96,
},
})(ListItem);
type Props = {
incident: Incident;
onIncidentAction: () => void;
};
const IncidentPhase = ({ currentPhase }: { currentPhase: string }) => {
switch (currentPhase) {
case 'UNACKED':
return <StatusError />;
case 'ACKED':
return <StatusWarning />;
default:
return <StatusOK />;
}
};
const IncidentAction = ({
currentPhase,
userName,
incidentNames,
resolveAction,
aknowledgeAction,
}: {
currentPhase: string;
userName: string;
incidentNames: string[];
resolveAction: (args: PatchIncidentRequest) => void;
aknowledgeAction: (args: PatchIncidentRequest) => void;
}) => {
switch (currentPhase) {
case 'UNACKED':
return (
<Tooltip title="Aknowledge" placement="top">
<IconButton
onClick={() => aknowledgeAction({ userName, incidentNames })}
>
<DoneIcon />
</IconButton>
</Tooltip>
);
case 'ACKED':
return (
<Tooltip title="Resolve" placement="top">
<IconButton
onClick={() => resolveAction({ userName, incidentNames })}
>
<DoneAllIcon />
</IconButton>
</Tooltip>
);
default:
return <></>;
}
};
export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
const classes = useStyles();
const createdAt = formatDistanceToNowStrict(new Date(incident.startTime!));
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const userName = identityApi.getUserId();
const api = useApi(splunkOnCallApiRef);
const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-');
const user = hasBeenManuallyTriggered
? incident.monitorName?.replace('vouser-', '')
: incident.monitorName;
const [
{ value: resolveValue, loading: _resolveLoading, error: resolveError },
handleResolveIncident,
] = useAsyncFn(
async ({ userName, incidentNames }: PatchIncidentRequest) =>
await api.resolveIncident({
userName,
incidentNames,
}),
);
const [
{
value: acknowledgeValue,
loading: _acknowledgeLoading,
error: acknowledgeError,
},
handleAcknowledgeIncident,
] = useAsyncFn(
async ({ userName, incidentNames }: PatchIncidentRequest) =>
await api.acknowledgeIncident({
userName,
incidentNames,
}),
);
useEffect(() => {
if (acknowledgeValue) {
alertApi.post({
message: `Incident successfully acknowledged`,
});
}
if (resolveValue) {
alertApi.post({
message: `Incident successfully resolved`,
});
}
if (resolveValue || acknowledgeValue) {
onIncidentAction();
}
}, [acknowledgeValue, resolveValue, alertApi, onIncidentAction]);
if (acknowledgeError) {
alertApi.post({
message: `Failed to acknowledge incident. ${acknowledgeError.message}`,
severity: 'error',
});
}
if (resolveError) {
alertApi.post({
message: `Failed to resolve incident. ${resolveError.message}`,
severity: 'error',
});
}
return (
<ListItem dense key={incident.entityId}>
<ListItemIcon className={classes.listItemIcon}>
<Tooltip title={incident.currentPhase || ''} placement="top">
<div className={classes.denseListIcon}>
<IncidentPhase currentPhase={incident.currentPhase || ''} />
</div>
</Tooltip>
</ListItemIcon>
<ListItemText
primary={incident.entityDisplayName}
primaryTypographyProps={{
variant: 'body1',
className: classes.listItemPrimary,
}}
secondary={
<Typography noWrap variant="body2" color="textSecondary">
Created {createdAt} ago {user && `by ${user}`}
</Typography>
}
/>
{incident.incidentLink && incident.incidentNumber && (
<ListItemSecondaryAction>
<IncidentAction
userName={userName}
currentPhase={incident.currentPhase || ''}
incidentNames={[incident.incidentNumber]}
resolveAction={handleResolveIncident}
aknowledgeAction={handleAcknowledgeIncident}
/>
<Tooltip title="View in SplunkOnCall" placement="top">
<IconButton
href={incident.incidentLink}
target="_blank"
rel="noopener noreferrer"
color="primary"
>
<OpenInBrowserIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
)}
</ListItem>
);
};
@@ -0,0 +1,68 @@
/*
* 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, { useEffect } from 'react';
import { List, ListSubheader } from '@material-ui/core';
import { IncidentListItem } from './IncidentListItem';
import { IncidentsEmptyState } from './IncidentEmptyState';
import { useAsyncFn } from 'react-use';
import { splunkOnCallApiRef } from '../../api';
import { useApi, Progress } from '@backstage/core';
import { Alert } from '@material-ui/lab';
type Props = {
refreshIncidents: boolean;
};
export const Incidents = ({ refreshIncidents }: Props) => {
const api = useApi(splunkOnCallApiRef);
const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
async () => await api.getIncidents(),
);
useEffect(() => {
getIncidents();
}, [refreshIncidents, getIncidents]);
if (error) {
return (
<Alert severity="error">
Error encountered while fetching information. {error.message}
</Alert>
);
}
if (loading) {
return <Progress />;
}
if (!incidents?.length) {
return <IncidentsEmptyState />;
}
return (
<List dense subheader={<ListSubheader>INCIDENTS</ListSubheader>}>
{incidents!.map((incident, index) => (
<IncidentListItem
onIncidentAction={() => getIncidents()}
key={index}
incident={incident}
/>
))}
</List>
);
};
@@ -0,0 +1,17 @@
/*
* 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 { Incidents } from './Incidents';