Merge pull request #9289 from moltenice-forks/airbrake-api
Airbrake Plugin API connectivity
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-airbrake': minor
|
||||
---
|
||||
|
||||
API connectivity has added, but currently will only work by running it standalone on a browser with CORS disabled.
|
||||
@@ -4,6 +4,6 @@ author: Simply Business
|
||||
authorUrl: https://github.com/simplybusiness/
|
||||
category: Monitoring
|
||||
description: Access Airbrake error monitoring and other integrations from within Backstage
|
||||
documentation: https://github.com/backstage/backstage/blob/master/plugins/airbrake/README.md
|
||||
documentation: https://github.com/backstage/backstage/blob/master/plugins/airbrake
|
||||
iconUrl: https://wp-assets.airbrake.io/wp-content/uploads/2020/10/05222904/Square-white-A-on-Orange.png
|
||||
npmPackageName: '@backstage/plugin-airbrake'
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
createTheme,
|
||||
makeStyles,
|
||||
MuiThemeProvider,
|
||||
TextField,
|
||||
} from '@material-ui/core';
|
||||
import { Context } from '../ContextProvider';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
gap: '1em',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
});
|
||||
|
||||
const textFieldTheme = createTheme({
|
||||
palette: {
|
||||
type: 'dark',
|
||||
primary: {
|
||||
light: '#fff',
|
||||
main: '#fff',
|
||||
dark: '#fff',
|
||||
contrastText: '#fff',
|
||||
},
|
||||
secondary: {
|
||||
light: '#fff',
|
||||
main: '#fff',
|
||||
dark: '#fff',
|
||||
contrastText: '#fff',
|
||||
},
|
||||
action: {
|
||||
disabled: '#fff',
|
||||
},
|
||||
text: {
|
||||
primary: '#fff',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ApiBar = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Context.Consumer>
|
||||
{value => (
|
||||
<div className={classes.root}>
|
||||
<MuiThemeProvider theme={textFieldTheme}>
|
||||
<TextField
|
||||
label="Project ID"
|
||||
variant="outlined"
|
||||
defaultValue={value.projectId}
|
||||
onChange={e =>
|
||||
value.setProjectId?.(parseInt(e.target.value, 10) || undefined)
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label="API Key"
|
||||
variant="outlined"
|
||||
defaultValue={value.apiKey}
|
||||
onChange={e => value.setApiKey?.(e.target.value)}
|
||||
/>
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
)}
|
||||
</Context.Consumer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ApiBar } from './ApiBar';
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2022 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, {
|
||||
Dispatch,
|
||||
PropsWithChildren,
|
||||
SetStateAction,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
interface ContextInterface {
|
||||
projectId?: number;
|
||||
setProjectId?: Dispatch<SetStateAction<number | undefined>>;
|
||||
apiKey?: string;
|
||||
setApiKey?: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const Context = React.createContext<ContextInterface>({});
|
||||
|
||||
export const ContextProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
const [projectId, setProjectId] = useState<number>();
|
||||
const [apiKey, setApiKey] = useState<string>('');
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
projectId,
|
||||
setProjectId,
|
||||
apiKey,
|
||||
setApiKey,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ContextProvider, Context } from './ContextProvider';
|
||||
@@ -15,39 +15,67 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { EntityAirbrakeContent, airbrakePlugin } from '../src/plugin';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { airbrakePlugin, EntityAirbrakeContent } from '../src';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Page,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
airbrakeApiRef,
|
||||
MockAirbrakeApi,
|
||||
ProductionAirbrakeApi,
|
||||
} from '../src/api';
|
||||
import { ApiBar } from './components/ApiBar';
|
||||
import { Content, Header, Page } from '@backstage/core-components';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { createEntity } from '../src/api/mock/MockEntity';
|
||||
import CloudOffIcon from '@material-ui/icons/CloudOff';
|
||||
import CloudIcon from '@material-ui/icons/Cloud';
|
||||
import { Context, ContextProvider } from './components/ContextProvider';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(airbrakePlugin)
|
||||
.addPage({
|
||||
element: (
|
||||
<Page themeId="tool">
|
||||
<Header
|
||||
title="Airbrake demo application"
|
||||
subtitle="Test the plugin below"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Owner" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Header title="Airbrake demo application" subtitle="Mock API" />
|
||||
<Content>
|
||||
<ContentHeader title="Airbrake">
|
||||
<SupportButton>
|
||||
A description of your plugin goes here.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<EntityAirbrakeContent />
|
||||
<TestApiProvider apis={[[airbrakeApiRef, new MockAirbrakeApi(800)]]}>
|
||||
<EntityProvider entity={createEntity(1234)}>
|
||||
<EntityAirbrakeContent />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>
|
||||
</Content>
|
||||
</Page>
|
||||
),
|
||||
title: 'Root Page',
|
||||
path: '/airbrake',
|
||||
title: 'Mock API',
|
||||
path: '/airbrake-mock-api',
|
||||
icon: CloudOffIcon,
|
||||
})
|
||||
.addPage({
|
||||
element: (
|
||||
<ContextProvider>
|
||||
<Page themeId="tool">
|
||||
<Header title="Airbrake demo application" subtitle="Real API">
|
||||
<ApiBar />
|
||||
</Header>
|
||||
<Content>
|
||||
<Context.Consumer>
|
||||
{value => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[airbrakeApiRef, new ProductionAirbrakeApi(value.apiKey)],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={createEntity(value.projectId)}>
|
||||
<EntityAirbrakeContent />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>
|
||||
)}
|
||||
</Context.Consumer>
|
||||
</Content>
|
||||
</Page>
|
||||
</ContextProvider>
|
||||
),
|
||||
title: 'Real API',
|
||||
path: '/airbrake-real-api',
|
||||
icon: CloudIcon,
|
||||
})
|
||||
.render();
|
||||
|
||||
@@ -20,20 +20,23 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.9.10",
|
||||
"@backstage/core-components": "^0.8.8",
|
||||
"@backstage/core-plugin-api": "^0.6.0",
|
||||
"@backstage/dev-utils": "^0.2.21",
|
||||
"@backstage/plugin-catalog-react": "^0.6.14",
|
||||
"@backstage/test-utils": "^0.2.4",
|
||||
"@backstage/theme": "^0.2.14",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"react-use": "^17.2.4",
|
||||
"object-hash": "^2.2.0"
|
||||
"object-hash": "^2.2.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/object-hash": "^2.2.1",
|
||||
"@backstage/app-defaults": "^0.1.7",
|
||||
"@backstage/cli": "^0.13.2",
|
||||
"@backstage/core-app-api": "^0.5.2",
|
||||
@@ -44,8 +47,9 @@
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"msw": "^0.35.0",
|
||||
"@types/object-hash": "^2.2.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^0.35.0",
|
||||
"react-router": "6.0.0-beta.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
+10
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
* Copyright 2020 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.
|
||||
@@ -13,13 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EntityAirbrakeContent } from './plugin';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('The Airbrake entity', () => {
|
||||
it('should render the content properly', async () => {
|
||||
const rendered = await renderInTestApp(<EntityAirbrakeContent />);
|
||||
expect(rendered.getByText('ChunkLoadError')).toBeInTheDocument();
|
||||
});
|
||||
import { Groups } from './airbrakeGroups';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const airbrakeApiRef = createApiRef<AirbrakeApi>({
|
||||
id: 'plugin.airbrake.service',
|
||||
});
|
||||
|
||||
export interface AirbrakeApi {
|
||||
fetchGroups(projectId: string): Promise<Groups>;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2022 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 { ProductionAirbrakeApi } from './ProductionApi';
|
||||
import { rest } from 'msw';
|
||||
import mockGroupsData from './mock/airbrakeGroupsApiMock.json';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
|
||||
describe('The production Airbrake API', () => {
|
||||
const productionApi = new ProductionAirbrakeApi('fakeApiKey');
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
it('fetches groups using the provided project ID', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.airbrake.io/api/v4/projects/123456/groups',
|
||||
(req, res, ctx) => {
|
||||
if (req.url.searchParams.get('key') === 'fakeApiKey') {
|
||||
return res(ctx.status(200), ctx.json(mockGroupsData));
|
||||
}
|
||||
return res(ctx.status(401));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const groups = await productionApi.fetchGroups('123456');
|
||||
|
||||
expect(groups).toStrictEqual(mockGroupsData);
|
||||
});
|
||||
|
||||
it('throws if fetching groups was unsuccessful', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.airbrake.io/api/v4/projects/123456/groups',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await expect(productionApi.fetchGroups('123456')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
+16
-14
@@ -14,19 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EntityAirbrakeContent } from './EntityAirbrakeContent';
|
||||
import exampleData from './example-data.json';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { Groups } from './airbrakeGroups';
|
||||
import { AirbrakeApi } from './AirbrakeApi';
|
||||
|
||||
describe('EntityAirbrakeContent', () => {
|
||||
it('renders all errors sent from Airbrake', async () => {
|
||||
const table = await renderInTestApp(<EntityAirbrakeContent />);
|
||||
expect(exampleData.groups.length).toBeGreaterThan(0);
|
||||
for (const group of exampleData.groups) {
|
||||
expect(
|
||||
await table.getByText(group.errors[0].message),
|
||||
).toBeInTheDocument();
|
||||
export class ProductionAirbrakeApi implements AirbrakeApi {
|
||||
constructor(private readonly apiKey?: string) {}
|
||||
|
||||
async fetchGroups(projectId: string): Promise<Groups> {
|
||||
const apiUrl = `https://api.airbrake.io/api/v4/projects/${projectId}/groups?key=${this.apiKey}`;
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
|
||||
if (response.status >= 400 && response.status < 600) {
|
||||
throw new Error('Failed fetching Airbrake groups');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (await response.json()) as Groups;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2022 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 interface Groups {
|
||||
count: number;
|
||||
end: string;
|
||||
groups?: Group[] | null;
|
||||
page: number;
|
||||
resolvedCount: number;
|
||||
unresolvedCount: number;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
projectId: number;
|
||||
resolved: boolean;
|
||||
muted: boolean;
|
||||
mutedBy: number;
|
||||
mutedAt?: string | null;
|
||||
errors?: Error[];
|
||||
attributes?: string | null;
|
||||
context: Context;
|
||||
lastDeployId: string;
|
||||
lastDeployAt?: string | null;
|
||||
lastNoticeId: string;
|
||||
lastNoticeAt: string;
|
||||
noticeCount: number;
|
||||
noticeTotalCount: number;
|
||||
commentCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Error {
|
||||
type: string;
|
||||
message: string;
|
||||
backtrace?: Backtrace[];
|
||||
}
|
||||
|
||||
export interface Backtrace {
|
||||
file: string;
|
||||
function: string;
|
||||
line: number;
|
||||
column: number;
|
||||
code?: string | null;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
action: string;
|
||||
component: string;
|
||||
environment: string;
|
||||
severity: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './mock';
|
||||
export type { AirbrakeApi } from './AirbrakeApi';
|
||||
export { airbrakeApiRef } from './AirbrakeApi';
|
||||
export type { Groups } from './airbrakeGroups';
|
||||
export { ProductionAirbrakeApi } from './ProductionApi';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2020 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 { Groups } from '../airbrakeGroups';
|
||||
import { AirbrakeApi } from '../AirbrakeApi';
|
||||
import mockGroupsData from './airbrakeGroupsApiMock.json';
|
||||
|
||||
export class MockAirbrakeApi implements AirbrakeApi {
|
||||
waitTimeInMillis: number;
|
||||
|
||||
constructor(waitTimeInMillis = 10) {
|
||||
this.waitTimeInMillis = waitTimeInMillis;
|
||||
}
|
||||
|
||||
fetchGroups(): Promise<Groups> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(mockGroupsData), this.waitTimeInMillis);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2022 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 { AIRBRAKE_PROJECT_ID_ANNOTATION } from '../../components/useProjectId';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const createEntity = (projectId?: number) => {
|
||||
const projectIdString = projectId?.toString();
|
||||
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {
|
||||
[AIRBRAKE_PROJECT_ID_ANNOTATION]: projectIdString,
|
||||
},
|
||||
name: projectIdString,
|
||||
},
|
||||
} as Entity;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 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 { MockAirbrakeApi } from './MockApi';
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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 { Grid, Typography } from '@material-ui/core';
|
||||
import { InfoCard } from '@backstage/core-components';
|
||||
import exampleData from './example-data.json';
|
||||
import hash from 'object-hash';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(() => ({
|
||||
multilineText: {
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
}));
|
||||
|
||||
export const EntityAirbrakeContent = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid container spacing={3} direction="column">
|
||||
{exampleData.groups.map(group => (
|
||||
<Grid item key={group.id}>
|
||||
{group.errors.map(error => (
|
||||
<InfoCard title={error.type} key={hash(error)}>
|
||||
<Typography variant="body1" className={classes.multilineText}>
|
||||
{error.message}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
))}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2020 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 { EntityAirbrakeWidget } from './EntityAirbrakeWidget';
|
||||
import exampleData from '../../api/mock/airbrakeGroupsApiMock.json';
|
||||
import {
|
||||
MockErrorApi,
|
||||
renderInTestApp,
|
||||
setupRequestMockHandlers,
|
||||
TestApiProvider,
|
||||
} from '@backstage/test-utils';
|
||||
import { createEntity } from '../../api/mock/MockEntity';
|
||||
import {
|
||||
airbrakeApiRef,
|
||||
MockAirbrakeApi,
|
||||
ProductionAirbrakeApi,
|
||||
} from '../../api';
|
||||
import { errorApiRef } from '@backstage/core-plugin-api';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
describe('EntityAirbrakeContent', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
it('renders all errors sent from Airbrake', async () => {
|
||||
const widget = await renderInTestApp(
|
||||
<TestApiProvider apis={[[airbrakeApiRef, new MockAirbrakeApi()]]}>
|
||||
<EntityAirbrakeWidget entity={createEntity(123)} />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(exampleData.groups.length).toBeGreaterThan(0);
|
||||
for (const group of exampleData.groups) {
|
||||
expect(
|
||||
await widget.getByText(group.errors[0].message),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('states that the annotation is missing if no project ID annotation is provided', async () => {
|
||||
const widget = await renderInTestApp(
|
||||
<TestApiProvider apis={[[airbrakeApiRef, new MockAirbrakeApi()]]}>
|
||||
<EntityAirbrakeWidget entity={createEntity()} />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await expect(
|
||||
widget.findByText('Missing Annotation'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('states that an error occurred if the API call fails', async () => {
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.airbrake.io/api/v4/projects/123/groups',
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
const mockErrorApi = new MockErrorApi({ collect: true });
|
||||
|
||||
const widget = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[airbrakeApiRef, new ProductionAirbrakeApi('fakeApiKey')],
|
||||
[errorApiRef, mockErrorApi],
|
||||
]}
|
||||
>
|
||||
<EntityAirbrakeWidget entity={createEntity(123)} />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
widget.findByText(/.*there was an issue communicating with Airbrake.*/),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(mockErrorApi.getErrors().length).toBe(1);
|
||||
expect(mockErrorApi.getErrors()[0].error.message).toStrictEqual(
|
||||
'Failed fetching Airbrake groups',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Grid, Typography } from '@material-ui/core';
|
||||
import {
|
||||
EmptyState,
|
||||
InfoCard,
|
||||
MissingAnnotationEmptyState,
|
||||
Progress,
|
||||
} from '@backstage/core-components';
|
||||
import hash from 'object-hash';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { airbrakeApiRef } from '../../api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(() => ({
|
||||
multilineText: {
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
}));
|
||||
|
||||
export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const projectId = useProjectId(entity);
|
||||
const errorApi = useApi<ErrorApi>(errorApiRef);
|
||||
const airbrakeApi = useApi(airbrakeApiRef);
|
||||
|
||||
const { loading, value, error } = useAsync(
|
||||
() => airbrakeApi.fetchGroups(projectId),
|
||||
[airbrakeApi, projectId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
errorApi.post(error);
|
||||
}
|
||||
}, [error, errorApi]);
|
||||
|
||||
if (loading || !projectId || error) {
|
||||
return (
|
||||
<InfoCard title="Airbrake groups" variant="gridItem">
|
||||
{loading && <Progress />}
|
||||
|
||||
{!loading && !projectId && (
|
||||
<MissingAnnotationEmptyState
|
||||
annotation={AIRBRAKE_PROJECT_ID_ANNOTATION}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && error && (
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title="No information to display"
|
||||
description={`There is no Airbrake project with id '${projectId}' or there was an issue communicating with Airbrake.`}
|
||||
/>
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={3} direction="column">
|
||||
{value?.groups?.map(group => (
|
||||
<Grid item key={group.id}>
|
||||
{group.errors?.map(groupError => (
|
||||
<InfoCard title={groupError.type} key={hash(groupError)}>
|
||||
<Typography variant="body1" className={classes.multilineText}>
|
||||
{groupError.message}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
))}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 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 { EntityAirbrakeWidget } from './EntityAirbrakeWidget';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 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 { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const AIRBRAKE_PROJECT_ID_ANNOTATION = 'airbrake.io/project-id';
|
||||
|
||||
export const useProjectId = (entity: Entity) => {
|
||||
return entity?.metadata.annotations?.[AIRBRAKE_PROJECT_ID_ANNOTATION] ?? '';
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { EntityAirbrakeContent } from './extensions';
|
||||
import { Route } from 'react-router';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { airbrakeApiRef, MockAirbrakeApi } from './api';
|
||||
import { createEntity } from './api/mock/MockEntity';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
|
||||
describe('The Airbrake entity', () => {
|
||||
it('should render the content properly', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[airbrakeApiRef, new MockAirbrakeApi()]]}>
|
||||
<EntityProvider entity={createEntity(123)}>
|
||||
<Route path="/" element={<EntityAirbrakeContent />} />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await expect(
|
||||
rendered.findByText('ChunkLoadError'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 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 { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { airbrakePlugin } from './plugin';
|
||||
import { createRoutableExtension } from '@backstage/core-plugin-api';
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const EntityAirbrakeContent = airbrakePlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'EntityAirbrakeContent',
|
||||
mountPoint: rootRouteRef,
|
||||
component: () =>
|
||||
import('./components/EntityAirbrakeWidget').then(
|
||||
({ EntityAirbrakeWidget }) => {
|
||||
return () => {
|
||||
const { entity } = useEntity();
|
||||
return <EntityAirbrakeWidget entity={entity} />;
|
||||
};
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -13,4 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { airbrakePlugin, EntityAirbrakeContent } from './plugin';
|
||||
export { airbrakePlugin } from './plugin';
|
||||
export { EntityAirbrakeContent } from './extensions';
|
||||
|
||||
@@ -15,9 +15,16 @@
|
||||
*/
|
||||
|
||||
import { airbrakePlugin } from './plugin';
|
||||
import { ProductionAirbrakeApi } from './api';
|
||||
|
||||
describe('catalog', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(airbrakePlugin).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have at least one API, the production API', () => {
|
||||
const apiFactories = Array.from(airbrakePlugin.getApis());
|
||||
expect(apiFactories.length).toBe(1);
|
||||
expect(apiFactories[0].factory({})).toBeInstanceOf(ProductionAirbrakeApi);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,27 +13,21 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { createApiFactory, createPlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
import { airbrakeApiRef, ProductionAirbrakeApi } from './api';
|
||||
|
||||
export const airbrakePlugin = createPlugin({
|
||||
id: 'airbrake',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: airbrakeApiRef,
|
||||
deps: {},
|
||||
factory: () => new ProductionAirbrakeApi(),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const EntityAirbrakeContent = airbrakePlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'EntityAirbrakeContent',
|
||||
component: () =>
|
||||
import('./components/EntityAirbrakeContent/EntityAirbrakeContent').then(
|
||||
m => m.EntityAirbrakeContent,
|
||||
),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user