From e6a53d79f0c8b0369c82613d27c73c27283af8eb Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Thu, 29 Jul 2021 12:05:37 -0400 Subject: [PATCH] add FireHydrant plugin Signed-off-by: Christine Yi --- plugins/firehydrant/.eslintrc.js | 3 + plugins/firehydrant/README.md | 66 +++++ plugins/firehydrant/api-report.md | 23 ++ plugins/firehydrant/dev/index.tsx | 19 ++ plugins/firehydrant/doc/firehydrant_logo.png | Bin 0 -> 5387 bytes .../doc/firehydrant_plugin_screenshot.png | Bin 0 -> 57062 bytes plugins/firehydrant/package.json | 67 +++++ plugins/firehydrant/src/api/index.ts | 137 +++++++++ plugins/firehydrant/src/api/types.ts | 33 +++ .../ServiceAnalytics.test.tsx | 42 +++ .../ServiceAnalytics/ServiceAnalytics.tsx | 164 ++++++++++ .../ServiceDetailsCard.test.tsx | 118 ++++++++ .../ServiceDetailsCard/ServiceDetailsCard.tsx | 279 ++++++++++++++++++ .../components/ServiceDetailsCard/index.ts | 16 + .../src/components/serviceAnalytics.ts | 51 ++++ .../src/components/serviceDetails.ts | 39 +++ plugins/firehydrant/src/components/types.ts | 29 ++ plugins/firehydrant/src/index.ts | 16 + plugins/firehydrant/src/plugin.test.ts | 22 ++ plugins/firehydrant/src/plugin.ts | 46 +++ plugins/firehydrant/src/routes.ts | 20 ++ plugins/firehydrant/src/setupTests.ts | 17 ++ 22 files changed, 1207 insertions(+) create mode 100644 plugins/firehydrant/.eslintrc.js create mode 100644 plugins/firehydrant/README.md create mode 100644 plugins/firehydrant/api-report.md create mode 100644 plugins/firehydrant/dev/index.tsx create mode 100644 plugins/firehydrant/doc/firehydrant_logo.png create mode 100644 plugins/firehydrant/doc/firehydrant_plugin_screenshot.png create mode 100644 plugins/firehydrant/package.json create mode 100644 plugins/firehydrant/src/api/index.ts create mode 100644 plugins/firehydrant/src/api/types.ts create mode 100644 plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx create mode 100644 plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx create mode 100644 plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx create mode 100644 plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx create mode 100644 plugins/firehydrant/src/components/ServiceDetailsCard/index.ts create mode 100644 plugins/firehydrant/src/components/serviceAnalytics.ts create mode 100644 plugins/firehydrant/src/components/serviceDetails.ts create mode 100644 plugins/firehydrant/src/components/types.ts create mode 100644 plugins/firehydrant/src/index.ts create mode 100644 plugins/firehydrant/src/plugin.test.ts create mode 100644 plugins/firehydrant/src/plugin.ts create mode 100644 plugins/firehydrant/src/routes.ts create mode 100644 plugins/firehydrant/src/setupTests.ts diff --git a/plugins/firehydrant/.eslintrc.js b/plugins/firehydrant/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/firehydrant/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/firehydrant/README.md b/plugins/firehydrant/README.md new file mode 100644 index 0000000000..98f36eb4c3 --- /dev/null +++ b/plugins/firehydrant/README.md @@ -0,0 +1,66 @@ +# FireHydrant Plugin + +## Overview + +The [FireHydrant](https://firehydrant.io) plugin brings incident management to Backstage, and it displays service incidents information such as active incidents and incident analytics. There are also quick action links that let you create and view incidents in FireHydrant. + +firehydrant plugin screenshot + +## Features + +- View total active incidents for a service declared in FireHydrant +- Quick links to the top 5 most recent active incidents in FireHydrant +- View incident metrics for a given service within the last 30 days, including: healthiness, total time impacted, total number of incidents, and MTT* (Mean Time To *) data such as MTTD (detect), MTTA (acknowledge), MTTM (mitigate) and MTTR (resolve). + +## Setup + +1. Install the plugin: + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-firehydrant +``` + +2. Add the plugin to `EntityPage.tsx`, inside the `const overviewContent`'s parent `` component: + +```ts +// In packages/app/src/components/catalog/EntityPage.tsx +import { FirehydrantPage } from '@backstage/plugin-firehydrant'; + +// Add to code as a grid item + + +; +``` + +3. Add proxy configuration to `app-config.yaml`: + +```yaml +proxy: +'/firehydrant/api': + target: 'https://api.firehydrant.io/v1/' + changeOrigin: true + headers: + # Supply the token you generated from https://app.firehydrant.io/organizations/bots + Authorization: Bearer ${FIREHYDRANT_BOT_TOKEN} +``` + +as well as the default url configuration to `app-config.yaml`: + +```yaml +firehydrant: + baseUrl: https://api.firehydrant.io +``` + +Note: if you are not using environment variables, you can directly type the API Bot Token into `app-config.yaml`: + +```yaml +proxy: +'/firehydrant/api': + target: 'https://api.firehydrant.io/v1/' + changeOrigin: true + headers: + # Supply the token you generated from https://app.firehydrant.io/organizations/bots + Authorization: Bearer fhb-e4911b22bcd788c4a4afeb0c111ffbfa +``` diff --git a/plugins/firehydrant/api-report.md b/plugins/firehydrant/api-report.md new file mode 100644 index 0000000000..d12c2499c9 --- /dev/null +++ b/plugins/firehydrant/api-report.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/plugin-firehydrant" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const FirehydrantPage: () => JSX.Element; + +// @public (undocumented) +export const firehydrantPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/firehydrant/dev/index.tsx b/plugins/firehydrant/dev/index.tsx new file mode 100644 index 0000000000..237cb2737e --- /dev/null +++ b/plugins/firehydrant/dev/index.tsx @@ -0,0 +1,19 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { firehydrantPlugin } from '../src/plugin'; + +createDevApp().registerPlugin(firehydrantPlugin).render(); diff --git a/plugins/firehydrant/doc/firehydrant_logo.png b/plugins/firehydrant/doc/firehydrant_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3db13826348372b0847b9a891ea855c15c1a57c4 GIT binary patch literal 5387 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 literal 0 HcmV?d00001 diff --git a/plugins/firehydrant/doc/firehydrant_plugin_screenshot.png b/plugins/firehydrant/doc/firehydrant_plugin_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e79bb27339f22a0e199f0809dd1d1d53ff5d2d GIT binary patch literal 57062 zcmb@tWl)ga_XUciw35;wNOyNgcY}0ycc%i%wq{y*Gt z81cz-&U5zOYpuNuRge=$M!-dYfPg@jln_yZfOvBWesbQzf+O$Wu@0HW^= zekCWA8YHoDJYDGNjp5&J3PyWj2sqX^Z$3bhhtObFcXoY!^O5C3Z|jKwLUx3Mj>6~Z z;pK&znzt<;5hDJ(J+u74ut)fF%a*cePWdh?>5LN z*i{Z4^FqV4~iNBz$li8sDM_|Q

