add FireHydrant plugin
Signed-off-by: Christine Yi <ohchristineyi@gmail.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 {
|
||||
ServiceAnalyticsResponse,
|
||||
ServiceDetailsResponse,
|
||||
ServiceIncidentsResponse,
|
||||
} from './types';
|
||||
import { Incident, Service } from '../components/types';
|
||||
import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export interface FireHydrantAPI {
|
||||
getServiceAnalytics(options: {
|
||||
serviceId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}): Promise<ServiceAnalyticsResponse>;
|
||||
|
||||
getServiceDetails(options: {
|
||||
serviceName: string;
|
||||
}): Promise<ServiceDetailsResponse>;
|
||||
|
||||
getServiceIncidents(options: {
|
||||
serviceId: string;
|
||||
}): Promise<ServiceIncidentsResponse>;
|
||||
}
|
||||
|
||||
export const fireHydrantApiRef = createApiRef<FireHydrantAPI>({
|
||||
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<ServiceAnalyticsResponse> {
|
||||
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<ServiceDetailsResponse> {
|
||||
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<ServiceIncidentsResponse> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
@@ -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(
|
||||
<ServiceAnalytics
|
||||
error={null}
|
||||
loading={false}
|
||||
value={{
|
||||
id: 'b20de448-7855-4fb0-8d98-d9eafe82fc2c',
|
||||
mttd: 32,
|
||||
mtta: 44,
|
||||
mttm: 33,
|
||||
mttr: 23,
|
||||
count: 0,
|
||||
total_time: 0,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/32/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/44/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<div className={classes.container}>
|
||||
<Table
|
||||
title="Incident Analytics"
|
||||
subtitle={`${startDate.format('ll')} - ${endDate.format('ll')}`}
|
||||
options={{ paging: false, search: false }}
|
||||
columns={columns}
|
||||
data={[service]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<DenseTable
|
||||
service={serviceData}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ServiceDetailsCard />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ServiceDetailsCard />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/This service does not exist in FireHydrant/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<div className={classes.warning}>
|
||||
<WarningIcon />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<ServiceAnalytics
|
||||
loading={analyticsLoading}
|
||||
value={analyticsValue}
|
||||
error={analyticsError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ServiceWarning text="There was an issue connecting to FireHydrant." />
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<InfoCard className="infoCard">
|
||||
{!showServiceDetails && !loading && (
|
||||
<ServiceWarning text="This service does not exist in FireHydrant." />
|
||||
)}
|
||||
{showServiceDetails && (
|
||||
<Box
|
||||
alignItems="center"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
borderBottom="1px solid #d5d5d5"
|
||||
>
|
||||
<Box>
|
||||
<h2>{headerText}</h2>
|
||||
</Box>
|
||||
<Box>
|
||||
<MaterialButton
|
||||
className={classes.buttonLink}
|
||||
color="default"
|
||||
href={serviceIncidentsLink}
|
||||
target="_blank"
|
||||
variant="outlined"
|
||||
>
|
||||
View service incidents
|
||||
<ExitToAppIcon />
|
||||
</MaterialButton>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{activeIncidents && activeIncidents?.length > 0 && (
|
||||
<Box className={classes.linksContainer}>
|
||||
{incidents &&
|
||||
incidents?.slice(0, 5).map((incident: Incident, index: number) => (
|
||||
<div key={index}>
|
||||
<a className={classes.link} href={incident.incident_url}>
|
||||
{incident.name}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box paddingLeft="16px" marginTop="10px">
|
||||
<Typography variant="subtitle1">View in FireHydrant </Typography>
|
||||
<Box className={classes.buttonContainer} marginTop="10px">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={classes.button}
|
||||
href={`${BASE_URL}/incidents/new`}
|
||||
>
|
||||
<AddIcon className={classes.icon} />
|
||||
<span>Declare an incident</span>
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={classes.button}
|
||||
href={`${BASE_URL}/incidents`}
|
||||
>
|
||||
<WhatshotIcon className={classes.icon} />
|
||||
<span>View all incidents</span>
|
||||
</a>
|
||||
{showServiceDetails && (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={classes.button}
|
||||
href={`${BASE_URL}/services/${value?.service?.id}`}
|
||||
>
|
||||
<NotesIcon className={classes.icon} />
|
||||
<span>View Service Details</span>
|
||||
</a>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{showServiceDetails && (
|
||||
<Box>
|
||||
<ServiceAnalyticsView
|
||||
serviceId={serviceId}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user