Add PagerDuty ChangeEvents
Signed-off-by: Douglas Read <read.d@northeastern.edu>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
# PagerDuty + Backstage Integration Benefits
|
||||
|
||||
- Display relevant PagerDuty information about an entity within Backstage, such as the escalation policy or if there are any active incidents
|
||||
- Display relevant PagerDuty information about an entity within Backstage, such as the escalation policy, if there are any active incidents, and recent changes
|
||||
- Trigger an incident to the currently on-call responder(s) for a service
|
||||
|
||||
# How it Works
|
||||
|
||||
- The Backstage PagerDuty plugin allows PagerDuty information about a Backstage entity to be displayed within Backstage. This includes active incidents as well as the current on-call responders' names, email addresses, and links to their profiles in PagerDuty.
|
||||
- The Backstage PagerDuty plugin allows PagerDuty information about a Backstage entity to be displayed within Backstage. This includes active incidents, recent change events, as well as the current on-call responders' names, email addresses, and links to their profiles in PagerDuty.
|
||||
- Incidents can be manually triggered via the plugin with a user-provided description, which will in turn notify the current on-call responders.
|
||||
|
||||
# Requirements
|
||||
@@ -152,4 +152,4 @@ yarn remove @backstage/plugin-pagerduty
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Service, Incident, OnCall } from '../components/types';
|
||||
import { Service, Incident, ChangeEvent, OnCall } from '../components/types';
|
||||
import {
|
||||
PagerDutyApi,
|
||||
TriggerAlarmRequest,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
OnCallsResponse,
|
||||
ClientApiConfig,
|
||||
RequestOptions,
|
||||
ChangeEventsResponse,
|
||||
} from './types';
|
||||
import {
|
||||
createApiRef,
|
||||
@@ -69,6 +70,17 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
return incidents;
|
||||
}
|
||||
|
||||
async getChangeEventsByServiceId(serviceId: string): Promise<ChangeEvent[]> {
|
||||
const params = `limit=5&time_zone=UTC&sort_by=timestamp`;
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
'proxy',
|
||||
)}/pagerduty/services/${serviceId}/change_events?${params}`;
|
||||
|
||||
const { change_events } = await this.getByUrl<ChangeEventsResponse>(url);
|
||||
|
||||
return change_events;
|
||||
}
|
||||
|
||||
async getOnCallByPolicyId(policyId: string): Promise<OnCall[]> {
|
||||
const params = `time_zone=UTC&include[]=users&escalation_policy_ids[]=${policyId}`;
|
||||
const url = `${await this.config.discoveryApi.getBaseUrl(
|
||||
@@ -80,11 +92,11 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
}
|
||||
|
||||
triggerAlarm({
|
||||
integrationKey,
|
||||
source,
|
||||
description,
|
||||
userName,
|
||||
}: TriggerAlarmRequest): Promise<Response> {
|
||||
integrationKey,
|
||||
source,
|
||||
description,
|
||||
userName,
|
||||
}: TriggerAlarmRequest): Promise<Response> {
|
||||
const body = JSON.stringify({
|
||||
event_action: 'trigger',
|
||||
routing_key: integrationKey,
|
||||
@@ -124,7 +136,6 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
},
|
||||
};
|
||||
const response = await this.request(url, options);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Incident, OnCall, Service } from '../components/types';
|
||||
import { Incident, ChangeEvent, OnCall, Service } from '../components/types';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export type TriggerAlarmRequest = {
|
||||
@@ -37,6 +37,12 @@ export interface PagerDutyApi {
|
||||
*/
|
||||
getIncidentsByServiceId(serviceId: string): Promise<Incident[]>;
|
||||
|
||||
/**
|
||||
* Fetches a list of change events a provided service has.
|
||||
*
|
||||
*/
|
||||
getChangeEventsByServiceId(serviceId: string): Promise<ChangeEvent[]>;
|
||||
|
||||
/**
|
||||
* Fetches the list of users in an escalation policy.
|
||||
*
|
||||
@@ -57,6 +63,10 @@ export type IncidentsResponse = {
|
||||
incidents: Incident[];
|
||||
};
|
||||
|
||||
export type ChangeEventsResponse = {
|
||||
change_events: ChangeEvent[];
|
||||
};
|
||||
|
||||
export type OnCallsResponse = {
|
||||
oncalls: OnCall[];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 ChangeEventEmptyState = () => {
|
||||
return (
|
||||
<Grid container justify="center" direction="column" alignItems="center">
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">No change events to display yet.</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<img
|
||||
src={EmptyStateImage}
|
||||
alt="EmptyState"
|
||||
data-testid="emptyStateImg"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { ChangeEvent } from '../types';
|
||||
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>({
|
||||
denseListIcon: {
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
listItemPrimary: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
changeEvent: ChangeEvent;
|
||||
};
|
||||
|
||||
export const ChangeEventListItem = ({ changeEvent }: Props) => {
|
||||
const classes = useStyles();
|
||||
const duration =
|
||||
new Date().getTime() - new Date(changeEvent.timestamp).getTime();
|
||||
const changed_at = DateTime.local()
|
||||
.minus(Duration.fromMillis(duration))
|
||||
.toRelative({ locale: 'en' });
|
||||
let externalLinkElem = null;
|
||||
if (changeEvent.links.length > 0) {
|
||||
const text: string = changeEvent.links[0].text;
|
||||
externalLinkElem = (
|
||||
<Tooltip title={text} placement="top">
|
||||
<IconButton
|
||||
href={changeEvent.links[0].href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="primary"
|
||||
>
|
||||
<OpenInBrowserIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem dense key={changeEvent.id}>
|
||||
<ListItemText
|
||||
primary={<>{changeEvent.summary}</>}
|
||||
primaryTypographyProps={{
|
||||
variant: 'body1',
|
||||
className: classes.listItemPrimary,
|
||||
}}
|
||||
secondary={
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Triggered from {changeEvent.source} {changed_at}.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{externalLinkElem}
|
||||
<Tooltip title="View in Pagerduty" placement="top">
|
||||
<IconButton
|
||||
href={changeEvent.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="primary"
|
||||
>
|
||||
<OpenInBrowserIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { render, waitFor } from '@testing-library/react';
|
||||
import { ChangeEvent } from '../types';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import { ChangeEvents } from './ChangeEvents';
|
||||
|
||||
const mockPagerDutyApi = {
|
||||
getChangeEventsByServiceId: () => [],
|
||||
};
|
||||
const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]);
|
||||
|
||||
describe('Incidents', () => {
|
||||
it('Renders an empty state when there are no change events', async () => {
|
||||
mockPagerDutyApi.getChangeEventsByServiceId = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => []);
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<ChangeEvents serviceId="abc" refreshEvents={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('No change events to display yet.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders all change events', async () => {
|
||||
mockPagerDutyApi.getChangeEventsByServiceId = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
async () =>
|
||||
[
|
||||
{
|
||||
id: 'id1',
|
||||
source: 'changeSource1',
|
||||
html_url: 'www.pdlink.com',
|
||||
links: [
|
||||
{
|
||||
href: 'www.externalLink1.com',
|
||||
text: 'link1',
|
||||
},
|
||||
],
|
||||
summary: 'summary of event',
|
||||
timestamp: '5 minutes',
|
||||
},
|
||||
{
|
||||
id: 'id2',
|
||||
source: 'changeSource1',
|
||||
html_url: 'www.pdlink.com/link',
|
||||
links: [
|
||||
{
|
||||
href: 'www.externalLink1.com',
|
||||
text: 'link1',
|
||||
},
|
||||
],
|
||||
summary: 'sum of EVENT',
|
||||
timestamp: '20 minutes',
|
||||
},
|
||||
] as ChangeEvent[],
|
||||
);
|
||||
const { getByText, getAllByTitle, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<ChangeEvents serviceId="abc" refreshEvents={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(getByText('summary of event')).toBeInTheDocument();
|
||||
expect(getByText('sum of EVENT')).toBeInTheDocument();
|
||||
expect(getByText('5 minutes')).toBeInTheDocument();
|
||||
expect(getByText('20 minutes')).toBeInTheDocument();
|
||||
|
||||
// assert links, mailto and hrefs, date calculation
|
||||
expect(getAllByTitle('View in PagerDuty').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('Handle errors', async () => {
|
||||
mockPagerDutyApi.getChangeEventsByServiceId = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Error occurred'));
|
||||
|
||||
const { getByText, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<ChangeEvents serviceId="abc" refreshEvents={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
await waitFor(() => !queryByTestId('progress'));
|
||||
expect(
|
||||
getByText('Error encountered while fetching information. Error occurred'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { ChangeEventListItem } from './ChangeEventListItem';
|
||||
import { ChangeEventEmptyState } from './ChangeEventEmptyState';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { pagerDutyApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
type Props = {
|
||||
serviceId: string;
|
||||
refreshEvents: boolean;
|
||||
};
|
||||
|
||||
export const ChangeEvents = ({ serviceId, refreshEvents }: Props) => {
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
|
||||
const [{ value: changeEvents, loading, error }, getChangeEvents] = useAsyncFn(
|
||||
async () => await api.getChangeEventsByServiceId(serviceId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getChangeEvents();
|
||||
}, [refreshEvents, getChangeEvents]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching information. {error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (!changeEvents?.length) {
|
||||
return <ChangeEventEmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<List dense subheader={<ListSubheader>CHANGE EVENTS</ListSubheader>}>
|
||||
{changeEvents!.map((changeEvent, index) => (
|
||||
<ChangeEventListItem
|
||||
key={changeEvent.id + index}
|
||||
changeEvent={changeEvent}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { ChangeEvents } from './ChangeEvents';
|
||||
@@ -20,7 +20,7 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api';
|
||||
import { Service } from '../types';
|
||||
import { Service, User } from '../types';
|
||||
|
||||
import { alertApiRef, createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
@@ -52,19 +52,22 @@ const entity: Entity = {
|
||||
},
|
||||
};
|
||||
|
||||
const user: User = {
|
||||
name: 'person1',
|
||||
id: 'p1',
|
||||
summary: 'person1',
|
||||
email: 'person1@example.com',
|
||||
html_url: 'http://a.com/id1',
|
||||
};
|
||||
|
||||
const service: Service = {
|
||||
id: 'abc',
|
||||
name: 'pagerduty-name',
|
||||
html_url: 'www.example.com',
|
||||
escalation_policy: {
|
||||
id: 'def',
|
||||
user: {
|
||||
name: 'person1',
|
||||
id: 'p1',
|
||||
summary: 'person1',
|
||||
email: 'person1@example.com',
|
||||
html_url: 'http://a.com/id1',
|
||||
},
|
||||
user: user,
|
||||
html_url: 'http://a.com/id1',
|
||||
},
|
||||
integrationKey: 'abcd',
|
||||
};
|
||||
|
||||
@@ -24,15 +24,19 @@ import { pagerDutyApiRef, UnauthorizedError } from '../../api';
|
||||
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
||||
import { MissingTokenError } from '../Errors/MissingTokenError';
|
||||
import WebIcon from '@material-ui/icons/Web';
|
||||
import DateRangeIcon from '@material-ui/icons/DateRange';
|
||||
import { usePagerdutyEntity } from '../../hooks';
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from '../constants';
|
||||
import { TriggerDialog } from '../TriggerDialog';
|
||||
import { ChangeEvents } from '../ChangeEvents';
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Progress,
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
TabbedCard,
|
||||
CardTab,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
@@ -42,6 +46,8 @@ export const PagerDutyCard = () => {
|
||||
const { integrationKey } = usePagerdutyEntity();
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
|
||||
const [refreshChangeEvents, setRefreshChangeEvents] =
|
||||
useState<boolean>(false);
|
||||
const [dialogShown, setDialogShown] = useState<boolean>(false);
|
||||
|
||||
const showDialog = useCallback(() => {
|
||||
@@ -53,6 +59,7 @@ export const PagerDutyCard = () => {
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshIncidents(x => !x);
|
||||
setRefreshChangeEvents(x => !x);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
@@ -69,6 +76,7 @@ export const PagerDutyCard = () => {
|
||||
name: services[0].name,
|
||||
url: services[0].html_url,
|
||||
policyId: services[0].escalation_policy.id,
|
||||
policyLink: services[0].escalation_policy.html_url,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -101,19 +109,39 @@ export const PagerDutyCard = () => {
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
const escalationPolicyLink: IconLinkVerticalProps = {
|
||||
label: 'Escalation Policy',
|
||||
href: service!.policyLink,
|
||||
icon: <DateRangeIcon />,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card data-testid="pagerduty-card">
|
||||
<CardHeader
|
||||
title="PagerDuty"
|
||||
subheader={<HeaderIconLinkRow links={[serviceLink, triggerLink]} />}
|
||||
subheader={
|
||||
<HeaderIconLinkRow
|
||||
links={[serviceLink, triggerLink, escalationPolicyLink]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Incidents
|
||||
serviceId={service!.id}
|
||||
refreshIncidents={refreshIncidents}
|
||||
/>
|
||||
<TabbedCard>
|
||||
<CardTab label="Incidents">
|
||||
<Incidents
|
||||
serviceId={service!.id}
|
||||
refreshIncidents={refreshIncidents}
|
||||
/>
|
||||
</CardTab>
|
||||
<CardTab label="Change Events">
|
||||
<ChangeEvents
|
||||
serviceId={service!.id}
|
||||
refreshEvents={refreshChangeEvents}
|
||||
/>
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
<EscalationPolicy policyId={service!.policyId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,6 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type ChangeEvent = {
|
||||
id: string;
|
||||
integration: [
|
||||
{
|
||||
service: Service;
|
||||
},
|
||||
];
|
||||
source: string;
|
||||
html_url: string;
|
||||
links: [
|
||||
{
|
||||
href: string;
|
||||
text: string;
|
||||
},
|
||||
];
|
||||
summary: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export type Incident = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -36,6 +55,7 @@ export type Service = {
|
||||
escalation_policy: {
|
||||
id: string;
|
||||
user: User;
|
||||
html_url: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user