feat(splunk-on-call-plugin): add Incident
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user