From bfd8e9282dc439cf405cc76b4abad48b5660dcf7 Mon Sep 17 00:00:00 2001 From: Marko Simon Date: Mon, 3 Oct 2022 13:26:30 +0200 Subject: [PATCH] add services Signed-off-by: Marko Simon --- plugins/ilert/src/api/client.ts | 50 ++++- plugins/ilert/src/api/index.ts | 3 +- plugins/ilert/src/api/types.ts | 16 ++ .../src/components/AlertsPage/AlertsTable.tsx | 21 ++- .../src/components/ILertPage/ILertPage.tsx | 4 + .../components/Service/ServiceActionsMenu.tsx | 68 +++++++ .../src/components/Service/ServiceLink.tsx | 43 +++++ .../components/Service/ServiceNewModal.tsx | 136 ++++++++++++++ .../src/components/Service/ServiceStatus.tsx | 57 ++++++ plugins/ilert/src/components/Service/index.ts | 17 ++ .../components/ServicesPage/ServicesPage.tsx | 90 +++++++++ .../components/ServicesPage/ServicesTable.tsx | 172 ++++++++++++++++++ .../components/ServicesPage/StatusChip.tsx | 82 +++++++++ .../components/ServicesPage/TableTitle.tsx | 97 ++++++++++ .../src/components/ServicesPage/index.ts | 17 ++ plugins/ilert/src/hooks/useNewService.ts | 32 ++++ plugins/ilert/src/hooks/useServices.ts | 91 +++++++++ plugins/ilert/src/types.ts | 46 +++++ 18 files changed, 1031 insertions(+), 11 deletions(-) create mode 100644 plugins/ilert/src/components/Service/ServiceActionsMenu.tsx create mode 100644 plugins/ilert/src/components/Service/ServiceLink.tsx create mode 100644 plugins/ilert/src/components/Service/ServiceNewModal.tsx create mode 100644 plugins/ilert/src/components/Service/ServiceStatus.tsx create mode 100644 plugins/ilert/src/components/Service/index.ts create mode 100644 plugins/ilert/src/components/ServicesPage/ServicesPage.tsx create mode 100644 plugins/ilert/src/components/ServicesPage/ServicesTable.tsx create mode 100644 plugins/ilert/src/components/ServicesPage/StatusChip.tsx create mode 100644 plugins/ilert/src/components/ServicesPage/TableTitle.tsx create mode 100644 plugins/ilert/src/components/ServicesPage/index.ts create mode 100644 plugins/ilert/src/hooks/useNewService.ts create mode 100644 plugins/ilert/src/hooks/useServices.ts diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index 9b8a9f636e..4269ef583d 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -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 { + 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 { + 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 || ''; } diff --git a/plugins/ilert/src/api/index.ts b/plugins/ilert/src/api/index.ts index b33a702a5c..f0688e0f63 100644 --- a/plugins/ilert/src/api/index.ts +++ b/plugins/ilert/src/api/index.ts @@ -16,9 +16,10 @@ export { ilertApiRef, ILertClient } from './client'; export type { - EventRequest, + AlertEventRequest as EventRequest, GetAlertsCountOpts, GetAlertsOpts, + GetServicesOpts, ILertApi, TableState, } from './types'; diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index b7af3d845d..39eb373ae2 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -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; @@ -94,11 +106,15 @@ export interface ILertApi { end: string, ): Promise; + fetchServices(opts?: GetServicesOpts): Promise; + createService(eventRequest: ServiceRequest): Promise; + 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; } diff --git a/plugins/ilert/src/components/AlertsPage/AlertsTable.tsx b/plugins/ilert/src/components/AlertsPage/AlertsTable.tsx index cb3a6b886a..38ef4f07b1 100644 --- a/plugins/ilert/src/components/AlertsPage/AlertsTable.tsx +++ b/plugins/ilert/src/components/AlertsPage/AlertsTable.tsx @@ -140,14 +140,19 @@ export const AlertsTable = ({ ), }; - 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 => ( - - {ilertApi.getUserInitials((rowData as Alert).assignedTo)} + + {(rowData as Alert).responders.map((value, i, arr) => { + return ( + ilertApi.getUserInitials(value.user) + + (arr.length - 1 !== i ? ', ' : '') + ); + })} ), }; @@ -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 = ({ /> ) : ( - INCIDENTS + ALERTS ) } diff --git a/plugins/ilert/src/components/ILertPage/ILertPage.tsx b/plugins/ilert/src/components/ILertPage/ILertPage.tsx index 8dfed11690..cc7527c316 100644 --- a/plugins/ilert/src/components/ILertPage/ILertPage.tsx +++ b/plugins/ilert/src/components/ILertPage/ILertPage.tsx @@ -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 ; case 2: return ; + case 3: + return ; default: return null; } diff --git a/plugins/ilert/src/components/Service/ServiceActionsMenu.tsx b/plugins/ilert/src/components/Service/ServiceActionsMenu.tsx new file mode 100644 index 0000000000..096cc391ee --- /dev/null +++ b/plugins/ilert/src/components/Service/ServiceActionsMenu.tsx @@ -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); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + return ( + <> + + + + + + + + View in iLert + + + + + + ); +}; diff --git a/plugins/ilert/src/components/Service/ServiceLink.tsx b/plugins/ilert/src/components/Service/ServiceLink.tsx new file mode 100644 index 0000000000..f8b1e88531 --- /dev/null +++ b/plugins/ilert/src/components/Service/ServiceLink.tsx @@ -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 ( + + #{service.id} + + ); +}; diff --git a/plugins/ilert/src/components/Service/ServiceNewModal.tsx b/plugins/ilert/src/components/Service/ServiceNewModal.tsx new file mode 100644 index 0000000000..64823eaf46 --- /dev/null +++ b/plugins/ilert/src/components/Service/ServiceNewModal.tsx @@ -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 ( + + 'New alert' + + {/* + + 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. + + */} + { + setName(event.target.value); + }} + /> + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/Service/ServiceStatus.tsx b/plugins/ilert/src/components/Service/ServiceStatus.tsx new file mode 100644 index 0000000000..05923058aa --- /dev/null +++ b/plugins/ilert/src/components/Service/ServiceStatus.tsx @@ -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; + +export const ServiceStatus = ({ service }: { service: Service }) => { + const classes = useStyles(); + + return ( + +
+ {service.status === 'OPERATIONAL' ? : } +
+
+ ); +}; diff --git a/plugins/ilert/src/components/Service/index.ts b/plugins/ilert/src/components/Service/index.ts new file mode 100644 index 0000000000..5227f44573 --- /dev/null +++ b/plugins/ilert/src/components/Service/index.ts @@ -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'; diff --git a/plugins/ilert/src/components/ServicesPage/ServicesPage.tsx b/plugins/ilert/src/components/ServicesPage/ServicesPage.tsx new file mode 100644 index 0000000000..73b2d92f3c --- /dev/null +++ b/plugins/ilert/src/components/ServicesPage/ServicesPage.tsx @@ -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 ( + + + + ); + } + + return ( + + + + ); + } + + return ( + + + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx b/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx new file mode 100644 index 0000000000..bfb20acd71 --- /dev/null +++ b/plugins/ilert/src/components/ServicesPage/ServicesTable.tsx @@ -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 => , + }; + const nameColumn: TableColumn = { + title: 'Name', + field: 'name', + cellStyle: !compact ? xlColumnStyle : undefined, + headerStyle: !compact ? xlColumnStyle : undefined, + render: rowData => {(rowData as Service).name}, + }; + const statusColumn: TableColumn = { + title: 'Status', + field: 'status', + cellStyle: xsColumnStyle, + headerStyle: xsColumnStyle, + render: rowData => , + }; + const uptimeColumn: TableColumn = { + title: 'Uptime in the last 90 days', + field: 'uptimePercentage', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + {(rowData as Service).uptime.uptimePercentage.p90} + + ), + }; + const actionsColumn: TableColumn = { + title: '', + field: '', + cellStyle: xsColumnStyle, + headerStyle: xsColumnStyle, + render: rowData => , + }; + + 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 ( + + No services + + } + title={ + + SERVICES + + } + page={tableState.page} + onPageChange={onChangePage} + onRowsPerPageChange={onChangeRowsPerPage} + // localization={{ header: { actions: undefined } }} + columns={columns} + data={services} + isLoading={isLoading} + /> + ); +}; diff --git a/plugins/ilert/src/components/ServicesPage/StatusChip.tsx b/plugins/ilert/src/components/ServicesPage/StatusChip.tsx new file mode 100644 index 0000000000..5eb31b8a94 --- /dev/null +++ b/plugins/ilert/src/components/ServicesPage/StatusChip.tsx @@ -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 ; + case UNDER_MAINTENANCE: + return ; + case DEGRADED: + return ; + case PARTIAL_OUTAGE: + return ; + case MAJOR_OUTAGE: + return ; + default: + return ; + } +}; diff --git a/plugins/ilert/src/components/ServicesPage/TableTitle.tsx b/plugins/ilert/src/components/ServicesPage/TableTitle.tsx new file mode 100644 index 0000000000..c610e91d94 --- /dev/null +++ b/plugins/ilert/src/components/ServicesPage/TableTitle.tsx @@ -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 ( +
+ + Status: + + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/ServicesPage/index.ts b/plugins/ilert/src/components/ServicesPage/index.ts new file mode 100644 index 0000000000..6c4eef8100 --- /dev/null +++ b/plugins/ilert/src/components/ServicesPage/index.ts @@ -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'; diff --git a/plugins/ilert/src/hooks/useNewService.ts b/plugins/ilert/src/hooks/useNewService.ts new file mode 100644 index 0000000000..7d50d29a39 --- /dev/null +++ b/plugins/ilert/src/hooks/useNewService.ts @@ -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; +}; diff --git a/plugins/ilert/src/hooks/useServices.ts b/plugins/ilert/src/hooks/useServices.ts new file mode 100644 index 0000000000..b6ea120f80 --- /dev/null +++ b/plugins/ilert/src/hooks/useServices.ts @@ -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({ + page: 0, + pageSize: 10, + }); + + const [servicesList, setServicesList] = React.useState([]); + 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; +}; diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts index 3e5ecf94c4..ebe5c1b39d 100644 --- a/plugins/ilert/src/types.ts +++ b/plugins/ilert/src/types.ts @@ -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; +}