@@ -28,6 +28,7 @@ import {
|
||||
EscalationPolicy,
|
||||
OnCall,
|
||||
Schedule,
|
||||
Service,
|
||||
UptimeMonitor,
|
||||
User,
|
||||
} from '../types';
|
||||
@@ -35,7 +36,9 @@ import {
|
||||
EventRequest,
|
||||
GetAlertsCountOpts,
|
||||
GetAlertsOpts,
|
||||
GetServicesOpts,
|
||||
ILertApi,
|
||||
ServiceRequest,
|
||||
} from './types';
|
||||
|
||||
/** @public */
|
||||
@@ -127,7 +130,6 @@ export class ILertClient implements ILertApi {
|
||||
});
|
||||
}
|
||||
const response = await this.fetch(`/api/alerts?${query.toString()}`, init);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -444,7 +446,10 @@ export class ILertClient implements ILertApi {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/schedules', init);
|
||||
const response = await this.fetch(
|
||||
'/api/schedules?include=currentShift&include=nextShift',
|
||||
init,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -479,6 +484,41 @@ export class ILertClient implements ILertApi {
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchServices(opts?: GetServicesOpts): Promise<Service[]> {
|
||||
const init = {
|
||||
headers: JSON_HEADERS,
|
||||
};
|
||||
const query = new URLSearchParams();
|
||||
if (opts?.maxResults !== undefined) {
|
||||
query.append('max-results', String(opts.maxResults));
|
||||
}
|
||||
if (opts?.startIndex !== undefined) {
|
||||
query.append('start-index', String(opts.startIndex));
|
||||
}
|
||||
|
||||
query.append('include', 'uptime');
|
||||
|
||||
const response = await this.fetch(
|
||||
`/api/services?${query.toString()}`,
|
||||
init,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async createService(serviceRequest: ServiceRequest): Promise<boolean> {
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({
|
||||
// apiKey: eventRequest.integrationKey,
|
||||
name: serviceRequest.name,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await this.fetch('/api/services', init);
|
||||
return response;
|
||||
}
|
||||
|
||||
getAlertDetailsURL(alert: Alert): string {
|
||||
return `${this.baseUrl}/alert/view.jsf?id=${encodeURIComponent(alert.id)}`;
|
||||
}
|
||||
@@ -510,6 +550,12 @@ export class ILertClient implements ILertApi {
|
||||
)}`;
|
||||
}
|
||||
|
||||
getServiceDetailsURL(service: Service): string {
|
||||
return `${this.baseUrl}/service/view.jsf?id=${encodeURIComponent(
|
||||
service.id,
|
||||
)}`;
|
||||
}
|
||||
|
||||
getUserPhoneNumber(user: User | null) {
|
||||
return user?.mobile?.number || user?.landline?.number || '';
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
export { ilertApiRef, ILertClient } from './client';
|
||||
export type {
|
||||
EventRequest,
|
||||
AlertEventRequest as EventRequest,
|
||||
GetAlertsCountOpts,
|
||||
GetAlertsOpts,
|
||||
GetServicesOpts,
|
||||
ILertApi,
|
||||
TableState,
|
||||
} from './types';
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
EscalationPolicy,
|
||||
OnCall,
|
||||
Schedule,
|
||||
Service,
|
||||
UptimeMonitor,
|
||||
User,
|
||||
} from '../types';
|
||||
@@ -46,6 +47,12 @@ export type GetAlertsCountOpts = {
|
||||
states?: AlertStatus[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type GetServicesOpts = {
|
||||
maxResults?: number;
|
||||
startIndex?: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type EventRequest = {
|
||||
integrationKey: string;
|
||||
@@ -55,6 +62,11 @@ export type EventRequest = {
|
||||
source: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ServiceRequest = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface ILertApi {
|
||||
fetchAlerts(opts?: GetAlertsOpts): Promise<Alert[]>;
|
||||
@@ -94,11 +106,15 @@ export interface ILertApi {
|
||||
end: string,
|
||||
): Promise<Schedule>;
|
||||
|
||||
fetchServices(opts?: GetServicesOpts): Promise<Service[]>;
|
||||
createService(eventRequest: ServiceRequest): Promise<boolean>;
|
||||
|
||||
getAlertDetailsURL(alert: Alert): string;
|
||||
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
|
||||
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
|
||||
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
|
||||
getScheduleDetailsURL(schedule: Schedule): string;
|
||||
getServiceDetailsURL(service: Service): string;
|
||||
getUserPhoneNumber(user: User | null): string;
|
||||
getUserInitials(user: User | null): string;
|
||||
}
|
||||
|
||||
@@ -140,14 +140,19 @@ export const AlertsTable = ({
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
const assignedToColumn: TableColumn = {
|
||||
title: 'Assigned to',
|
||||
field: 'assignedTo',
|
||||
const respondersColumn: TableColumn = {
|
||||
title: 'Responders',
|
||||
field: 'responders',
|
||||
cellStyle: !compact ? mdColumnStyle : lgColumnStyle,
|
||||
headerStyle: !compact ? mdColumnStyle : lgColumnStyle,
|
||||
render: rowData => (
|
||||
<Typography noWrap>
|
||||
{ilertApi.getUserInitials((rowData as Alert).assignedTo)}
|
||||
<Typography>
|
||||
{(rowData as Alert).responders.map((value, i, arr) => {
|
||||
return (
|
||||
ilertApi.getUserInitials(value.user) +
|
||||
(arr.length - 1 !== i ? ', ' : '')
|
||||
);
|
||||
})}
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
@@ -187,7 +192,7 @@ export const AlertsTable = ({
|
||||
? [
|
||||
summaryColumn,
|
||||
durationColumn,
|
||||
assignedToColumn,
|
||||
respondersColumn,
|
||||
statusColumn,
|
||||
actionsColumn,
|
||||
]
|
||||
@@ -196,7 +201,7 @@ export const AlertsTable = ({
|
||||
summaryColumn,
|
||||
sourceColumn,
|
||||
durationColumn,
|
||||
assignedToColumn,
|
||||
respondersColumn,
|
||||
priorityColumn,
|
||||
statusColumn,
|
||||
actionsColumn,
|
||||
@@ -247,7 +252,7 @@ export const AlertsTable = ({
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="button" color="textSecondary">
|
||||
INCIDENTS
|
||||
ALERTS
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import React from 'react';
|
||||
import { AlertsPage } from '../AlertsPage';
|
||||
import { OnCallSchedulesPage } from '../OnCallSchedulesPage';
|
||||
import { ServicesPage } from '../ServicesPage';
|
||||
import { UptimeMonitorsPage } from '../UptimeMonitorsPage';
|
||||
|
||||
/** @public */
|
||||
@@ -32,6 +33,7 @@ export const ILertPage = () => {
|
||||
{ label: 'Who is on call?' },
|
||||
{ label: 'Alerts' },
|
||||
{ label: 'Uptime Monitors' },
|
||||
{ label: 'Services' },
|
||||
];
|
||||
const renderTab = () => {
|
||||
switch (selectedTab) {
|
||||
@@ -41,6 +43,8 @@ export const ILertPage = () => {
|
||||
return <AlertsPage />;
|
||||
case 2:
|
||||
return <UptimeMonitorsPage />;
|
||||
case 3:
|
||||
return <ServicesPage />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2021 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 { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import React from 'react';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { Service } from '../../types';
|
||||
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export const ServiceActionsMenu = ({ service }: { service: Service }) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id={`service-actions-menu-${service.id}`}
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
style: { maxHeight: 48 * 5.5 },
|
||||
}}
|
||||
>
|
||||
<MenuItem key="details" onClick={handleCloseMenu}>
|
||||
<Typography variant="inherit" noWrap>
|
||||
<Link to={ilertApi.getServiceDetailsURL(service)}>
|
||||
View in iLert
|
||||
</Link>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2021 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 { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { Service } from '../../types';
|
||||
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
link: {
|
||||
lineHeight: '22px',
|
||||
},
|
||||
});
|
||||
|
||||
export const ServiceLink = ({ service }: { service: Service | null }) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
if (!service) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={classes.link} to={ilertApi.getServiceDetailsURL(service)}>
|
||||
#{service.id}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2021 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 { alertApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import React from 'react';
|
||||
import { ilertApiRef } from '../../api';
|
||||
import { useNewService } from '../../hooks/useNewService';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
width: '100%',
|
||||
},
|
||||
option: {
|
||||
fontSize: 15,
|
||||
'& > span': {
|
||||
marginRight: 10,
|
||||
fontSize: 18,
|
||||
},
|
||||
},
|
||||
optionWrapper: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
},
|
||||
sourceImage: {
|
||||
height: 22,
|
||||
paddingRight: 4,
|
||||
},
|
||||
}));
|
||||
|
||||
export const ServiceNewModal = ({
|
||||
isModalOpened,
|
||||
setIsModalOpened,
|
||||
refetchServices,
|
||||
}: {
|
||||
isModalOpened: boolean;
|
||||
setIsModalOpened: (open: boolean) => void;
|
||||
refetchServices: () => void;
|
||||
}) => {
|
||||
const [{ name, isLoading }, { setName, setIsLoading }] = useNewService();
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsModalOpened(false);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await ilertApi.createService({
|
||||
name,
|
||||
});
|
||||
alertApi.post({ message: 'Service created.' });
|
||||
refetchServices();
|
||||
} catch (err) {
|
||||
alertApi.post({ message: err, severity: 'error' });
|
||||
}
|
||||
setIsModalOpened(false);
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const canCreate = !!name;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isModalOpened}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="create-service-form-title"
|
||||
>
|
||||
<DialogTitle id="create-service-form-title">'New alert'</DialogTitle>
|
||||
<DialogContent>
|
||||
{/* <Alert severity="info">
|
||||
<Typography variant="body1" gutterBottom 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>
|
||||
</Alert> */}
|
||||
<TextField
|
||||
disabled={isLoading}
|
||||
label="Name"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
classes={{
|
||||
root: classes.formControl,
|
||||
}}
|
||||
value={name}
|
||||
onChange={event => {
|
||||
setName(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={!canCreate}
|
||||
onClick={handleCreate}
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2021 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 { StatusError, StatusOK } from '@backstage/core-components';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import React from 'react';
|
||||
import {
|
||||
DEGRADED,
|
||||
MAJOR_OUTAGE,
|
||||
OPERATIONAL,
|
||||
PARTIAL_OUTAGE,
|
||||
Service,
|
||||
UNDER_MAINTENANCE,
|
||||
} from '../../types';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
denseListIcon: {
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceStatusLabels = {
|
||||
[OPERATIONAL]: 'Operational',
|
||||
[UNDER_MAINTENANCE]: 'Under maintenance',
|
||||
[DEGRADED]: 'Degraded',
|
||||
[PARTIAL_OUTAGE]: 'Partial outage',
|
||||
[MAJOR_OUTAGE]: 'Major outage',
|
||||
} as Record<string, string>;
|
||||
|
||||
export const ServiceStatus = ({ service }: { service: Service }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Tooltip title={service.status} placement="top">
|
||||
<div className={classes.denseListIcon}>
|
||||
{service.status === 'OPERATIONAL' ? <StatusError /> : <StatusOK />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 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 * from './ServiceActionsMenu';
|
||||
export * from './ServiceStatus';
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
Content,
|
||||
ContentHeader,
|
||||
ResponseErrorPanel,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import React from 'react';
|
||||
import { useServices } from '../../hooks/useServices';
|
||||
import { MissingAuthorizationHeaderError } from '../Errors';
|
||||
import { ServiceNewModal } from '../Service/ServiceNewModal';
|
||||
import { ServicesTable } from './ServicesTable';
|
||||
|
||||
export const ServicesPage = () => {
|
||||
const [
|
||||
{ tableState, services, isLoading, error },
|
||||
{ onChangePage, onChangeRowsPerPage, refetchServices, setIsLoading },
|
||||
] = useServices(true);
|
||||
|
||||
const [isModalOpened, setIsModalOpened] = React.useState(false);
|
||||
|
||||
const handleCreateNewServiceClick = () => {
|
||||
setIsModalOpened(true);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return (
|
||||
<Content>
|
||||
<MissingAuthorizationHeaderError />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<ContentHeader title="Services">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleCreateNewServiceClick}
|
||||
>
|
||||
Create Service
|
||||
</Button>
|
||||
<ServiceNewModal
|
||||
isModalOpened={isModalOpened}
|
||||
setIsModalOpened={setIsModalOpened}
|
||||
refetchServices={refetchServices}
|
||||
/>
|
||||
<SupportButton>
|
||||
This helps you to bring iLert into your developer portal.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<ServicesTable
|
||||
services={services}
|
||||
tableState={tableState}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2021 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 { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import React from 'react';
|
||||
import { ilertApiRef, TableState } from '../../api';
|
||||
import { Service } from '../../types';
|
||||
import { StatusChip } from './StatusChip';
|
||||
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { ServiceActionsMenu } from '../Service/ServiceActionsMenu';
|
||||
import { ServiceLink } from '../Service/ServiceLink';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
empty: {
|
||||
padding: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export const ServicesTable = ({
|
||||
services,
|
||||
tableState,
|
||||
isLoading,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
compact,
|
||||
}: {
|
||||
services: Service[];
|
||||
tableState: TableState;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
onChangePage: (page: number) => void;
|
||||
onChangeRowsPerPage: (pageSize: number) => void;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const xsColumnStyle = {
|
||||
width: '5%',
|
||||
maxWidth: '5%',
|
||||
};
|
||||
const smColumnStyle = {
|
||||
width: '10%',
|
||||
maxWidth: '10%',
|
||||
};
|
||||
const mdColumnStyle = {
|
||||
width: '15%',
|
||||
maxWidth: '15%',
|
||||
};
|
||||
const lgColumnStyle = {
|
||||
width: '20%',
|
||||
maxWidth: '20%',
|
||||
};
|
||||
const xlColumnStyle = {
|
||||
width: '30%',
|
||||
maxWidth: '30%',
|
||||
};
|
||||
|
||||
const idColumn: TableColumn = {
|
||||
title: 'ID',
|
||||
field: 'id',
|
||||
highlight: true,
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
render: rowData => <ServiceLink service={rowData as Service} />,
|
||||
};
|
||||
const nameColumn: TableColumn = {
|
||||
title: 'Name',
|
||||
field: 'name',
|
||||
cellStyle: !compact ? xlColumnStyle : undefined,
|
||||
headerStyle: !compact ? xlColumnStyle : undefined,
|
||||
render: rowData => <Typography>{(rowData as Service).name}</Typography>,
|
||||
};
|
||||
const statusColumn: TableColumn = {
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
cellStyle: xsColumnStyle,
|
||||
headerStyle: xsColumnStyle,
|
||||
render: rowData => <StatusChip service={rowData as Service} />,
|
||||
};
|
||||
const uptimeColumn: TableColumn = {
|
||||
title: 'Uptime in the last 90 days',
|
||||
field: 'uptimePercentage',
|
||||
cellStyle: smColumnStyle,
|
||||
headerStyle: smColumnStyle,
|
||||
render: rowData => (
|
||||
<Typography>
|
||||
{(rowData as Service).uptime.uptimePercentage.p90}
|
||||
</Typography>
|
||||
),
|
||||
};
|
||||
const actionsColumn: TableColumn = {
|
||||
title: '',
|
||||
field: '',
|
||||
cellStyle: xsColumnStyle,
|
||||
headerStyle: xsColumnStyle,
|
||||
render: rowData => <ServiceActionsMenu service={rowData as Service} />,
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = compact
|
||||
? [nameColumn, statusColumn, uptimeColumn, actionsColumn]
|
||||
: [idColumn, nameColumn, statusColumn, uptimeColumn, actionsColumn];
|
||||
let tableStyle: React.CSSProperties = {};
|
||||
if (compact) {
|
||||
tableStyle = {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
minWidth: '0',
|
||||
height: 'calc(100% - 10px)',
|
||||
boxShadow: 'none !important',
|
||||
borderRadius: 'none !important',
|
||||
};
|
||||
} else {
|
||||
tableStyle = {
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
style={tableStyle}
|
||||
options={{
|
||||
sorting: false,
|
||||
search: !compact,
|
||||
paging: !compact,
|
||||
actionsColumnIndex: -1,
|
||||
pageSize: tableState.pageSize,
|
||||
pageSizeOptions: !compact ? [10, 20, 50, 100] : [3, 10, 20, 50, 100],
|
||||
padding: 'dense',
|
||||
loadingType: 'overlay',
|
||||
showEmptyDataSourceMessage: !isLoading,
|
||||
showTitle: true,
|
||||
toolbar: true,
|
||||
}}
|
||||
emptyContent={
|
||||
<Typography color="textSecondary" className={classes.empty}>
|
||||
No services
|
||||
</Typography>
|
||||
}
|
||||
title={
|
||||
<Typography variant="button" color="textSecondary">
|
||||
SERVICES
|
||||
</Typography>
|
||||
}
|
||||
page={tableState.page}
|
||||
onPageChange={onChangePage}
|
||||
onRowsPerPageChange={onChangeRowsPerPage}
|
||||
// localization={{ header: { actions: undefined } }}
|
||||
columns={columns}
|
||||
data={services}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Chip, withStyles } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import {
|
||||
DEGRADED,
|
||||
MAJOR_OUTAGE,
|
||||
OPERATIONAL,
|
||||
PARTIAL_OUTAGE,
|
||||
Service,
|
||||
UNDER_MAINTENANCE,
|
||||
} from '../../types';
|
||||
import { serviceStatusLabels } from '../Service/ServiceStatus';
|
||||
|
||||
const OperationalChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#4caf50',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
const UnderMaintenanceChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#ffb74d',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
const DegradedChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#d32f2f',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
const PartialOutageChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#d4a5bb',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
const MajorOutageChip = withStyles({
|
||||
root: {
|
||||
backgroundColor: '#28c548',
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
},
|
||||
})(Chip);
|
||||
|
||||
export const StatusChip = ({ service }: { service: Service }) => {
|
||||
const label = `${serviceStatusLabels[service.status]}`;
|
||||
|
||||
switch (service.status) {
|
||||
case OPERATIONAL:
|
||||
return <OperationalChip label={label} size="small" />;
|
||||
case UNDER_MAINTENANCE:
|
||||
return <UnderMaintenanceChip label={label} size="small" />;
|
||||
case DEGRADED:
|
||||
return <DegradedChip label={label} size="small" />;
|
||||
case PARTIAL_OUTAGE:
|
||||
return <PartialOutageChip label={label} size="small" />;
|
||||
case MAJOR_OUTAGE:
|
||||
return <MajorOutageChip label={label} size="small" />;
|
||||
default:
|
||||
return <Chip label={label} size="small" />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2021 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 Checkbox from '@material-ui/core/Checkbox';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import React from 'react';
|
||||
import { ACCEPTED, AlertStatus, PENDING, RESOLVED } from '../../types';
|
||||
import { alertStatusLabels } from '../Alert/AlertStatus';
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
label: {
|
||||
marginTop: 8,
|
||||
marginRight: 4,
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 120,
|
||||
maxWidth: 300,
|
||||
},
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export const TableTitle = ({
|
||||
alertStates,
|
||||
onAlertStatesChange,
|
||||
}: {
|
||||
alertStates: AlertStatus[];
|
||||
onAlertStatesChange: (states: AlertStatus[]) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const handleAlertStatusSelectChange = (event: any) => {
|
||||
onAlertStatesChange(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography noWrap className={classes.label}>
|
||||
Status:
|
||||
</Typography>
|
||||
<FormControl
|
||||
className={classes.formControl}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
<Select
|
||||
id="alerts-status-select"
|
||||
multiple
|
||||
value={alertStates}
|
||||
onChange={handleAlertStatusSelectChange}
|
||||
renderValue={(selected: any) => selected.join(', ')}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{[PENDING, ACCEPTED, RESOLVED].map(state => (
|
||||
<MenuItem key={state} value={state}>
|
||||
<Checkbox
|
||||
checked={alertStates.indexOf(state as AlertStatus) > -1}
|
||||
/>
|
||||
<ListItemText primary={alertStatusLabels[state]} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 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 * from './ServicesPage';
|
||||
export * from './ServicesTable';
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2021 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';
|
||||
|
||||
export const useNewService = () => {
|
||||
const [name, setName] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
return [
|
||||
{
|
||||
name,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setName,
|
||||
setIsLoading,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2021 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 { errorApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import React from 'react';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { GetServicesOpts, ilertApiRef, TableState } from '../api';
|
||||
import { Service } from '../types';
|
||||
|
||||
export const useServices = (paging: boolean) => {
|
||||
const ilertApi = useApi(ilertApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [tableState, setTableState] = React.useState<TableState>({
|
||||
page: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const [servicesList, setServicesList] = React.useState<Service[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const fetchServicesCall = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const opts: GetServicesOpts = {};
|
||||
if (paging) {
|
||||
opts.maxResults = tableState.pageSize;
|
||||
opts.startIndex = tableState.page * tableState.pageSize;
|
||||
}
|
||||
const data = await ilertApi.fetchServices(opts);
|
||||
setServicesList(data || []);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
if (!(e instanceof AuthenticationError)) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchServices = useAsyncRetry(fetchServicesCall, [tableState]);
|
||||
|
||||
const refetchServices = () => {
|
||||
setTableState({ ...tableState, page: 0 });
|
||||
Promise.all([fetchServicesCall()]);
|
||||
};
|
||||
|
||||
const error = fetchServices.error;
|
||||
const retry = () => {
|
||||
fetchServices.retry();
|
||||
};
|
||||
|
||||
const onChangePage = (page: number) => {
|
||||
setTableState({ ...tableState, page });
|
||||
};
|
||||
const onChangeRowsPerPage = (p: number) => {
|
||||
setTableState({ ...tableState, pageSize: p });
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
tableState,
|
||||
services: servicesList,
|
||||
isLoading,
|
||||
error,
|
||||
},
|
||||
{
|
||||
setTableState,
|
||||
setServicesList,
|
||||
setIsLoading,
|
||||
retry,
|
||||
refetchServices,
|
||||
onChangePage,
|
||||
onChangeRowsPerPage,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
@@ -26,6 +26,7 @@ export interface Alert {
|
||||
alertKey: string;
|
||||
alertSource: AlertSource | null;
|
||||
assignedTo: User | null;
|
||||
responders: Responder[];
|
||||
logEntries: LogEntry[];
|
||||
links: Link[];
|
||||
images: Image[];
|
||||
@@ -99,6 +100,13 @@ export interface User {
|
||||
department: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Responder {
|
||||
acceptedAt?: string;
|
||||
status: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type UserRole =
|
||||
| 'USER'
|
||||
@@ -223,6 +231,26 @@ export type AlertSourceAlertPriorityRule =
|
||||
| 'LOW'
|
||||
| 'HIGH_DURING_SUPPORT_HOURS'
|
||||
| 'LOW_DURING_SUPPORT_HOURS';
|
||||
|
||||
/** @public */
|
||||
export const OPERATIONAL = 'OPERATIONAL';
|
||||
/** @public */
|
||||
export const UNDER_MAINTENANCE = 'UNDER_MAINTENANCE';
|
||||
/** @public */
|
||||
export const DEGRADED = 'DEGRADED';
|
||||
/** @public */
|
||||
export const PARTIAL_OUTAGE = 'PARTIAL_OUTAGE';
|
||||
/** @public */
|
||||
export const MAJOR_OUTAGE = 'MAJOR_OUTAGE';
|
||||
|
||||
/** @public */
|
||||
export type ServiceStatus =
|
||||
| typeof OPERATIONAL
|
||||
| typeof UNDER_MAINTENANCE
|
||||
| typeof DEGRADED
|
||||
| typeof PARTIAL_OUTAGE
|
||||
| typeof MAJOR_OUTAGE;
|
||||
|
||||
/** @public */
|
||||
export interface AlertSourceEmailPredicate {
|
||||
field: 'EMAIL_FROM' | 'EMAIL_SUBJECT' | 'EMAIL_BODY';
|
||||
@@ -376,3 +404,21 @@ export interface OnCall {
|
||||
end: string;
|
||||
escalationLevel: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Service {
|
||||
id: number;
|
||||
name: string;
|
||||
status: ServiceStatus;
|
||||
uptime: Uptime;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Uptime {
|
||||
uptimePercentage: UptimePercentage;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface UptimePercentage {
|
||||
p90: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user