add services

Signed-off-by: Marko Simon <marko@ilert.com>
This commit is contained in:
Marko Simon
2022-10-03 13:26:30 +02:00
parent f41d2d69a6
commit bfd8e9282d
18 changed files with 1031 additions and 11 deletions
+48 -2
View File
@@ -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 || '';
}
+2 -1
View File
@@ -16,9 +16,10 @@
export { ilertApiRef, ILertClient } from './client';
export type {
EventRequest,
AlertEventRequest as EventRequest,
GetAlertsCountOpts,
GetAlertsOpts,
GetServicesOpts,
ILertApi,
TableState,
} from './types';
+16
View File
@@ -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';
+32
View File
@@ -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;
};
+91
View File
@@ -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;
};
+46
View File
@@ -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;
}