feat(splunk-on-call-plugin): update style + fetch
Signed-off-by: Remi <remi.d45@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user