feat(splunk-on-call-plugin): update style + fetch

Signed-off-by: Remi <remi.d45@gmail.com>
This commit is contained in:
Remi
2021-02-26 12:32:00 +01:00
parent 14af7ac400
commit 274b00ed52
7 changed files with 138 additions and 94 deletions
+2 -6
View File
@@ -48,22 +48,18 @@ import {
## Client configuration
In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action.
The user supplied must be a valid Splunk On-Call user and a member of your organization.
In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint.
In addition, you also need to enable the REST Endpoint integration on https://portal.victorops.com/ by going to Integrations > 3rd Party Integrations > REST Generic.
To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST Generic.
You can now copy the URL to notify: `<SPLUNK_ON_CALL_REST_ENDPOINT>/$routing_key`
In `app-config.yaml`:
```yaml
splunkOnCall:
username: <SPLUNK_ON_CALL_USERNAME>
eventsRestEndpoint: <SPLUNK_ON_CALL_REST_ENDPOINT>
```
The user supplied must be a valid Splunk On-Call user and a member of your organization.
In order to make the API calls, you need to provide a new proxy config which will redirect to the Splunk On-Call API endpoint and add authentication information in the headers:
```yaml
-3
View File
@@ -42,12 +42,9 @@ export const splunkOnCallApiRef = createApiRef<SplunkOnCallApi>({
export class SplunkOnCallClient implements SplunkOnCallApi {
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
const usernameFromConfig: string | null =
configApi.getOptionalString('splunkOnCall.username') || null;
const eventsRestEndpoint: string | null =
configApi.getOptionalString('splunkOnCall.eventsRestEndpoint') || null;
return new SplunkOnCallClient({
username: usernameFromConfig,
eventsRestEndpoint,
discoveryApi,
});
-1
View File
@@ -89,7 +89,6 @@ export type OnCallsResponse = {
};
export type ClientApiConfig = {
username: string | null;
eventsRestEndpoint: string | null;
discoveryApi: DiscoveryApi;
};
@@ -15,7 +15,13 @@
*/
import React from 'react';
import { List, ListSubheader } from '@material-ui/core';
import {
createStyles,
List,
ListSubheader,
makeStyles,
Theme,
} from '@material-ui/core';
import { EscalationUsersEmptyState } from './EscalationUsersEmptyState';
import { EscalationUser } from './EscalationUser';
import { useAsync } from 'react-use';
@@ -24,12 +30,28 @@ import { useApi, Progress } from '@backstage/core';
import { Alert } from '@material-ui/lab';
import { User } from '../types';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
maxHeight: '400px',
overflow: 'auto',
},
subheader: {
backgroundColor: 'white',
},
progress: {
margin: `0 ${theme.spacing(2)}px`,
},
}),
);
type Props = {
users: { [key: string]: User };
team: string;
};
export const EscalationPolicy = ({ users, team }: Props) => {
const classes = useStyles();
const api = useApi(splunkOnCallApiRef);
const { value: userNames, loading, error } = useAsync(async () => {
@@ -54,24 +76,30 @@ export const EscalationPolicy = ({ users, team }: Props) => {
);
}
if (loading) {
return <Progress />;
}
if (!userNames?.length) {
if (!loading && !userNames?.length) {
return <EscalationUsersEmptyState />;
}
return (
<List dense subheader={<ListSubheader>ON CALL</ListSubheader>}>
{userNames &&
<List
className={classes.root}
dense
subheader={
<ListSubheader className={classes.subheader}>ON CALL</ListSubheader>
}
>
{loading ? (
<Progress className={classes.progress} />
) : (
userNames &&
userNames.map(
(userName, index) =>
userName &&
userName in users && (
<EscalationUser key={index} user={users[userName]} />
),
)}
)
)}
</List>
);
};
@@ -15,7 +15,13 @@
*/
import React, { useEffect } from 'react';
import { List, ListSubheader } from '@material-ui/core';
import {
createStyles,
List,
ListSubheader,
makeStyles,
Theme,
} from '@material-ui/core';
import { IncidentListItem } from './IncidentListItem';
import { IncidentsEmptyState } from './IncidentEmptyState';
import { useAsyncFn } from 'react-use';
@@ -23,12 +29,28 @@ import { splunkOnCallApiRef } from '../../api';
import { useApi, Progress } from '@backstage/core';
import { Alert } from '@material-ui/lab';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
maxHeight: '400px',
overflow: 'auto',
},
subheader: {
backgroundColor: 'white',
},
progress: {
margin: `0 ${theme.spacing(2)}px`,
},
}),
);
type Props = {
refreshIncidents: boolean;
team: string;
};
export const Incidents = ({ refreshIncidents, team }: Props) => {
const classes = useStyles();
const api = useApi(splunkOnCallApiRef);
const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn(
@@ -61,24 +83,32 @@ export const Incidents = ({ refreshIncidents, team }: Props) => {
);
}
if (loading) {
return <Progress />;
}
if (!incidents?.length) {
if (!loading && !incidents?.length) {
return <IncidentsEmptyState />;
}
return (
<List dense subheader={<ListSubheader>INCIDENTS</ListSubheader>}>
{incidents!.map((incident, index) => (
<IncidentListItem
onIncidentAction={() => getIncidents()}
key={index}
team={team}
incident={incident}
/>
))}
<List
className={classes.root}
dense
subheader={
<ListSubheader className={classes.subheader}>
CRITICAL INCIDENTS
</ListSubheader>
}
>
{loading ? (
<Progress className={classes.progress} />
) : (
incidents!.map((incident, index) => (
<IncidentListItem
onIncidentAction={() => getIncidents()}
key={index}
team={team}
incident={incident}
/>
))
)}
</List>
);
};
@@ -34,6 +34,7 @@ import {
} from '@material-ui/core';
import { Incidents } from './Incident';
import { EscalationPolicy } from './Escalation';
import WebIcon from '@material-ui/icons/Web';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { splunkOnCallApiRef, UnauthorizedError } from '../api';
@@ -64,16 +65,6 @@ 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 valid username to your 'app-config.yaml' if you want to enable Splunk On-Call. Make sure that the user is a member of your organization."
/>
</CardContent>
);
export const MissingEventsRestEndpoint = () => (
<CardContent>
<EmptyState
@@ -99,8 +90,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM];
const username = config.getOptionalString('splunkOnCall.username') || null;
const eventsRestEndpoint =
config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null;
@@ -126,9 +115,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
return { usersHashMap, userList: allUsers };
});
const incidentCreator =
username && users?.userList.find(user => user.username === username);
if (error instanceof UnauthorizedError) {
return <MissingApiKeyOrApiIdError />;
}
@@ -149,9 +135,6 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
if (!team) {
return <MissingTeamAnnotation />;
}
if (!username || !incidentCreator) {
return <MissingUsername />;
}
if (!eventsRestEndpoint) {
return <MissingEventsRestEndpoint />;
@@ -163,15 +146,12 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
{users?.usersHashMap && team && (
<EscalationPolicy team={team} users={users.usersHashMap} />
)}
{incidentCreator && (
<TriggerDialog
team={team}
incidentCreator={incidentCreator}
showDialog={showDialog}
handleDialog={handleDialog}
onIncidentCreated={handleRefresh}
/>
)}
<TriggerDialog
team={team}
showDialog={showDialog}
handleDialog={handleDialog}
onIncidentCreated={handleRefresh}
/>
</>
);
};
@@ -191,15 +171,22 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
icon: <AlarmAddIcon onClick={handleDialog} />,
};
const serviceLink = {
label: 'Portal',
href: 'https://portal.victorops.com/',
icon: <WebIcon />,
};
return (
<Card>
<CardHeader
title="Splunk On-Call"
subheader={[
<Typography key="team_name">Team: {team}</Typography>,
username && (
<HeaderIconLinkRow key="incident_trigger" links={[triggerLink]} />
),
<HeaderIconLinkRow
key="incident_trigger"
links={[serviceLink, triggerLink]}
/>,
]}
/>
<Divider />
@@ -36,12 +36,10 @@ import { useApi, alertApiRef } from '@backstage/core';
import { useAsyncFn } from 'react-use';
import { splunkOnCallApiRef } from '../../api';
import { Alert } from '@material-ui/lab';
import { User } from '../types';
import { TriggerAlarmRequest } from '../../api/types';
type Props = {
team: string;
incidentCreator: User;
showDialog: boolean;
handleDialog: () => void;
onIncidentCreated: () => void;
@@ -58,8 +56,17 @@ const useStyles = makeStyles((theme: Theme) =>
},
formControl: {
margin: theme.spacing(1),
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
minWidth: `calc(100% - ${theme.spacing(2)}px)`,
},
formHeader: {
width: '50%',
},
incidentType: {
width: '90%',
},
targets: {
display: 'flex',
flexDirection: 'column',
@@ -70,7 +77,6 @@ const useStyles = makeStyles((theme: Theme) =>
export const TriggerDialog = ({
team,
incidentCreator,
showDialog,
handleDialog,
onIncidentCreated: onIncidentCreated,
@@ -142,10 +148,7 @@ export const TriggerDialog = ({
<DialogTitle>This action will trigger an incident</DialogTitle>
<DialogContent>
<Typography variant="subtitle1" gutterBottom align="justify">
Created by:{' '}
<b>
{incidentCreator?.firstName} {incidentCreator?.lastName}
</b>
Created by: <b>{`{ REST } Endpoint`}</b>
</Typography>
<Alert severity="info">
<Typography variant="body1" align="justify">
@@ -163,22 +166,35 @@ export const TriggerDialog = ({
align="justify"
>
Please describe the problem you want to report. Be as descriptive as
possible. Your signed in user and a reference to the current page will
automatically be amended to the alarm so that the receiver can reach
out to you if necessary.
possible. <br />
Note that only the <b>Incident type</b>, <b>Incident display name</b>{' '}
and the <b>Incident message</b> fields are <b>required</b>.
</Typography>
<FormControl className={classes.formControl}>
<InputLabel id="demo-simple-select-label">Incident type</InputLabel>
<Select
id="incident-type"
value={incidentType}
onChange={handleIncidentType}
inputProps={{ 'data-testid': 'trigger-incident-type' }}
>
<MenuItem value="CRITICAL">Critical</MenuItem>
<MenuItem value="WARNING">Warning</MenuItem>
<MenuItem value="INFO">Info</MenuItem>
</Select>
<div className={classes.formHeader}>
<InputLabel id="demo-simple-select-label">Incident type</InputLabel>
<Select
id="incident-type"
className={classes.incidentType}
value={incidentType}
onChange={handleIncidentType}
inputProps={{ 'data-testid': 'trigger-incident-type' }}
>
<MenuItem value="CRITICAL">Critical</MenuItem>
<MenuItem value="WARNING">Warning</MenuItem>
<MenuItem value="INFO">Info</MenuItem>
</Select>
</div>
<TextField
className={classes.formHeader}
id="datetime-local"
label="Incident start time"
type="datetime-local"
onChange={handleIncidentStartTime}
InputLabelProps={{
shrink: true,
}}
/>
</FormControl>
<TextField
inputProps={{ 'data-testid': 'trigger-incident-id' }}
@@ -211,15 +227,6 @@ export const TriggerDialog = ({
variant="outlined"
onChange={handleIncidentMessage}
/>
<TextField
id="datetime-local"
label="Incident start time"
type="datetime-local"
onChange={handleIncidentStartTime}
InputLabelProps={{
shrink: true,
}}
/>
</DialogContent>
<DialogActions>
<Button