add FireHydrant plugin

Signed-off-by: Christine Yi <ohchristineyi@gmail.com>
This commit is contained in:
Christine Yi
2021-07-29 12:05:37 -04:00
parent c49b012d55
commit e6a53d79f0
22 changed files with 1207 additions and 0 deletions
+137
View File
@@ -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;
}
}
+33
View File
@@ -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 />
&nbsp;&nbsp;<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&nbsp;&nbsp;
<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;
};
+16
View File
@@ -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';
+22
View File
@@ -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();
});
});
+46
View File
@@ -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,
}),
);
+20
View File
@@ -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',
});
+17
View File
@@ -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';