Merge pull request #4719 from ayshiff/feature/splunk-on-call-plugin-rest-endpoint
Splunk On-Call plugin: REST endpoint
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-splunk-on-call': minor
|
||||
---
|
||||
|
||||
Updated splunk-on-call plugin to use the REST endpoint (incident creation-acknowledgement-resolution).
|
||||
It implies switching from `splunkOnCall.username` configuration to `splunkOnCall.eventsRestEndpoint` configuration, this is a breaking change.
|
||||
@@ -48,18 +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.
|
||||
|
||||
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
|
||||
@@ -87,6 +87,14 @@ annotations:
|
||||
splunk.com/on-call-team': <SPLUNK_ON_CALL_TEAM_NAME>
|
||||
```
|
||||
|
||||
### Create the Routing Key
|
||||
|
||||
To be able to use the REST Endpoint seen above, you must have created a routing key with the **same name** as the provided team.
|
||||
|
||||
You can create a new routing key on https://portal.victorops.com/ by going to Settings > Routing Keys.
|
||||
|
||||
You can read [Create & Manage Alert Routing Keys](https://help.victorops.com/knowledge-base/routing-keys/#routing-key-tips-tricks) for further information.
|
||||
|
||||
## Providing the API key and API id
|
||||
|
||||
In order for the client to make requests to the [Splunk On-Call API](https://portal.victorops.com/public/api-docs.html#/) it needs an [API ID and an API Key](https://help.victorops.com/knowledge-base/api/).
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
RequestOptions,
|
||||
ListUserResponse,
|
||||
EscalationPolicyResponse,
|
||||
PatchIncidentRequest,
|
||||
} from './types';
|
||||
|
||||
export class UnauthorizedError extends Error {}
|
||||
@@ -43,10 +42,10 @@ 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,
|
||||
});
|
||||
}
|
||||
@@ -80,50 +79,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
|
||||
return teams;
|
||||
}
|
||||
|
||||
async acknowledgeIncident({
|
||||
incidentNames,
|
||||
}: PatchIncidentRequest): Promise<Response> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userName: this.config.username,
|
||||
incidentNames,
|
||||
}),
|
||||
};
|
||||
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
)}/splunk-on-call/v1/incidents/ack`;
|
||||
|
||||
return this.request(url, options);
|
||||
}
|
||||
|
||||
async resolveIncident({
|
||||
incidentNames,
|
||||
}: PatchIncidentRequest): Promise<Response> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userName: this.config.username,
|
||||
incidentNames,
|
||||
}),
|
||||
};
|
||||
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
)}/splunk-on-call/v1/incidents/resolve`;
|
||||
|
||||
return this.request(url, options);
|
||||
}
|
||||
|
||||
async getUsers(): Promise<User[]> {
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
@@ -142,19 +97,22 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
|
||||
return policies;
|
||||
}
|
||||
|
||||
async triggerAlarm({
|
||||
summary,
|
||||
details,
|
||||
userName,
|
||||
targets,
|
||||
isMultiResponder,
|
||||
async incidentAction({
|
||||
routingKey,
|
||||
incidentType,
|
||||
incidentId,
|
||||
incidentDisplayName,
|
||||
incidentMessage,
|
||||
incidentStartTime,
|
||||
}: TriggerAlarmRequest): Promise<Response> {
|
||||
const body = JSON.stringify({
|
||||
summary,
|
||||
details,
|
||||
userName: this.config.username || userName,
|
||||
targets,
|
||||
isMultiResponder,
|
||||
message_type: incidentType,
|
||||
...(incidentId ? { entity_id: incidentId } : {}),
|
||||
...(incidentDisplayName
|
||||
? { entity_display_name: incidentDisplayName }
|
||||
: {}),
|
||||
...(incidentMessage ? { state_message: incidentMessage } : {}),
|
||||
...(incidentStartTime ? { state_start_time: incidentStartTime } : {}),
|
||||
});
|
||||
|
||||
const options = {
|
||||
@@ -166,9 +124,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi {
|
||||
body,
|
||||
};
|
||||
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
)}/splunk-on-call/v1/incidents`;
|
||||
const url = `${this.config.eventsRestEndpoint}/${routingKey}`;
|
||||
|
||||
return this.request(url, options);
|
||||
}
|
||||
|
||||
@@ -23,22 +23,20 @@ import {
|
||||
} from '../components/types';
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
|
||||
export enum TargetType {
|
||||
UserValue = 'User',
|
||||
EscalationPolicyValue = 'EscalationPolicy',
|
||||
}
|
||||
|
||||
export type IncidentTarget = {
|
||||
type: TargetType;
|
||||
slug: string;
|
||||
};
|
||||
export type MessageType =
|
||||
| 'CRITICAL'
|
||||
| 'WARNING'
|
||||
| 'ACKNOWLEDGEMENT'
|
||||
| 'INFO'
|
||||
| 'RECOVERY';
|
||||
|
||||
export type TriggerAlarmRequest = {
|
||||
targets: IncidentTarget[];
|
||||
details: string;
|
||||
summary: string;
|
||||
userName: string;
|
||||
isMultiResponder?: boolean;
|
||||
routingKey?: string;
|
||||
incidentType: MessageType;
|
||||
incidentId?: string;
|
||||
incidentDisplayName?: string;
|
||||
incidentMessage?: string;
|
||||
incidentStartTime?: number;
|
||||
};
|
||||
|
||||
export interface SplunkOnCallApi {
|
||||
@@ -53,19 +51,9 @@ export interface SplunkOnCallApi {
|
||||
getOnCallUsers(): Promise<OnCall[]>;
|
||||
|
||||
/**
|
||||
* Triggers an incident to specific users and/or specific teams.
|
||||
* Triggers-Resolves-Acknowledge an incident.
|
||||
*/
|
||||
triggerAlarm(request: TriggerAlarmRequest): Promise<Response>;
|
||||
|
||||
/**
|
||||
* Resolves an incident.
|
||||
*/
|
||||
resolveIncident(request: PatchIncidentRequest): Promise<Response>;
|
||||
|
||||
/**
|
||||
* Acknowledge an incident.
|
||||
*/
|
||||
acknowledgeIncident(request: PatchIncidentRequest): Promise<Response>;
|
||||
incidentAction(request: TriggerAlarmRequest): Promise<Response>;
|
||||
|
||||
/**
|
||||
* Get a list of users for your organization.
|
||||
@@ -83,11 +71,6 @@ export interface SplunkOnCallApi {
|
||||
getEscalationPolicies(): Promise<EscalationPolicyInfo[]>;
|
||||
}
|
||||
|
||||
export type PatchIncidentRequest = {
|
||||
incidentNames: string[];
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type EscalationPolicyResponse = {
|
||||
policies: EscalationPolicyInfo[];
|
||||
};
|
||||
@@ -106,7 +89,7 @@ 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: theme.palette.background.paper,
|
||||
},
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ import { Incident, IncidentPhase } from '../types';
|
||||
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
||||
import { splunkOnCallApiRef } from '../../api/client';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { PatchIncidentRequest } from '../../api/types';
|
||||
import { TriggerAlarmRequest } from '../../api/types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
denseListIcon: {
|
||||
@@ -61,6 +61,7 @@ const useStyles = makeStyles({
|
||||
});
|
||||
|
||||
type Props = {
|
||||
team: string;
|
||||
incident: Incident;
|
||||
onIncidentAction: () => void;
|
||||
};
|
||||
@@ -93,20 +94,24 @@ const incidentPhaseTooltip = (currentPhase: IncidentPhase) => {
|
||||
|
||||
const IncidentAction = ({
|
||||
currentPhase,
|
||||
incidentNames,
|
||||
incidentId,
|
||||
resolveAction,
|
||||
acknowledgeAction,
|
||||
}: {
|
||||
currentPhase: string;
|
||||
incidentNames: string[];
|
||||
resolveAction: (args: PatchIncidentRequest) => void;
|
||||
acknowledgeAction: (args: PatchIncidentRequest) => void;
|
||||
incidentId: string;
|
||||
resolveAction: (args: TriggerAlarmRequest) => void;
|
||||
acknowledgeAction: (args: TriggerAlarmRequest) => void;
|
||||
}) => {
|
||||
switch (currentPhase) {
|
||||
case 'UNACKED':
|
||||
return (
|
||||
<Tooltip title="Aknowledge" placement="top">
|
||||
<IconButton onClick={() => acknowledgeAction({ incidentNames })}>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
acknowledgeAction({ incidentId, incidentType: 'ACKNOWLEDGEMENT' })
|
||||
}
|
||||
>
|
||||
<DoneIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -114,7 +119,11 @@ const IncidentAction = ({
|
||||
case 'ACKED':
|
||||
return (
|
||||
<Tooltip title="Resolve" placement="top">
|
||||
<IconButton onClick={() => resolveAction({ incidentNames })}>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
resolveAction({ incidentId, incidentType: 'RECOVERY' })
|
||||
}
|
||||
>
|
||||
<DoneAllIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -124,7 +133,11 @@ const IncidentAction = ({
|
||||
}
|
||||
};
|
||||
|
||||
export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
|
||||
export const IncidentListItem = ({
|
||||
incident,
|
||||
onIncidentAction,
|
||||
team,
|
||||
}: Props) => {
|
||||
const classes = useStyles();
|
||||
const duration =
|
||||
new Date().getTime() - new Date(incident.startTime!).getTime();
|
||||
@@ -136,17 +149,26 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
|
||||
|
||||
const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-');
|
||||
|
||||
const user = hasBeenManuallyTriggered
|
||||
? incident.monitorName?.replace('vouser-', '')
|
||||
: incident.monitorName;
|
||||
const source = () => {
|
||||
if (hasBeenManuallyTriggered) {
|
||||
return incident.monitorName?.replace('vouser-', '');
|
||||
}
|
||||
if (incident.monitorType === 'API') {
|
||||
return '{ REST }';
|
||||
}
|
||||
|
||||
return incident.monitorName;
|
||||
};
|
||||
|
||||
const [
|
||||
{ value: resolveValue, error: resolveError },
|
||||
handleResolveIncident,
|
||||
] = useAsyncFn(
|
||||
async ({ incidentNames }: PatchIncidentRequest) =>
|
||||
await api.resolveIncident({
|
||||
incidentNames,
|
||||
async ({ incidentId, incidentType }: TriggerAlarmRequest) =>
|
||||
await api.incidentAction({
|
||||
routingKey: team,
|
||||
incidentType,
|
||||
incidentId,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -154,9 +176,11 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
|
||||
{ value: acknowledgeValue, error: acknowledgeError },
|
||||
handleAcknowledgeIncident,
|
||||
] = useAsyncFn(
|
||||
async ({ incidentNames }: PatchIncidentRequest) =>
|
||||
await api.acknowledgeIncident({
|
||||
incidentNames,
|
||||
async ({ incidentId, incidentType }: TriggerAlarmRequest) =>
|
||||
await api.incidentAction({
|
||||
routingKey: team,
|
||||
incidentType,
|
||||
incidentId,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -211,7 +235,8 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
|
||||
}}
|
||||
secondary={
|
||||
<Typography noWrap variant="body2" color="textSecondary">
|
||||
Created {createdAt} {user && `by ${user}`}
|
||||
#{incident.incidentNumber} - Created {createdAt}{' '}
|
||||
{source() && `by ${source()}`}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
@@ -220,7 +245,7 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => {
|
||||
<ListItemSecondaryAction>
|
||||
<IncidentAction
|
||||
currentPhase={incident.currentPhase || ''}
|
||||
incidentNames={[incident.incidentNumber]}
|
||||
incidentId={incident.entityId}
|
||||
resolveAction={handleResolveIncident}
|
||||
acknowledgeAction={handleAcknowledgeIncident}
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,10 @@ describe('Incidents', () => {
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
|
||||
await waitFor(
|
||||
() => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders all incidents', async () => {
|
||||
@@ -87,11 +90,15 @@ describe('Incidents', () => {
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText('user', {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
getByText('user', {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument(),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
expect(getByText('test-incident')).toBeInTheDocument();
|
||||
expect(getByTitle('Acknowledged')).toBeInTheDocument();
|
||||
expect(getByLabelText('Status warning')).toBeInTheDocument();
|
||||
@@ -113,8 +120,14 @@ describe('Incidents', () => {
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText('Error encountered while fetching information. Error occurred'),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
getByText(
|
||||
'Error encountered while fetching information. Error occurred',
|
||||
),
|
||||
).toBeInTheDocument(),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,16 +29,36 @@ 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: theme.palette.background.paper,
|
||||
},
|
||||
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(
|
||||
async () => {
|
||||
// For some reason the changes applied to incidents (trigger-resolve-acknowledge)
|
||||
// may take some time to actually be applied after receiving the response from the server.
|
||||
// The timeout compensates for this latency.
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
const allIncidents = await api.getIncidents();
|
||||
const teams = await api.getTeams();
|
||||
const teamSlug = teams.find(teamValue => teamValue.name === team)?.slug;
|
||||
@@ -57,23 +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}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ const mockSplunkOnCallApi: Partial<SplunkOnCallClient> = {
|
||||
|
||||
const configApi: ConfigApi = new ConfigReader({
|
||||
splunkOnCall: {
|
||||
username: MOCKED_USER.username,
|
||||
eventsRestEndpoint: 'EXAMPLE_REST_ENDPOINT',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -91,7 +91,10 @@ describe('SplunkOnCallCard', () => {
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('Create Incident')).toBeInTheDocument();
|
||||
expect(getByText('Nice! No incidents found!')).toBeInTheDocument();
|
||||
await waitFor(
|
||||
() => expect(getByText('Nice! No incidents found!')).toBeInTheDocument(),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
expect(getByText('Empty escalation policy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,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';
|
||||
@@ -47,12 +48,12 @@ export const MissingTeamAnnotation = () => (
|
||||
<MissingAnnotationEmptyState annotation={SPLUNK_ON_CALL_TEAM} />
|
||||
);
|
||||
|
||||
export const MissingUsername = () => (
|
||||
export const MissingEventsRestEndpoint = () => (
|
||||
<CardContent>
|
||||
<EmptyState
|
||||
title="No Splunk On-Call user available."
|
||||
title="No Splunk On-Call REST endpoint 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."
|
||||
description="You need to add a valid REST endpoint to your 'app-config.yaml' if you want to enable Splunk On-Call."
|
||||
/>
|
||||
</CardContent>
|
||||
);
|
||||
@@ -71,7 +72,8 @@ 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');
|
||||
const eventsRestEndpoint =
|
||||
config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null;
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshIncidents(x => !x);
|
||||
@@ -95,9 +97,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 />;
|
||||
}
|
||||
@@ -118,8 +117,9 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
|
||||
if (!team) {
|
||||
return <MissingTeamAnnotation />;
|
||||
}
|
||||
if (!username || !incidentCreator) {
|
||||
return <MissingUsername />;
|
||||
|
||||
if (!eventsRestEndpoint) {
|
||||
return <MissingEventsRestEndpoint />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -128,15 +128,12 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
|
||||
{users?.usersHashMap && team && (
|
||||
<EscalationPolicy team={team} users={users.usersHashMap} />
|
||||
)}
|
||||
{users && incidentCreator && (
|
||||
<TriggerDialog
|
||||
users={users.userList}
|
||||
incidentCreator={incidentCreator}
|
||||
showDialog={showDialog}
|
||||
handleDialog={handleDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
/>
|
||||
)}
|
||||
<TriggerDialog
|
||||
team={team}
|
||||
showDialog={showDialog}
|
||||
handleDialog={handleDialog}
|
||||
onIncidentCreated={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -148,15 +145,22 @@ export const SplunkOnCallCard = ({ entity }: Props) => {
|
||||
icon: <AlarmAddIcon />,
|
||||
};
|
||||
|
||||
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 />
|
||||
|
||||
@@ -24,13 +24,11 @@ import {
|
||||
} from '@backstage/core';
|
||||
import { splunkOnCallApiRef } from '../../api';
|
||||
import { TriggerDialog } from './TriggerDialog';
|
||||
import { ESCALATION_POLICIES, MOCKED_USER } from '../../api/mocks';
|
||||
|
||||
describe('TriggerDialog', () => {
|
||||
const mockTriggerAlarmFn = jest.fn();
|
||||
const mockSplunkOnCallApi = {
|
||||
triggerAlarm: mockTriggerAlarmFn,
|
||||
getEscalationPolicies: async () => ESCALATION_POLICIES,
|
||||
incidentAction: mockTriggerAlarmFn,
|
||||
};
|
||||
|
||||
const apis = ApiRegistry.from([
|
||||
@@ -45,14 +43,13 @@ describe('TriggerDialog', () => {
|
||||
]);
|
||||
|
||||
it('open the dialog and trigger an alarm', async () => {
|
||||
const { getByText, getByRole, getAllByRole, getByTestId } = render(
|
||||
const { getByText, getByRole, getByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<TriggerDialog
|
||||
team="Example"
|
||||
showDialog
|
||||
incidentCreator={MOCKED_USER}
|
||||
handleDialog={() => {}}
|
||||
users={[MOCKED_USER]}
|
||||
onIncidentCreated={() => {}}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
@@ -65,36 +62,20 @@ describe('TriggerDialog', () => {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
const summary = getByTestId('trigger-summary-input');
|
||||
const body = getByTestId('trigger-body-input');
|
||||
const behavior = getByTestId('trigger-select-behavior');
|
||||
const description = 'Test Trigger Alarm';
|
||||
await act(async () => {
|
||||
fireEvent.change(summary, { target: { value: description } });
|
||||
fireEvent.change(body, { target: { value: description } });
|
||||
fireEvent.change(behavior, { target: { value: '0' } });
|
||||
fireEvent.mouseDown(getAllByRole('button')[0]);
|
||||
});
|
||||
const incidentType = getByTestId('trigger-incident-type');
|
||||
const incidentId = getByTestId('trigger-incident-id');
|
||||
const incidentDisplayName = getByTestId('trigger-incident-displayName');
|
||||
const incidentMessage = getByTestId('trigger-incident-message');
|
||||
|
||||
// Trigger user targets select
|
||||
const options = getAllByRole('option');
|
||||
await act(async () => {
|
||||
fireEvent.click(options[0]);
|
||||
fireEvent.keyDown(options[0], {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
keyCode: 27,
|
||||
charCode: 27,
|
||||
fireEvent.change(incidentType, { target: { value: 'CRITICAL' } });
|
||||
fireEvent.change(incidentId, { target: { value: 'incident-id' } });
|
||||
fireEvent.change(incidentDisplayName, {
|
||||
target: { value: 'incident-display-name' },
|
||||
});
|
||||
fireEvent.change(incidentMessage, {
|
||||
target: { value: 'incident-message' },
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger policy targets select
|
||||
await act(async () => {
|
||||
fireEvent.mouseDown(getAllByRole('button')[1]);
|
||||
});
|
||||
const policiesOptions = getAllByRole('option');
|
||||
await act(async () => {
|
||||
fireEvent.click(policiesOptions[0]);
|
||||
});
|
||||
|
||||
// Trigger incident creation button
|
||||
@@ -104,14 +85,11 @@ describe('TriggerDialog', () => {
|
||||
});
|
||||
expect(mockTriggerAlarmFn).toHaveBeenCalled();
|
||||
expect(mockTriggerAlarmFn).toHaveBeenCalledWith({
|
||||
summary: description,
|
||||
details: description,
|
||||
userName: 'test_user',
|
||||
targets: [
|
||||
{ slug: 'test_user', type: 'User' },
|
||||
{ slug: 'team-zEalMCgwYSA0Lt40', type: 'EscalationPolicy' },
|
||||
],
|
||||
isMultiResponder: false,
|
||||
incidentType: 'CRITICAL',
|
||||
incidentId: 'incident-id',
|
||||
routingKey: 'Example',
|
||||
incidentDisplayName: 'incident-display-name',
|
||||
incidentMessage: 'incident-message',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
CircularProgress,
|
||||
Select,
|
||||
MenuItem,
|
||||
Input,
|
||||
Chip,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
@@ -35,23 +33,13 @@ import {
|
||||
InputLabel,
|
||||
} from '@material-ui/core';
|
||||
import { useApi, alertApiRef } from '@backstage/core';
|
||||
import { useAsync, useAsyncFn } from 'react-use';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { splunkOnCallApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { User } from '../types';
|
||||
import { IncidentTarget, TargetType } from '../../api/types';
|
||||
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
import { TriggerAlarmRequest } from '../../api/types';
|
||||
|
||||
type Props = {
|
||||
users: User[];
|
||||
incidentCreator: User;
|
||||
team: string;
|
||||
showDialog: boolean;
|
||||
handleDialog: () => void;
|
||||
onIncidentCreated: () => void;
|
||||
@@ -68,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',
|
||||
@@ -79,8 +76,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
);
|
||||
|
||||
export const TriggerDialog = ({
|
||||
users,
|
||||
incidentCreator,
|
||||
team,
|
||||
showDialog,
|
||||
handleDialog,
|
||||
onIncidentCreated: onIncidentCreated,
|
||||
@@ -89,72 +85,46 @@ export const TriggerDialog = ({
|
||||
const api = useApi(splunkOnCallApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const [userTargets, setUserTargets] = useState<string[]>([]);
|
||||
const [policyTargets, setPolicyTargets] = useState<string[]>([]);
|
||||
const [detailsValue, setDetails] = useState<string>('');
|
||||
const [summaryValue, setSummary] = useState<string>('');
|
||||
const [isMultiResponderValue, setIsMultiResponder] = useState<string>('1');
|
||||
const [incidentType, setIncidentType] = useState<string>('');
|
||||
const [incidentId, setIncidentId] = useState<string>();
|
||||
const [incidentDisplayName, setIncidentDisplayName] = useState<string>('');
|
||||
const [incidentMessage, setIncidentMessage] = useState<string>('');
|
||||
const [incidentStartTime, setIncidentStartTime] = useState<number>();
|
||||
|
||||
const [
|
||||
{ value, loading: triggerLoading, error: triggerError },
|
||||
handleTriggerAlarm,
|
||||
] = useAsyncFn(
|
||||
async (
|
||||
summary: string,
|
||||
details: string,
|
||||
userName: string,
|
||||
targets: IncidentTarget[],
|
||||
isMultiResponder: boolean,
|
||||
) =>
|
||||
await api.triggerAlarm({
|
||||
summary,
|
||||
details,
|
||||
userName,
|
||||
targets,
|
||||
isMultiResponder,
|
||||
}),
|
||||
async (params: TriggerAlarmRequest) => await api.incidentAction(params),
|
||||
);
|
||||
|
||||
const {
|
||||
value: policies,
|
||||
loading: policiesLoaading,
|
||||
error: policiesError,
|
||||
} = useAsync(async () => {
|
||||
const allPolicies = await api.getEscalationPolicies();
|
||||
return allPolicies;
|
||||
});
|
||||
|
||||
const handleUserTargets = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setUserTargets(event.target.value as string[]);
|
||||
const handleIncidentType = (event: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setIncidentType(event.target.value as string);
|
||||
};
|
||||
|
||||
const handlePolicyTargets = (
|
||||
event: React.ChangeEvent<{ value: unknown }>,
|
||||
const handleIncidentId = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setIncidentId(event.target.value as string);
|
||||
};
|
||||
|
||||
const handleIncidentDisplayName = (
|
||||
event: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) => {
|
||||
setPolicyTargets(event.target.value as string[]);
|
||||
setIncidentDisplayName(event.target.value);
|
||||
};
|
||||
|
||||
const detailsChanged = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDetails(event.target.value);
|
||||
};
|
||||
|
||||
const summaryChanged = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setSummary(event.target.value);
|
||||
};
|
||||
|
||||
const isMultiResponderChanged = (
|
||||
event: React.ChangeEvent<{ value: unknown }>,
|
||||
const handleIncidentMessage = (
|
||||
event: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) => {
|
||||
setIsMultiResponder(event.target.value as string);
|
||||
setIncidentMessage(event.target.value);
|
||||
};
|
||||
|
||||
const targets = (): IncidentTarget[] => [
|
||||
...userTargets.map(user => ({ slug: user, type: TargetType.UserValue })),
|
||||
...policyTargets.map(user => ({
|
||||
slug: user,
|
||||
type: TargetType.EscalationPolicyValue,
|
||||
})),
|
||||
];
|
||||
const handleIncidentStartTime = (
|
||||
event: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) => {
|
||||
const dateTime = new Date(event.target.value).getTime();
|
||||
const dateTimeInSeconds = Math.floor(dateTime / 1000);
|
||||
setIncidentStartTime(dateTimeInSeconds);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
@@ -178,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">
|
||||
@@ -199,136 +166,66 @@ 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.
|
||||
</Typography>
|
||||
<div style={{ marginTop: '1em' }}>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
Select the targets
|
||||
</Typography>
|
||||
<div className={classes.targets}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel>Select Users</InputLabel>
|
||||
<Select
|
||||
id="user-targets"
|
||||
multiple
|
||||
value={userTargets}
|
||||
onChange={handleUserTargets}
|
||||
input={<Input />}
|
||||
renderValue={selected => (
|
||||
<div className={classes.chips}>
|
||||
{(selected as string[]).map(selectedUser => {
|
||||
const element = users.find(
|
||||
user => user.username === selectedUser,
|
||||
);
|
||||
return (
|
||||
<Chip
|
||||
key={selectedUser}
|
||||
label={`${element?.firstName} ${element?.lastName}`}
|
||||
className={classes.chip}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{users.map(user => (
|
||||
<MenuItem key={user.email} value={user.username}>
|
||||
{user.firstName} {user.lastName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel>Select Teams / Policies</InputLabel>
|
||||
<Select
|
||||
id="policy-targets"
|
||||
multiple
|
||||
value={policyTargets}
|
||||
onChange={handlePolicyTargets}
|
||||
input={<Input />}
|
||||
renderValue={selected => (
|
||||
<div className={classes.chips}>
|
||||
{(selected as string[]).map(selectedPolicy => {
|
||||
const element = policies?.find(
|
||||
policy => policy.policy.slug === selectedPolicy,
|
||||
);
|
||||
return (
|
||||
<Chip
|
||||
key={selectedPolicy}
|
||||
label={element?.policy.name}
|
||||
className={classes.chip}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{!policiesError &&
|
||||
!policiesLoaading &&
|
||||
policies &&
|
||||
policies.map(policy => (
|
||||
<MenuItem
|
||||
key={policy.policy.slug}
|
||||
value={policy.policy.slug}
|
||||
>
|
||||
{policy.policy.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<Typography
|
||||
style={{ marginTop: '1em' }}
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
Acknowledge Behavior
|
||||
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}>
|
||||
<Select
|
||||
id="multi-responder"
|
||||
value={isMultiResponderValue}
|
||||
onChange={isMultiResponderChanged}
|
||||
inputProps={{ 'data-testid': 'trigger-select-behavior' }}
|
||||
>
|
||||
<MenuItem value="1">
|
||||
Stop paging after a single escalation policy or user has
|
||||
acknowledged
|
||||
</MenuItem>
|
||||
<MenuItem value="0">
|
||||
Continue paging until each escalation policy or user above has
|
||||
acknowledged
|
||||
</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
|
||||
required
|
||||
inputProps={{ 'data-testid': 'trigger-summary-input' }}
|
||||
inputProps={{ 'data-testid': 'trigger-incident-id' }}
|
||||
id="summary"
|
||||
multiline
|
||||
fullWidth
|
||||
rows="4"
|
||||
margin="normal"
|
||||
label="Incident summary"
|
||||
label="Incident id"
|
||||
variant="outlined"
|
||||
onChange={summaryChanged}
|
||||
onChange={handleIncidentId}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
inputProps={{ 'data-testid': 'trigger-body-input' }}
|
||||
inputProps={{ 'data-testid': 'trigger-incident-displayName' }}
|
||||
id="summary"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
label="Incident display name"
|
||||
variant="outlined"
|
||||
onChange={handleIncidentDisplayName}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
inputProps={{ 'data-testid': 'trigger-incident-message' }}
|
||||
id="details"
|
||||
multiline
|
||||
fullWidth
|
||||
rows="2"
|
||||
margin="normal"
|
||||
label="Incident body"
|
||||
label="Incident message"
|
||||
variant="outlined"
|
||||
onChange={detailsChanged}
|
||||
onChange={handleIncidentMessage}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -337,20 +234,21 @@ export const TriggerDialog = ({
|
||||
id="trigger"
|
||||
color="secondary"
|
||||
disabled={
|
||||
!detailsValue ||
|
||||
!summaryValue ||
|
||||
(!userTargets.length && !policyTargets.length) ||
|
||||
!incidentType.length ||
|
||||
!incidentDisplayName ||
|
||||
!incidentMessage ||
|
||||
triggerLoading
|
||||
}
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
handleTriggerAlarm(
|
||||
summaryValue,
|
||||
detailsValue,
|
||||
incidentCreator.username!,
|
||||
targets(),
|
||||
!!Number(isMultiResponderValue),
|
||||
)
|
||||
handleTriggerAlarm({
|
||||
routingKey: team,
|
||||
incidentType,
|
||||
incidentDisplayName,
|
||||
incidentMessage,
|
||||
...(incidentId ? { incidentId } : {}),
|
||||
...(incidentStartTime ? { incidentStartTime } : {}),
|
||||
} as TriggerAlarmRequest)
|
||||
}
|
||||
endIcon={triggerLoading && <CircularProgress size={16} />}
|
||||
>
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IncidentTarget } from '../api/types';
|
||||
|
||||
export type Team = {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
@@ -68,14 +66,6 @@ export type User = {
|
||||
_selfUrl?: string;
|
||||
};
|
||||
|
||||
export type CreateIncidentRequest = {
|
||||
summary: string;
|
||||
details: string;
|
||||
userName: string;
|
||||
targets: IncidentTarget;
|
||||
isMultiResponder: boolean;
|
||||
};
|
||||
|
||||
export type IncidentPhase = 'UNACKED' | 'ACKED' | 'RESOLVED';
|
||||
|
||||
export type Incident = {
|
||||
@@ -88,7 +78,7 @@ export type Incident = {
|
||||
alertCount?: number;
|
||||
lastAlertTime?: string;
|
||||
lastAlertId?: string;
|
||||
entityId?: string;
|
||||
entityId: string;
|
||||
host?: string;
|
||||
service?: string;
|
||||
pagedUsers?: string[];
|
||||
|
||||
Reference in New Issue
Block a user