feat(splunk-on-call-plugin): edge cases

This commit is contained in:
Remi
2021-02-07 15:02:21 +01:00
parent f952bac577
commit 3f2f242479
8 changed files with 75 additions and 56 deletions
+1 -1
View File
@@ -394,5 +394,5 @@ homepage:
timezone: 'Asia/Tokyo'
pagerduty:
eventsBaseUrl: 'https://events.pagerduty.com/v2'
splunkoncall:
splunkOnCall:
username: 'guest'
+1 -1
View File
@@ -54,7 +54,7 @@ The user supplied must be a valid Splunk On-Call user and a member of your organ
In `app-config.yaml`:
```yaml
splunkoncall:
splunkOnCall:
username: <SPLUNK_ON_CALL_USERNAME>
```
+3 -9
View File
@@ -44,7 +44,7 @@ export const splunkOnCallApiRef = createApiRef<SplunkOnCallApi>({
export class SplunkOnCallClient implements SplunkOnCallApi {
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
const usernameFromConfig: string | null =
configApi.getOptionalString('splunkoncall.username') || 'ayshiff';
configApi.getOptionalString('splunkoncall.username') || null;
return new SplunkOnCallClient({
username: usernameFromConfig,
discoveryApi,
@@ -52,10 +52,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
}
constructor(private readonly config: ClientApiConfig) {}
getUsername(): string | undefined {
return this.config.username as string;
}
async getIncidents(): Promise<Incident[]> {
const url = `${await this.config.discoveryApi.getBaseUrl(
'proxy',
@@ -85,7 +81,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
}
async acknowledgeIncident({
userName,
incidentNames,
}: PatchIncidentRequest): Promise<Response> {
const options = {
@@ -95,7 +90,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userName: this.config.username || userName,
userName: this.config.username,
incidentNames,
}),
};
@@ -108,7 +103,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
}
async resolveIncident({
userName,
incidentNames,
}: PatchIncidentRequest): Promise<Response> {
const options = {
@@ -118,7 +112,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userName: this.config.username || userName,
userName: this.config.username,
incidentNames,
}),
};
-6
View File
@@ -83,15 +83,9 @@ export interface SplunkOnCallApi {
* Get a list of escalation policies for your organization.
*/
getEscalationPolicies(): Promise<EscalationPolicyInfo[]>;
/**
* Get the current user username.
*/
getUsername(): string | undefined;
}
export type PatchIncidentRequest = {
userName: string;
incidentNames: string[];
message?: string;
};
@@ -94,13 +94,11 @@ const incidentPhaseTooltip = (currentPhase: IncidentPhase) => {
const IncidentAction = ({
currentPhase,
userName,
incidentNames,
resolveAction,
aknowledgeAction,
}: {
currentPhase: string;
userName: string;
incidentNames: string[];
resolveAction: (args: PatchIncidentRequest) => void;
aknowledgeAction: (args: PatchIncidentRequest) => void;
@@ -109,9 +107,7 @@ const IncidentAction = ({
case 'UNACKED':
return (
<Tooltip title="Aknowledge" placement="top">
<IconButton
onClick={() => aknowledgeAction({ userName, incidentNames })}
>
<IconButton onClick={() => aknowledgeAction({ incidentNames })}>
<DoneIcon />
</IconButton>
</Tooltip>
@@ -119,9 +115,7 @@ const IncidentAction = ({
case 'ACKED':
return (
<Tooltip title="Resolve" placement="top">
<IconButton
onClick={() => resolveAction({ userName, incidentNames })}
>
<IconButton onClick={() => resolveAction({ incidentNames })}>
<DoneAllIcon />
</IconButton>
</Tooltip>
@@ -135,8 +129,6 @@ 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-');
@@ -149,9 +141,8 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
{ value: resolveValue, loading: _resolveLoading, error: resolveError },
handleResolveIncident,
] = useAsyncFn(
async ({ userName, incidentNames }: PatchIncidentRequest) =>
async ({ incidentNames }: PatchIncidentRequest) =>
await api.resolveIncident({
userName,
incidentNames,
}),
);
@@ -164,9 +155,8 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
},
handleAcknowledgeIncident,
] = useAsyncFn(
async ({ userName, incidentNames }: PatchIncidentRequest) =>
async ({ incidentNames }: PatchIncidentRequest) =>
await api.acknowledgeIncident({
userName,
incidentNames,
}),
);
@@ -230,7 +220,6 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
{incident.incidentLink && incident.incidentNumber && (
<ListItemSecondaryAction>
<IncidentAction
userName={userName}
currentPhase={incident.currentPhase || ''}
incidentNames={[incident.incidentNumber]}
resolveAction={handleResolveIncident}
@@ -59,7 +59,7 @@ const entity: Entity = {
metadata: {
name: 'splunkoncall-test',
annotations: {
'splunkoncall.com/integration-key': 'abc123',
'splunk-on-call.com/team': 'Example',
},
},
};
@@ -14,7 +14,14 @@
* limitations under the License.
*/
import React, { useState, useCallback } from 'react';
import { useApi, Progress, HeaderIconLinkRow } from '@backstage/core';
import {
useApi,
Progress,
HeaderIconLinkRow,
MissingAnnotationEmptyState,
configApiRef,
EmptyState,
} from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import {
Button,
@@ -53,6 +60,20 @@ const useStyles = makeStyles({
export const SPLUNK_ON_CALL_TEAM = 'splunk-on-call.com/team';
export const MissingTeamAnnotation = () => (
<MissingAnnotationEmptyState annotation={SPLUNK_ON_CALL_TEAM} />
);
export const MissingUsername = () => (
<CardContent>
<EmptyState
title="No Splunk On-Call user available."
missing="info"
description="You need to add a username to your 'app.config.yml' if you want to enable Splunk On-Call."
/>
</CardContent>
);
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]);
@@ -62,11 +83,14 @@ type Props = {
export const SplunkOnCallCard = ({ entity }: Props) => {
const classes = useStyles();
const config = useApi(configApiRef);
const api = useApi(splunkOnCallApiRef);
const [showDialog, setShowDialog] = useState<boolean>(false);
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM];
const username = config.getOptionalString('splunkOnCall.username');
const handleRefresh = useCallback(() => {
setRefreshIncidents(x => !x);
}, []);
@@ -89,6 +113,9 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
return { usersHashMap, userList: users };
});
const incidentCreator =
username && users?.userList.find(user => user.username === username);
if (error instanceof UnauthorizedError) {
return <MissingTokenError />;
}
@@ -105,6 +132,33 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
return <Progress />;
}
const Content = () => {
if (!team) {
return <MissingTeamAnnotation />;
}
if (!username) {
return <MissingUsername />;
}
return (
<>
<Incidents team={team} refreshIncidents={refreshIncidents} />
{users?.usersHashMap && team && (
<EscalationPolicy team={team} users={users.usersHashMap} />
)}
{users && incidentCreator && (
<TriggerDialog
users={users.userList}
incidentCreator={incidentCreator}
showDialog={showDialog}
handleDialog={handleDialog}
onIncidentCreated={handleRefresh}
/>
)}
</>
);
};
const triggerLink = {
label: 'Create Incident',
action: (
@@ -126,23 +180,14 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
title="Splunk On-Call"
subheader={[
<Typography key="team_name">Team: {team}</Typography>,
<HeaderIconLinkRow key="incident_trigger" links={[triggerLink]} />,
username && (
<HeaderIconLinkRow key="incident_trigger" links={[triggerLink]} />
),
]}
/>
<Divider />
<CardContent>
<Incidents team={team} refreshIncidents={refreshIncidents} />
{users?.usersHashMap && (
<EscalationPolicy team={team} users={users.usersHashMap} />
)}
{users && (
<TriggerDialog
users={users.userList}
showDialog={showDialog}
handleDialog={handleDialog}
onIncidentCreated={handleRefresh}
/>
)}
<Content />
</CardContent>
</Card>
);
@@ -34,8 +34,6 @@ import {
FormControl,
InputLabel,
} from '@material-ui/core';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { useApi, alertApiRef, identityApiRef } from '@backstage/core';
import { useAsync, useAsyncFn } from 'react-use';
import { splunkOnCallApiRef } from '../../api';
@@ -52,8 +50,8 @@ const MenuProps = {
};
type Props = {
// name: string;
users: User[];
incidentCreator: User;
showDialog: boolean;
handleDialog: () => void;
onIncidentCreated: () => void;
@@ -82,6 +80,7 @@ const useStyles = makeStyles((theme: Theme) =>
export const TriggerDialog = ({
users,
incidentCreator,
showDialog,
handleDialog,
onIncidentCreated: onIncidentCreated,
@@ -118,10 +117,6 @@ export const TriggerDialog = ({
}),
);
const username = api.getUsername();
const currentUser = users.find(user => user.username === username);
const {
value: policies,
loading: policiesLoaading,
@@ -187,7 +182,7 @@ export const TriggerDialog = ({
<Typography variant="subtitle1" gutterBottom align="justify">
Created by:{' '}
<b>
{currentUser?.firstName} {currentUser?.lastName}
{incidentCreator?.firstName} {incidentCreator?.lastName}
</b>
</Typography>
<Alert severity="info">
@@ -257,7 +252,9 @@ export const TriggerDialog = ({
value={policyTargets}
onChange={handlePolicyTargets}
input={<Input />}
inputProps={{ 'data-testid': 'trigger-select-policies-target' }}
inputProps={{
'data-testid': 'trigger-select-policies-target',
}}
renderValue={selected => (
<div className={classes.chips}>
{(selected as string[]).map(value => {