gH2c{@qG!3Q5{r*IOL2 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

7^9j0^lG5-`K>j@po`bNMRLvBJ+a3 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 + + getServiceDetails(options: { + serviceName: string; + }): Promise; + + getServiceIncidents(options: { + serviceId: string; + }): Promise; +} + +export const fireHydrantApiRef = createApiRef({ + id: 'plugin.firehydrant.service', + description: 'Used by FireHydrant plugin for requests', +}); + +export type Options = { + discoveryApi: DiscoveryApi; + proxyPath?: string; +}; + +const DEFAULT_PROXY_PATH = '/firehydrant/api'; + +export class FireHydrantAPIClient implements FireHydrantAPI { + private readonly discoveryApi: DiscoveryApi; + private readonly proxyPath: string; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + this.proxyPath = options.proxyPath ?? DEFAULT_PROXY_PATH; + } + + async getServiceAnalytics(options: { + serviceId: string; + startDate: string; + endDate: string; + }): Promise { + const proxyUrl = await this.getApiUrl(); + const response = await fetch( + `${proxyUrl}/metrics/services/${options.serviceId}?start_date=${options.startDate}&end_date=${options.endDate}`, + ); + if (!response.ok) { + throw new Error( + `There was a problem fetching FireHydrant analytics data: ${response.statusText}`, + ); + } + const json = await response.json(); + return json; + } + + async getServiceDetails(options: { + serviceName: string; + }): Promise { + const proxyUrl = await this.getApiUrl(); + const response = await fetch( + `${proxyUrl}/services?query=${options.serviceName}`, + ); + + if (!response.ok) { + throw new Error( + `There was a problem fetching FireHydrant data: ${response.statusText}`, + ); + } + + const json = await response.json(); + + const servicesData: ServiceDetailsResponse = { + service: {} as Service, + incidents: [] as Incident[], + }; + + if (response.ok) { + if (json.data?.length === 0) { + return servicesData; + } + + servicesData.service = json.data[0]; + + const incidentsJson = await this.getServiceIncidents({ + serviceId: json.data[0].id, + }); + + servicesData.incidents = incidentsJson; + } + return servicesData; + } + + async getServiceIncidents(options: { + serviceId: string; + }): Promise { + const proxyUrl = await this.getApiUrl(); + const response = await fetch( + `${proxyUrl}/incidents?services=${options.serviceId}&active=true`, + ); + if (!response.ok) { + throw new Error( + `There was a problem fetching FireHydrant incidents data: ${response.statusText}`, + ); + } + + const json = await response.json(); + return json.data; + } + + private async getApiUrl() { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return proxyUrl + this.proxyPath; + } +} diff --git a/plugins/firehydrant/src/api/types.ts b/plugins/firehydrant/src/api/types.ts new file mode 100644 index 0000000000..089c850c97 --- /dev/null +++ b/plugins/firehydrant/src/api/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { Incident, Service } from '../components/types'; + +export type ServiceDetailsResponse = { + service?: Service; + incidents?: ServiceIncidentsResponse; +}; + +export type ServiceAnalyticsResponse = { + id: string; + mttd?: number; + mtta?: number; + mttm?: number; + mttr?: number; + count: number; + total_time: number; +}; + +export type ServiceIncidentsResponse = Incident[]; diff --git a/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx new file mode 100644 index 0000000000..82bbac57c1 --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx @@ -0,0 +1,42 @@ +/* + * 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'; +import { screen } from '@testing-library/react'; +import { ServiceAnalytics } from './ServiceAnalytics'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('ServiceAnalytics', () => { + it('renders service analytics', async () => { + await renderInTestApp( + , + ); + + expect(await screen.findByText(/32/)).toBeInTheDocument(); + expect(await screen.findByText(/44/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx new file mode 100644 index 0000000000..996d705a9a --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx @@ -0,0 +1,164 @@ +/* + * 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'; +import { makeStyles } from '@material-ui/core/styles'; +import moment, { Moment } from 'moment'; +import Alert from '@material-ui/lab/Alert'; +import { Table, TableColumn, Progress } from '@backstage/core-components'; + +const useStyles = makeStyles({ + container: { + overflow: 'auto', + width: '100%', + '& h5': { + fontSize: '18px !important', + }, + '& td': { + minWidth: '145px', + }, + '& th': { + minWidth: '145px', + }, + }, +}); + +export const DenseTable = ({ + service, + startDate, + endDate, +}: { + service: object; + startDate: Moment; + endDate: Moment; +}) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { field: 'healthiness', title: 'Healthiness' }, + { field: 'impacted', title: 'Impacted' }, + { field: 'incidents', title: 'Incidents' }, + { field: 'mttd', title: 'MTTD' }, + { field: 'mtta', title: 'MTTA' }, + { field: 'mttm', title: 'MTTM' }, + { field: 'mttr', title: 'MTTR' }, + ]; + + return ( +

+ + + ); +}; + +export const secondsToDhms = (seconds: number) => { + const secs = Number(seconds); + const d = Math.floor(secs / (60 * 60 * 24)); + const h = Math.floor((secs / (60 * 60)) % 24); + const m = Math.floor((secs / 60) % 60); + const s = secs % 60; + + const dDisplay = d > 0 ? `${d}d ` : ''; + const hDisplay = h > 0 ? `${h}h ` : '00h '; + const mDisplay = m > 0 ? `${m}m ` : '00m '; + const sDisplay = s > 0 ? `${s}s` : '00s'; + return dDisplay + hDisplay + mDisplay + sDisplay; +}; + +export const truncateNum = (num: number) => { + const matcher = `^-?\\d+(?:\\.\\d{0,3})?`; + const re = new RegExp(matcher); + const match = num.toString().match(re); + const result = (match && match[0]) || '0'; // eslint-disable-line no-mixed-operators + return result; +}; + +export const calcHealthiness = ({ + mttm, + incidents, + range, +}: { + mttm: number; + incidents: number; + range: number; +}) => { + const num = (1 - (mttm * incidents) / range) * 100; + return `${truncateNum(num)}%`; +}; + +type AnalyticsDataType = { + id?: string; + count?: number; + mttd?: number; + mtta?: number; + mttm?: number; + mttr?: number; + total_time?: number; +}; + +export const ServiceAnalytics = ({ + value, + loading, + error, +}: { + value: AnalyticsDataType; + loading: boolean; + error: any; +}) => { + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + const startDate = moment().subtract(30, 'days').utc(); + const endDate = moment().utc(); + + // Transform and format the data to display in the table + if (value.id) { + const serviceData = { + healthiness: + value.mttm && value.count + ? calcHealthiness({ + mttm: value.mttm, + incidents: value.count, + range: endDate.diff(startDate, 'seconds'), + }) + : '100%', + impacted: value.total_time ? secondsToDhms(value.total_time) : '-', + incidents: value.count, + mttd: value.mttd ? secondsToDhms(value.mttd) : '-', + mtta: value.mtta ? secondsToDhms(value.mtta) : '-', + mttm: value.mttm ? secondsToDhms(value.mttm) : '-', + mttr: value.mttr ? secondsToDhms(value.mttr) : '-', + }; + + return ( + + ); + } + + return null; +}; diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx new file mode 100644 index 0000000000..1123f014da --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx @@ -0,0 +1,118 @@ +/* + * 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'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { fireHydrantApiRef } from '../../api'; +import { screen } from '@testing-library/react'; +import { ServiceDetailsCard } from './ServiceDetailsCard'; +import { Service, Incident } from '../types'; +import { renderInTestApp } from '@backstage/test-utils'; + +const mockFireHydrantApi = { + getServiceDetails: () => {}, + getServiceAnalytics: () => {}, +}; + +const apis = ApiRegistry.from([[fireHydrantApiRef, mockFireHydrantApi]]); + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntity: () => { + return { entity: { metadata: { name: 'service-example' } } }; + }, +})); + +describe('ServiceDetailsCard', () => { + it('renders service incidents', async () => { + mockFireHydrantApi.getServiceDetails = jest.fn().mockImplementationOnce( + async () => + [ + { + service: { + id: '12345', + name: 'service-example', + description: 'This is a sample description', + active_incidents: ['654321'], + } as Service, + incidents: [ + { + id: '654321', + active: true, + description: 'test incident', + name: 'incident name here', + incident_url: 'http://example.com', + } as Incident, + ], + }, + ][0], + ); + + mockFireHydrantApi.getServiceAnalytics = jest.fn().mockImplementationOnce( + async () => + [ + { + buckets: [ + { + metrics: { + 12345: { + count: 0, + mtta: null, + mttd: null, + mttm: null, + mttr: null, + total_time: null, + }, + }, + }, + ], + }, + ][0], + ); + + await renderInTestApp( + + + , + ); + + expect( + await screen.findByText(/View service incidents/), + ).toBeInTheDocument(); + expect( + await screen.findByText(/There is 1 active incident/), + ).toBeInTheDocument(); + expect(await screen.findByText(/incident name here/)).toBeInTheDocument(); + }); + + it('handles empty response', async () => { + mockFireHydrantApi.getServiceDetails = jest + .fn() + .mockImplementationOnce(async () => []); + + mockFireHydrantApi.getServiceAnalytics = jest + .fn() + .mockImplementationOnce(async () => []); + + await renderInTestApp( + + + , + ); + + expect( + await screen.findByText(/This service does not exist in FireHydrant/), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx new file mode 100644 index 0000000000..7ebbcd8351 --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx @@ -0,0 +1,279 @@ +/* + * 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, { useEffect, useState } from 'react'; +import moment from 'moment'; +import { ServiceAnalytics } from '../ServiceAnalytics/ServiceAnalytics'; +import { + Box, + Button as MaterialButton, + Typography, + makeStyles, +} from '@material-ui/core'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import NotesIcon from '@material-ui/icons/Notes'; +import WhatshotIcon from '@material-ui/icons/Whatshot'; +import WarningIcon from '@material-ui/icons/Warning'; +import AddIcon from '@material-ui/icons/Add'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Incident } from '../types'; +import { ServiceIncidentsResponse } from '../../api/types'; +import { useServiceDetails } from '../serviceDetails'; +import { useServiceAnalytics } from '../serviceAnalytics'; +import { InfoCard, Progress } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +const useStyles = makeStyles(theme => ({ + infoCard: { + height: '342px', + }, + button: { + color: '#3b2492', + display: 'grid', + gridGap: '4px', + textAlign: 'center', + justifyItems: 'center', + width: '102px', + backgroundColor: theme.palette.type === 'dark' ? '#f1edff' : '', + '&:hover, &:focus': { + backgroundColor: '#f1edff', + color: '#614ab6', + }, + '&:active': { + color: '#3b2492', + backgroundColor: '#b2a6e3', + boxShadow: + 'rgb(59, 36, 146) 0px 0px 0px 1px inset, rgb(141, 134, 188) 3px 3px 0px 0px inset;', + }, + border: '1px solid #3b2492', + borderRadius: '5px', + padding: '10px', + }, + buttonLink: { + backgroundColor: '#3b2492', + color: '#FFF', + textTransform: 'none', + '&:hover': { + backgroundColor: '#614ab6', + }, + }, + buttonContainer: { + display: 'grid', + gridGap: '24px', + gridAutoFlow: 'column', + gridAutoColumns: 'min-content', + }, + icon: { + color: '#f1642d', + }, + link: { + textDecoration: 'underline', + fontSize: '16px', + lineHeight: '27px', + color: '#3b2492', + '&:hover, &:focus': { + fontWeight: '500', + }, + }, + linksContainer: { + borderBottom: '1px solid #d5d5d5', + padding: '10px 0px 10px 20px', + backgroundColor: '#f1edff', + marginBottom: '20px', + }, + table: { + width: '100%', + }, + warning: { + display: 'flex', + alignItems: 'center', + padding: '10px', + background: '#f1edff', + color: theme.palette.type === 'dark' ? '#3b2492' : '#fff', + }, +})); + +const ServiceWarning = ({ text }: { text: string }) => { + const classes = useStyles(); + return ( +
+ +   {text} +
+ ); +}; + +const ServiceAnalyticsView = ({ + serviceId, + startDate, + endDate, +}: { + serviceId: string; + startDate: moment.Moment; + endDate: moment.Moment; +}) => { + const { + loading: analyticsLoading, + value: analyticsValue = {}, + error: analyticsError, + } = useServiceAnalytics({ + serviceId, + startDate: startDate.format('YYYY-MM-DD'), + endDate: endDate.format('YYYY-MM-DD'), + }); + + return ( + + ); +}; + +export const ServiceDetailsCard = () => { + const { entity } = useEntity(); + const classes = useStyles(); + const [showServiceDetails, setShowServiceDetails] = useState(false); + const configApi = useApi(configApiRef); + + const BASE_URL = + configApi.getOptionalString('firehydrant.baseUrl') || + 'https://app.firehydrant.io'; + + const startDate = moment().subtract(30, 'days').utc(); + const endDate = moment().utc(); + + const { loading, value, error } = useServiceDetails({ + serviceName: entity?.metadata?.name, + }); + + const activeIncidents: string[] = value?.service?.active_incidents ?? []; + const incidents: ServiceIncidentsResponse = value?.incidents ?? []; + const serviceId: string = value?.service?.id!; + + useEffect(() => { + if (value?.service && Object.keys(value?.service).length > 0) { + setShowServiceDetails(true); + } + }, [value]); + + if (loading) { + return ; + } + + if (error) { + return ( + + ); + } + + const headerText: string = showServiceDetails + ? `There ${activeIncidents?.length === 1 ? 'is' : 'are'} ${ + activeIncidents?.length + } active incident${activeIncidents?.length === 1 ? '' : 's'}.` + : ''; + + const serviceIncidentsLink: string = `${BASE_URL}/incidents?search={"services":[{"label":${JSON.stringify( + value?.service?.name, + )},"value":${JSON.stringify(value?.service?.id)}}]}`; + + return ( + + {!showServiceDetails && !loading && ( + + )} + {showServiceDetails && ( + + +

{headerText}

+
+ + + View service incidents   + + + +
+ )} + {activeIncidents && activeIncidents?.length > 0 && ( + + {incidents && + incidents?.slice(0, 5).map((incident: Incident, index: number) => ( + + ))} + + )} + + View in FireHydrant + + + + Declare an incident + + + + View all incidents + + {showServiceDetails && ( + + + View Service Details + + )} + + + {showServiceDetails && ( + + + + )} +
+ ); +}; diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/index.ts b/plugins/firehydrant/src/components/ServiceDetailsCard/index.ts new file mode 100644 index 0000000000..33e68ecf3b --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { ServiceDetailsCard } from './ServiceDetailsCard'; diff --git a/plugins/firehydrant/src/components/serviceAnalytics.ts b/plugins/firehydrant/src/components/serviceAnalytics.ts new file mode 100644 index 0000000000..ba88af86da --- /dev/null +++ b/plugins/firehydrant/src/components/serviceAnalytics.ts @@ -0,0 +1,51 @@ +/* + * 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 { useAsyncRetry } from 'react-use'; +import { fireHydrantApiRef } from '../api'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useServiceAnalytics = ({ + serviceId, + startDate, + endDate, +}: { + serviceId: string; + startDate: string; + endDate: string; +}) => { + const api = useApi(fireHydrantApiRef); + const errorApi = useApi(errorApiRef); + + const { loading, value, error, retry } = useAsyncRetry(async () => { + try { + return await api.getServiceAnalytics({ + serviceId: serviceId, + startDate: startDate, + endDate: endDate, + }); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }); + + return { + loading, + value, + error, + retry, + }; +}; diff --git a/plugins/firehydrant/src/components/serviceDetails.ts b/plugins/firehydrant/src/components/serviceDetails.ts new file mode 100644 index 0000000000..b8cac2736a --- /dev/null +++ b/plugins/firehydrant/src/components/serviceDetails.ts @@ -0,0 +1,39 @@ +/* + * 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 { useAsyncRetry } from 'react-use'; +import { fireHydrantApiRef } from '../api'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useServiceDetails = ({ serviceName }: { serviceName: string }) => { + const api = useApi(fireHydrantApiRef); + const errorApi = useApi(errorApiRef); + + const { loading, value, error, retry } = useAsyncRetry(async () => { + try { + return await api.getServiceDetails({ serviceName: serviceName }); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }); + + return { + loading, + value, + error, + retry, + }; +}; diff --git a/plugins/firehydrant/src/components/types.ts b/plugins/firehydrant/src/components/types.ts new file mode 100644 index 0000000000..e607b2357d --- /dev/null +++ b/plugins/firehydrant/src/components/types.ts @@ -0,0 +1,29 @@ +/* + * 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 type Service = { + id: string; + name: string; + description?: string; + active_incidents?: string[]; +}; + +export type Incident = { + id: string; + active: boolean; + description?: string; + name: string; + incident_url: string; +}; diff --git a/plugins/firehydrant/src/index.ts b/plugins/firehydrant/src/index.ts new file mode 100644 index 0000000000..3b9b584fb8 --- /dev/null +++ b/plugins/firehydrant/src/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { firehydrantPlugin, FirehydrantPage } from './plugin'; diff --git a/plugins/firehydrant/src/plugin.test.ts b/plugins/firehydrant/src/plugin.test.ts new file mode 100644 index 0000000000..ffde182e64 --- /dev/null +++ b/plugins/firehydrant/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { firehydrantPlugin } from './plugin'; + +describe('firehydrant', () => { + it('should export plugin', () => { + expect(firehydrantPlugin).toBeDefined(); + }); +}); diff --git a/plugins/firehydrant/src/plugin.ts b/plugins/firehydrant/src/plugin.ts new file mode 100644 index 0000000000..e4794ab4ce --- /dev/null +++ b/plugins/firehydrant/src/plugin.ts @@ -0,0 +1,46 @@ +/* + * 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 { fireHydrantApiRef, FireHydrantAPIClient } from './api'; +import { + createApiFactory, + createPlugin, + discoveryApiRef, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const firehydrantPlugin = createPlugin({ + id: 'firehydrant', + apis: [ + createApiFactory({ + api: fireHydrantApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new FireHydrantAPIClient({ discoveryApi }), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +export const FirehydrantPage = firehydrantPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ServiceDetailsCard').then(m => m.ServiceDetailsCard), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/firehydrant/src/routes.ts b/plugins/firehydrant/src/routes.ts new file mode 100644 index 0000000000..eb2f8b6375 --- /dev/null +++ b/plugins/firehydrant/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + title: 'firehydrant', +}); diff --git a/plugins/firehydrant/src/setupTests.ts b/plugins/firehydrant/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/firehydrant/src/setupTests.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. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill';