From 135df6accc042471807536659e79cec942212fb8 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 10 Jan 2022 10:52:02 +0000 Subject: [PATCH 01/42] Setup the API code Signed-off-by: Karan Shah --- plugins/airbrake/src/api/airbrake-api.ts | 26 ++++++ plugins/airbrake/src/api/airbrake-groups.d.ts | 65 ++++++++++++++ plugins/airbrake/src/api/index.ts | 21 +++++ .../api/mock/airbrake-groups-api-mock.json | 85 +++++++++++++++++++ plugins/airbrake/src/api/mock/index.ts | 17 ++++ plugins/airbrake/src/api/mock/mock-api.ts | 27 ++++++ plugins/airbrake/src/api/production-api.ts | 37 ++++++++ 7 files changed, 278 insertions(+) create mode 100644 plugins/airbrake/src/api/airbrake-api.ts create mode 100644 plugins/airbrake/src/api/airbrake-groups.d.ts create mode 100644 plugins/airbrake/src/api/index.ts create mode 100644 plugins/airbrake/src/api/mock/airbrake-groups-api-mock.json create mode 100644 plugins/airbrake/src/api/mock/index.ts create mode 100644 plugins/airbrake/src/api/mock/mock-api.ts create mode 100644 plugins/airbrake/src/api/production-api.ts diff --git a/plugins/airbrake/src/api/airbrake-api.ts b/plugins/airbrake/src/api/airbrake-api.ts new file mode 100644 index 0000000000..1a3cfa72e4 --- /dev/null +++ b/plugins/airbrake/src/api/airbrake-api.ts @@ -0,0 +1,26 @@ +/* + * 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 './airbrake-groups'; +import { createApiRef } from '@backstage/core-plugin-api'; + +export const airbrakeApiRef = createApiRef({ + id: 'plugin.airbrake.service', +}); + +export interface AirbrakeApi { + fetchGroups(project: string): Promise; +} diff --git a/plugins/airbrake/src/api/airbrake-groups.d.ts b/plugins/airbrake/src/api/airbrake-groups.d.ts new file mode 100644 index 0000000000..df4c5de120 --- /dev/null +++ b/plugins/airbrake/src/api/airbrake-groups.d.ts @@ -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?: null; + errors?: Error[] | null; + attributes?: null; + context: Context; + lastDeployId: string; + lastDeployAt?: null; + lastNoticeId: string; + lastNoticeAt: string; + noticeCount: number; + noticeTotalCount: number; + commentCount: number; + createdAt: string; +} + +export interface Error { + type: string; + message: string; + backtrace?: Backtrace[] | null; +} + +export interface Backtrace { + file: string; + function: string; + line: number; + column: number; + code?: null; +} + +export interface Context { + action: string; + component: string; + environment: string; + severity: string; +} diff --git a/plugins/airbrake/src/api/index.ts b/plugins/airbrake/src/api/index.ts new file mode 100644 index 0000000000..7bad94e298 --- /dev/null +++ b/plugins/airbrake/src/api/index.ts @@ -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 './airbrake-api'; +export { airbrakeApiRef } from './airbrake-api'; +export type { Groups } from './airbrake-groups'; +export { ProductionAirbrakeApi } from './production-api'; diff --git a/plugins/airbrake/src/api/mock/airbrake-groups-api-mock.json b/plugins/airbrake/src/api/mock/airbrake-groups-api-mock.json new file mode 100644 index 0000000000..fa654ec50b --- /dev/null +++ b/plugins/airbrake/src/api/mock/airbrake-groups-api-mock.json @@ -0,0 +1,85 @@ +{ + "count": 2, + "end": "", + "groups": [ + { + "id": "1", + "projectId": 123, + "resolved": false, + "muted": false, + "mutedBy": 0, + "mutedAt": null, + "errors": [ + { + "type": "Error", + "message": "useSearch must be used within a SearchContextProvider", + "backtrace": [ + { + "file": "webpack-internal:///../../node_modules/@backstage/plugin-search/dist/esm/index-893ec2f5.esm.js", + "function": "useSearch", + "line": 303, + "column": 11, + "code": null + } + ] + } + ], + "attributes": null, + "context": { + "action": "", + "component": "", + "environment": "local", + "severity": "error" + }, + "lastDeployId": "0", + "lastDeployAt": null, + "lastNoticeId": "234", + "lastNoticeAt": "2021-12-19T09:59:00.124Z", + "noticeCount": 5, + "noticeTotalCount": 5, + "commentCount": 0, + "createdAt": "2021-12-19T09:44:30.067447Z" + }, + { + "id": "2", + "projectId": 123, + "resolved": true, + "muted": false, + "mutedBy": 0, + "mutedAt": null, + "errors": [ + { + "type": "ChunkLoadError", + "message": "Loading chunk 7764 failed.", + "backtrace": [ + { + "file": "/PROJECT_ROOT/static/runtime.069c874d.js", + "function": "Object._.f.j", + "line": 1, + "column": 19465, + "code": null + } + ] + } + ], + "attributes": null, + "context": { + "action": "", + "component": "", + "environment": "local", + "severity": "error" + }, + "lastDeployId": "0", + "lastDeployAt": null, + "lastNoticeId": "345", + "lastNoticeAt": "2021-12-15T17:16:38.419Z", + "noticeCount": 1, + "noticeTotalCount": 1, + "commentCount": 0, + "createdAt": "2021-12-15T17:16:38.41983Z" + } + ], + "page": 1, + "resolvedCount": 1, + "unresolvedCount": 1 +} diff --git a/plugins/airbrake/src/api/mock/index.ts b/plugins/airbrake/src/api/mock/index.ts new file mode 100644 index 0000000000..f084268951 --- /dev/null +++ b/plugins/airbrake/src/api/mock/index.ts @@ -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 './mock-api'; diff --git a/plugins/airbrake/src/api/mock/mock-api.ts b/plugins/airbrake/src/api/mock/mock-api.ts new file mode 100644 index 0000000000..df0293da7c --- /dev/null +++ b/plugins/airbrake/src/api/mock/mock-api.ts @@ -0,0 +1,27 @@ +/* + * 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 '../airbrake-groups'; +import { AirbrakeApi } from '../airbrake-api'; +import mockGroupsData from './airbrake-groups-api-mock.json'; + +export class MockAirbrakeApi implements AirbrakeApi { + fetchGroups(): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(mockGroupsData), 800); + }); + } +} diff --git a/plugins/airbrake/src/api/production-api.ts b/plugins/airbrake/src/api/production-api.ts new file mode 100644 index 0000000000..37ac938752 --- /dev/null +++ b/plugins/airbrake/src/api/production-api.ts @@ -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 { Groups } from './airbrake-groups'; +import { AirbrakeApi } from './airbrake-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; + +export class ProductionAirbrakeApi implements AirbrakeApi { + constructor(private readonly discoveryApi: DiscoveryApi) {} + + async fetchGroups(project: string): Promise { + const apiUrl = `${await this.discoveryApi.getBaseUrl( + 'proxy', + )}/airbrake/api`; + + const response = await fetch(`${apiUrl}/v4/projects/${project}/groups`); + + if (response.status >= 400 && response.status < 600) { + throw new Error('Failed fetching Airbrake issues'); + } + + return (await response.json()) as Groups; + } +} From f3d7baaefa248681c8db3e0829f95cf02af40934 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 10 Jan 2022 11:49:23 +0000 Subject: [PATCH 02/42] Connect the API code to the components Signed-off-by: Karan Shah --- plugins/airbrake/package.json | 1 + plugins/airbrake/src/api/airbrake-groups.d.ts | 12 +-- .../EntityAirbrakeContent.tsx | 65 ++++++++++++-- .../EntityAirbrakeContent/example-data.json | 85 ------------------- .../airbrake/src/components/useProjectSlug.ts | 23 +++++ 5 files changed, 87 insertions(+), 99 deletions(-) delete mode 100644 plugins/airbrake/src/components/EntityAirbrakeContent/example-data.json create mode 100644 plugins/airbrake/src/components/useProjectSlug.ts diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 7294c97413..7f96f0bc61 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -20,6 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.9.7", "@backstage/core-components": "^0.8.3", "@backstage/core-plugin-api": "^0.4.1", "@backstage/theme": "^0.2.14", diff --git a/plugins/airbrake/src/api/airbrake-groups.d.ts b/plugins/airbrake/src/api/airbrake-groups.d.ts index df4c5de120..e1887eca9b 100644 --- a/plugins/airbrake/src/api/airbrake-groups.d.ts +++ b/plugins/airbrake/src/api/airbrake-groups.d.ts @@ -29,12 +29,12 @@ export interface Group { resolved: boolean; muted: boolean; mutedBy: number; - mutedAt?: null; - errors?: Error[] | null; - attributes?: null; + mutedAt?: string | null; + errors?: Error[]; + attributes?: string | null; context: Context; lastDeployId: string; - lastDeployAt?: null; + lastDeployAt?: string | null; lastNoticeId: string; lastNoticeAt: string; noticeCount: number; @@ -46,7 +46,7 @@ export interface Group { export interface Error { type: string; message: string; - backtrace?: Backtrace[] | null; + backtrace?: Backtrace[]; } export interface Backtrace { @@ -54,7 +54,7 @@ export interface Backtrace { function: string; line: number; column: number; - code?: null; + code?: string | null; } export interface Context { diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx b/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx index 062d9e898a..2bd3917443 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx @@ -13,13 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import React, { useEffect } from 'react'; import { Grid, Typography } from '@material-ui/core'; -import { InfoCard } from '@backstage/core-components'; -import exampleData from './example-data.json'; +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_SLUG_ANNOTATION, + useProjectSlug, +} from '../useProjectSlug'; const useStyles = makeStyles(() => ({ multilineText: { @@ -27,17 +39,54 @@ const useStyles = makeStyles(() => ({ }, })); -export const EntityAirbrakeContent = () => { +export const EntityAirbrakeContent = ({ entity }: { entity: Entity }) => { const classes = useStyles(); + const projectId = useProjectSlug(entity); + const errorApi = useApi(errorApiRef); + const airbrakeApi = useApi(airbrakeApiRef); + + const { loading, value, error } = useAsync( + () => airbrakeApi.fetchGroups(projectId), + [projectId], + ); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + if (loading || !projectId || error) { + return ( + + {loading && } + + {!loading && !projectId && ( + + )} + + {!loading && error && ( + + )} + + ); + } + return ( - {exampleData.groups.map(group => ( + {value?.groups?.map(group => ( - {group.errors.map(error => ( - + {group.errors?.map(groupError => ( + - {error.message} + {groupError.message} ))} diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/example-data.json b/plugins/airbrake/src/components/EntityAirbrakeContent/example-data.json deleted file mode 100644 index fa654ec50b..0000000000 --- a/plugins/airbrake/src/components/EntityAirbrakeContent/example-data.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "count": 2, - "end": "", - "groups": [ - { - "id": "1", - "projectId": 123, - "resolved": false, - "muted": false, - "mutedBy": 0, - "mutedAt": null, - "errors": [ - { - "type": "Error", - "message": "useSearch must be used within a SearchContextProvider", - "backtrace": [ - { - "file": "webpack-internal:///../../node_modules/@backstage/plugin-search/dist/esm/index-893ec2f5.esm.js", - "function": "useSearch", - "line": 303, - "column": 11, - "code": null - } - ] - } - ], - "attributes": null, - "context": { - "action": "", - "component": "", - "environment": "local", - "severity": "error" - }, - "lastDeployId": "0", - "lastDeployAt": null, - "lastNoticeId": "234", - "lastNoticeAt": "2021-12-19T09:59:00.124Z", - "noticeCount": 5, - "noticeTotalCount": 5, - "commentCount": 0, - "createdAt": "2021-12-19T09:44:30.067447Z" - }, - { - "id": "2", - "projectId": 123, - "resolved": true, - "muted": false, - "mutedBy": 0, - "mutedAt": null, - "errors": [ - { - "type": "ChunkLoadError", - "message": "Loading chunk 7764 failed.", - "backtrace": [ - { - "file": "/PROJECT_ROOT/static/runtime.069c874d.js", - "function": "Object._.f.j", - "line": 1, - "column": 19465, - "code": null - } - ] - } - ], - "attributes": null, - "context": { - "action": "", - "component": "", - "environment": "local", - "severity": "error" - }, - "lastDeployId": "0", - "lastDeployAt": null, - "lastNoticeId": "345", - "lastNoticeAt": "2021-12-15T17:16:38.419Z", - "noticeCount": 1, - "noticeTotalCount": 1, - "commentCount": 0, - "createdAt": "2021-12-15T17:16:38.41983Z" - } - ], - "page": 1, - "resolvedCount": 1, - "unresolvedCount": 1 -} diff --git a/plugins/airbrake/src/components/useProjectSlug.ts b/plugins/airbrake/src/components/useProjectSlug.ts new file mode 100644 index 0000000000..4c6337b49e --- /dev/null +++ b/plugins/airbrake/src/components/useProjectSlug.ts @@ -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_SLUG_ANNOTATION = 'airbrake.io/project-slug'; + +export const useProjectSlug = (entity: Entity) => { + return entity?.metadata.annotations?.[AIRBRAKE_PROJECT_SLUG_ANNOTATION] ?? ''; +}; From b7e049377029e2fb699d1d2c9fd6be111e7bb89a Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 10 Jan 2022 13:51:33 +0000 Subject: [PATCH 03/42] Documentation should just point to the plugin folder, not the README file directly. Signed-off-by: Karan Shah --- microsite/data/plugins/airbrake.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/airbrake.yaml b/microsite/data/plugins/airbrake.yaml index 96de75c002..29c1f394d7 100644 --- a/microsite/data/plugins/airbrake.yaml +++ b/microsite/data/plugins/airbrake.yaml @@ -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' From d16c977381980614e529a65a28bc6b5dc5a2e352 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 10 Jan 2022 16:27:46 +0000 Subject: [PATCH 04/42] Incorporate the API changes to the rest of the code. Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 2 +- plugins/airbrake/package.json | 1 + .../EntityAirbrakeWidget.test.tsx} | 22 +++++++++-- .../EntityAirbrakeWidget.tsx} | 2 +- .../components/EntityAirbrakeWidget/index.ts | 17 +++++++++ ...keContent.test.tsx => extensions.test.tsx} | 2 +- plugins/airbrake/src/extensions.tsx | 38 +++++++++++++++++++ plugins/airbrake/src/index.ts | 3 +- plugins/airbrake/src/plugin.ts | 28 ++++++++------ 9 files changed, 96 insertions(+), 19 deletions(-) rename plugins/airbrake/src/components/{EntityAirbrakeContent/EntityAirbrakeContent.test.tsx => EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx} (61%) rename plugins/airbrake/src/components/{EntityAirbrakeContent/EntityAirbrakeContent.tsx => EntityAirbrakeWidget/EntityAirbrakeWidget.tsx} (97%) create mode 100644 plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts rename plugins/airbrake/src/{EntityAirbrakeContent.test.tsx => extensions.test.tsx} (94%) create mode 100644 plugins/airbrake/src/extensions.tsx diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index e74ce68ee5..4c52e27094 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { EntityAirbrakeContent, airbrakePlugin } from '../src/plugin'; +import { EntityAirbrakeContent, airbrakePlugin } from '../src'; import { Content, ContentHeader, diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 7f96f0bc61..5ebfae651f 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -23,6 +23,7 @@ "@backstage/catalog-model": "^0.9.7", "@backstage/core-components": "^0.8.3", "@backstage/core-plugin-api": "^0.4.1", + "@backstage/plugin-catalog-react": "^0.6.10", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx similarity index 61% rename from plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.test.tsx rename to plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index a621eb281c..d93c642e6c 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -15,13 +15,29 @@ */ import React from 'react'; -import { EntityAirbrakeContent } from './EntityAirbrakeContent'; -import exampleData from './example-data.json'; +import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; +import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; import { renderInTestApp } from '@backstage/test-utils'; +import { AIRBRAKE_PROJECT_SLUG_ANNOTATION } from '../useProjectSlug'; +import { Entity } from '@backstage/catalog-model'; describe('EntityAirbrakeContent', () => { + const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [AIRBRAKE_PROJECT_SLUG_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + it('renders all errors sent from Airbrake', async () => { - const table = await renderInTestApp(); + const table = await renderInTestApp( + , + ); expect(exampleData.groups.length).toBeGreaterThan(0); for (const group of exampleData.groups) { expect( diff --git a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx similarity index 97% rename from plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx rename to plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 2bd3917443..53f7d63f13 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeContent/EntityAirbrakeContent.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -39,7 +39,7 @@ const useStyles = makeStyles(() => ({ }, })); -export const EntityAirbrakeContent = ({ entity }: { entity: Entity }) => { +export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { const classes = useStyles(); const projectId = useProjectSlug(entity); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts b/plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts new file mode 100644 index 0000000000..0a85263c5f --- /dev/null +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/index.ts @@ -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'; diff --git a/plugins/airbrake/src/EntityAirbrakeContent.test.tsx b/plugins/airbrake/src/extensions.test.tsx similarity index 94% rename from plugins/airbrake/src/EntityAirbrakeContent.test.tsx rename to plugins/airbrake/src/extensions.test.tsx index f0afef425f..db8670ac50 100644 --- a/plugins/airbrake/src/EntityAirbrakeContent.test.tsx +++ b/plugins/airbrake/src/extensions.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { EntityAirbrakeContent } from './plugin'; +import { EntityAirbrakeContent } from './extensions'; import { renderInTestApp } from '@backstage/test-utils'; describe('The Airbrake entity', () => { diff --git a/plugins/airbrake/src/extensions.tsx b/plugins/airbrake/src/extensions.tsx new file mode 100644 index 0000000000..582088b250 --- /dev/null +++ b/plugins/airbrake/src/extensions.tsx @@ -0,0 +1,38 @@ +/* + * 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 }) => { + const AirbrakePage = () => { + const { entity } = useEntity(); + return ; + }; + return AirbrakePage; + }, + ), + }), +); diff --git a/plugins/airbrake/src/index.ts b/plugins/airbrake/src/index.ts index 8f319e02fd..d5aedd9ba6 100644 --- a/plugins/airbrake/src/index.ts +++ b/plugins/airbrake/src/index.ts @@ -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'; diff --git a/plugins/airbrake/src/plugin.ts b/plugins/airbrake/src/plugin.ts index 0a3679baa4..30dc6749c5 100644 --- a/plugins/airbrake/src/plugin.ts +++ b/plugins/airbrake/src/plugin.ts @@ -14,26 +14,30 @@ * limitations under the License. */ import { + configApiRef, + createApiFactory, createPlugin, - createRoutableExtension, + discoveryApiRef, + identityApiRef, } 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: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi }) => new ProductionAirbrakeApi(discoveryApi), + }), + ], routes: { root: rootRouteRef, }, }); - -export const EntityAirbrakeContent = airbrakePlugin.provide( - createRoutableExtension({ - name: 'EntityAirbrakeContent', - component: () => - import('./components/EntityAirbrakeContent/EntityAirbrakeContent').then( - m => m.EntityAirbrakeContent, - ), - mountPoint: rootRouteRef, - }), -); From 32ab08a4b14e4a2ef1b538fee7a5f58d368367cd Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 11 Jan 2022 09:35:22 +0000 Subject: [PATCH 05/42] Register the mock API for the dev app Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 6 ++++++ plugins/airbrake/src/plugin.ts | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index 4c52e27094..ec6c4fd0b6 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -24,9 +24,15 @@ import { Page, SupportButton, } from '@backstage/core-components'; +import { airbrakeApiRef, MockAirbrakeApi } from '../src/api'; createDevApp() .registerPlugin(airbrakePlugin) + .registerApi({ + api: airbrakeApiRef, + deps: {}, + factory: () => new MockAirbrakeApi(), + }) .addPage({ element: ( diff --git a/plugins/airbrake/src/plugin.ts b/plugins/airbrake/src/plugin.ts index 30dc6749c5..5a58f5b4b3 100644 --- a/plugins/airbrake/src/plugin.ts +++ b/plugins/airbrake/src/plugin.ts @@ -14,11 +14,9 @@ * limitations under the License. */ import { - configApiRef, createApiFactory, createPlugin, discoveryApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; @@ -30,9 +28,7 @@ export const airbrakePlugin = createPlugin({ createApiFactory({ api: airbrakeApiRef, deps: { - configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, }, factory: ({ discoveryApi }) => new ProductionAirbrakeApi(discoveryApi), }), From 87ffa4698ddc6bd1a8b3c655a831ea8a1db36567 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 11 Jan 2022 10:14:21 +0000 Subject: [PATCH 06/42] Create a mock entity Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 8 +++-- plugins/airbrake/src/api/mock/mock-entity.ts | 29 +++++++++++++++++++ .../EntityAirbrakeWidget.test.tsx | 15 +--------- plugins/airbrake/src/extensions.tsx | 3 +- 4 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 plugins/airbrake/src/api/mock/mock-entity.ts diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index ec6c4fd0b6..cd980408ae 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { EntityAirbrakeContent, airbrakePlugin } from '../src'; +import { airbrakePlugin, EntityAirbrakeContent } from '../src'; import { Content, ContentHeader, @@ -25,6 +25,8 @@ import { SupportButton, } from '@backstage/core-components'; import { airbrakeApiRef, MockAirbrakeApi } from '../src/api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { entity } from '../src/api/mock/mock-entity'; createDevApp() .registerPlugin(airbrakePlugin) @@ -49,7 +51,9 @@ createDevApp() A description of your plugin goes here. - + + + ), diff --git a/plugins/airbrake/src/api/mock/mock-entity.ts b/plugins/airbrake/src/api/mock/mock-entity.ts new file mode 100644 index 0000000000..7e5b897cca --- /dev/null +++ b/plugins/airbrake/src/api/mock/mock-entity.ts @@ -0,0 +1,29 @@ +/* + * 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_SLUG_ANNOTATION } from '../../components/useProjectSlug'; +import { Entity } from '@backstage/catalog-model'; + +export const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [AIRBRAKE_PROJECT_SLUG_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index d93c642e6c..000bd8f230 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -18,22 +18,9 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; import { renderInTestApp } from '@backstage/test-utils'; -import { AIRBRAKE_PROJECT_SLUG_ANNOTATION } from '../useProjectSlug'; -import { Entity } from '@backstage/catalog-model'; +import { entity } from '../../api/mock/mock-entity'; describe('EntityAirbrakeContent', () => { - const entity = (name?: string) => - ({ - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - annotations: { - [AIRBRAKE_PROJECT_SLUG_ANNOTATION]: name, - }, - name: name, - }, - } as Entity); - it('renders all errors sent from Airbrake', async () => { const table = await renderInTestApp( , diff --git a/plugins/airbrake/src/extensions.tsx b/plugins/airbrake/src/extensions.tsx index 582088b250..6fbea68409 100644 --- a/plugins/airbrake/src/extensions.tsx +++ b/plugins/airbrake/src/extensions.tsx @@ -27,11 +27,10 @@ export const EntityAirbrakeContent = airbrakePlugin.provide( component: () => import('./components/EntityAirbrakeWidget').then( ({ EntityAirbrakeWidget }) => { - const AirbrakePage = () => { + return () => { const { entity } = useEntity(); return ; }; - return AirbrakePage; }, ), }), From 35ab9befbaddb8aa8832df66613a16f7091d96c9 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 11 Jan 2022 10:45:29 +0000 Subject: [PATCH 07/42] Rename entity() to createEntity() Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 4 ++-- plugins/airbrake/src/api/mock/mock-entity.ts | 2 +- .../EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index cd980408ae..8c3dd2f643 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -26,7 +26,7 @@ import { } from '@backstage/core-components'; import { airbrakeApiRef, MockAirbrakeApi } from '../src/api'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { entity } from '../src/api/mock/mock-entity'; +import { createEntity } from '../src/api/mock/mock-entity'; createDevApp() .registerPlugin(airbrakePlugin) @@ -51,7 +51,7 @@ createDevApp() A description of your plugin goes here. - + diff --git a/plugins/airbrake/src/api/mock/mock-entity.ts b/plugins/airbrake/src/api/mock/mock-entity.ts index 7e5b897cca..b169938004 100644 --- a/plugins/airbrake/src/api/mock/mock-entity.ts +++ b/plugins/airbrake/src/api/mock/mock-entity.ts @@ -16,7 +16,7 @@ import { AIRBRAKE_PROJECT_SLUG_ANNOTATION } from '../../components/useProjectSlug'; import { Entity } from '@backstage/catalog-model'; -export const entity = (name?: string) => +export const createEntity = (name?: string) => ({ apiVersion: 'backstage.io/v1alpha1', kind: 'Component', diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 000bd8f230..3c63f84f19 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -18,12 +18,12 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; import { renderInTestApp } from '@backstage/test-utils'; -import { entity } from '../../api/mock/mock-entity'; +import { createEntity } from '../../api/mock/mock-entity'; describe('EntityAirbrakeContent', () => { it('renders all errors sent from Airbrake', async () => { const table = await renderInTestApp( - , + , ); expect(exampleData.groups.length).toBeGreaterThan(0); for (const group of exampleData.groups) { From 4b794662c184fe1af07e3b413e33cc32cc8dd301 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 25 Jan 2022 10:40:40 +0000 Subject: [PATCH 08/42] Added the UI ability to change APIs Signed-off-by: Karan Shah --- .../airbrake/dev/components/ApiBar/ApiBar.tsx | 62 +++++++++++++++++++ .../airbrake/dev/components/ApiBar/index.ts | 17 +++++ plugins/airbrake/dev/index.tsx | 35 +++++------ 3 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 plugins/airbrake/dev/components/ApiBar/ApiBar.tsx create mode 100644 plugins/airbrake/dev/components/ApiBar/index.ts diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx new file mode 100644 index 0000000000..f86cb348ba --- /dev/null +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -0,0 +1,62 @@ +/* + * 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, { useState } from 'react'; +import { Select, SelectItem } from '@backstage/core-components'; +import { makeStyles, TextField } from '@material-ui/core'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + gap: '1em', + flexWrap: 'wrap', + }, +}); + +export const ApiBar = () => { + const classes = useStyles(); + const apiOptions: SelectItem[] = [ + { label: 'Fake', value: 'fake' }, + { label: 'Real', value: 'real' }, + ]; + const [api, setApi] = useState('fake'); + const [projectId, setProjectId] = useState(); + const [apiKey, setApiKey] = useState(''); + + return ( +
+ setApi(newValue as string)} - /> + value={api} + onChange={e => setApi(e.target.value)} + > + {apiOptions.map(option => ( + + {option.label} + + ))} + {api === 'real' && ( <> Date: Wed, 26 Jan 2022 10:27:54 +0000 Subject: [PATCH 10/42] Start moving towards 2 pages Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index 17bbcf3d18..36f271a995 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -21,6 +21,8 @@ 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/mock-entity'; +import AirplanemodeActiveIcon from '@material-ui/icons/AirplanemodeActive'; +import AirplanemodeInactiveIcon from '@material-ui/icons/AirplanemodeInactive'; createDevApp() .registerPlugin(airbrakePlugin) @@ -29,6 +31,32 @@ createDevApp() deps: {}, factory: () => new MockAirbrakeApi(), }) + .addPage({ + element: ( + +
+ +
+ + + +
+
+ + ), + title: 'Mock API', + path: '/airbrake-mock-api', + icon: AirplanemodeActiveIcon, + }) .addPage({ element: ( @@ -52,7 +80,8 @@ createDevApp() ), - title: 'Root Page', - path: '/airbrake', + title: 'Real API', + path: '/airbrake-real-api', + icon: AirplanemodeInactiveIcon, }) .render(); From de5133035009e4d7b3f05f40566315b80802442d Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 26 Jan 2022 10:42:20 +0000 Subject: [PATCH 11/42] Added in FlexColumn.tsx Signed-off-by: Karan Shah --- .../airbrake/dev/components/ApiBar/ApiBar.tsx | 41 +++++-------------- .../dev/components/FlexColumn/FlexColumn.tsx | 34 +++++++++++++++ .../dev/components/FlexColumn/index.ts | 18 ++++++++ plugins/airbrake/dev/index.tsx | 21 +++------- 4 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx create mode 100644 plugins/airbrake/dev/components/FlexColumn/index.ts diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx index d782113dc0..5c2b71ca14 100644 --- a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { useState } from 'react'; -import { makeStyles, MenuItem, TextField } from '@material-ui/core'; +import { makeStyles, TextField } from '@material-ui/core'; const useStyles = makeStyles({ root: { @@ -26,42 +26,21 @@ const useStyles = makeStyles({ export const ApiBar = () => { const classes = useStyles(); - const apiOptions = [ - { label: 'Fake', value: 'fake' }, - { label: 'Real', value: 'real' }, - ]; - const [api, setApi] = useState('fake'); const [projectId, setProjectId] = useState(); const [apiKey, setApiKey] = useState(''); return (
setApi(e.target.value)} - > - {apiOptions.map(option => ( - - {option.label} - - ))} - - {api === 'real' && ( - <> - setProjectId(parseInt(e.target.value, 10))} - /> - setApiKey(e.target.value)} - /> - - )} + label="Project ID" + value={projectId} + onChange={e => setProjectId(parseInt(e.target.value, 10))} + /> + setApiKey(e.target.value)} + />
); }; diff --git a/plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx b/plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx new file mode 100644 index 0000000000..aaf64d3513 --- /dev/null +++ b/plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx @@ -0,0 +1,34 @@ +/* + * 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, { PropsWithChildren, ReactNode } from 'react'; + +export declare type FlexColumnProps = { + children?: ReactNode; +}; + +export const FlexColumn = (props: PropsWithChildren) => { + return ( +
+ {props.children} +
+ ); +}; diff --git a/plugins/airbrake/dev/components/FlexColumn/index.ts b/plugins/airbrake/dev/components/FlexColumn/index.ts new file mode 100644 index 0000000000..5926c3cb28 --- /dev/null +++ b/plugins/airbrake/dev/components/FlexColumn/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { FlexColumn } from './FlexColumn'; +export type { FlexColumnProps } from './FlexColumn'; diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index 36f271a995..ba520892cb 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -23,6 +23,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { createEntity } from '../src/api/mock/mock-entity'; import AirplanemodeActiveIcon from '@material-ui/icons/AirplanemodeActive'; import AirplanemodeInactiveIcon from '@material-ui/icons/AirplanemodeInactive'; +import { FlexColumn } from './components/FlexColumn'; createDevApp() .registerPlugin(airbrakePlugin) @@ -39,17 +40,11 @@ createDevApp() subtitle="This uses a fake API" /> -
+ -
+
), @@ -65,18 +60,12 @@ createDevApp() subtitle="Test the plugin below" /> -
+ -
+
), From 39a5bfb958f74f2a6d891c1946a1ec77decaa747 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 26 Jan 2022 10:53:42 +0000 Subject: [PATCH 12/42] Improved the icons Signed-off-by: Karan Shah --- .../dev/components/FlexColumn/FlexColumn.tsx | 34 ----------------- .../dev/components/FlexColumn/index.ts | 18 --------- plugins/airbrake/dev/index.tsx | 38 +++++++------------ 3 files changed, 14 insertions(+), 76 deletions(-) delete mode 100644 plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx delete mode 100644 plugins/airbrake/dev/components/FlexColumn/index.ts diff --git a/plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx b/plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx deleted file mode 100644 index aaf64d3513..0000000000 --- a/plugins/airbrake/dev/components/FlexColumn/FlexColumn.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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, { PropsWithChildren, ReactNode } from 'react'; - -export declare type FlexColumnProps = { - children?: ReactNode; -}; - -export const FlexColumn = (props: PropsWithChildren) => { - return ( -
- {props.children} -
- ); -}; diff --git a/plugins/airbrake/dev/components/FlexColumn/index.ts b/plugins/airbrake/dev/components/FlexColumn/index.ts deleted file mode 100644 index 5926c3cb28..0000000000 --- a/plugins/airbrake/dev/components/FlexColumn/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 { FlexColumn } from './FlexColumn'; -export type { FlexColumnProps } from './FlexColumn'; diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index ba520892cb..4163590069 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -21,9 +21,8 @@ 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/mock-entity'; -import AirplanemodeActiveIcon from '@material-ui/icons/AirplanemodeActive'; -import AirplanemodeInactiveIcon from '@material-ui/icons/AirplanemodeInactive'; -import { FlexColumn } from './components/FlexColumn'; +import CloudOffIcon from '@material-ui/icons/CloudOff'; +import CloudIcon from '@material-ui/icons/Cloud'; createDevApp() .registerPlugin(airbrakePlugin) @@ -35,42 +34,33 @@ createDevApp() .addPage({ element: ( -
+
- - - - - + + + ), title: 'Mock API', path: '/airbrake-mock-api', - icon: AirplanemodeActiveIcon, + icon: CloudOffIcon, }) .addPage({ element: ( -
+
+ +
- - - - - - + + + ), title: 'Real API', path: '/airbrake-real-api', - icon: AirplanemodeInactiveIcon, + icon: CloudIcon, }) .render(); From 1c2ba8ccdeeb83298aa601878ea7d246bb1837a2 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 27 Jan 2022 10:47:41 +0000 Subject: [PATCH 13/42] Added state to the top level for the real API Signed-off-by: Karan Shah --- .../airbrake/dev/components/ApiBar/ApiBar.tsx | 33 +++++++------ .../ContextProvider/ContextProvider.tsx | 48 +++++++++++++++++++ .../dev/components/ContextProvider/index.ts | 17 +++++++ plugins/airbrake/dev/index.tsx | 29 +++++++---- 4 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx create mode 100644 plugins/airbrake/dev/components/ContextProvider/index.ts diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx index 5c2b71ca14..bd8079420f 100644 --- a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React from 'react'; import { makeStyles, TextField } from '@material-ui/core'; +import { Context } from '../ContextProvider'; const useStyles = makeStyles({ root: { @@ -26,21 +27,23 @@ const useStyles = makeStyles({ export const ApiBar = () => { const classes = useStyles(); - const [projectId, setProjectId] = useState(); - const [apiKey, setApiKey] = useState(''); return ( -
- setProjectId(parseInt(e.target.value, 10))} - /> - setApiKey(e.target.value)} - /> -
+ + {value => ( +
+ value.setProjectId?.(parseInt(e.target.value, 10))} + /> + value.setApiKey?.(e.target.value)} + /> +
+ )} +
); }; diff --git a/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx b/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx new file mode 100644 index 0000000000..bbf72f3b99 --- /dev/null +++ b/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx @@ -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>; + apiKey?: String; + setApiKey?: Dispatch>; +} + +export const Context = React.createContext({}); + +export const ContextProvider = ({ children }: PropsWithChildren<{}>) => { + const [projectId, setProjectId] = useState(); + const [apiKey, setApiKey] = useState(''); + + return ( + + {children} + + ); +}; diff --git a/plugins/airbrake/dev/components/ContextProvider/index.ts b/plugins/airbrake/dev/components/ContextProvider/index.ts new file mode 100644 index 0000000000..f1e350868a --- /dev/null +++ b/plugins/airbrake/dev/components/ContextProvider/index.ts @@ -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'; diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index 4163590069..37b1810142 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -23,6 +23,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { createEntity } from '../src/api/mock/mock-entity'; import CloudOffIcon from '@material-ui/icons/CloudOff'; import CloudIcon from '@material-ui/icons/Cloud'; +import { ContextProvider, Context } from './components/ContextProvider'; createDevApp() .registerPlugin(airbrakePlugin) @@ -48,16 +49,24 @@ createDevApp() }) .addPage({ element: ( - -
- -
- - - - - -
+ + +
+ +
+ + + {value => ( + + + + )} + + +
+
), title: 'Real API', path: '/airbrake-real-api', From f3238a1a6c69f04a778bd9aef445c5eedf022ef9 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 27 Jan 2022 11:07:21 +0000 Subject: [PATCH 14/42] Make the project slug into a project ID Signed-off-by: Karan Shah --- plugins/airbrake/dev/components/ApiBar/ApiBar.tsx | 4 +++- plugins/airbrake/dev/index.tsx | 6 ++---- plugins/airbrake/src/api/mock/mock-entity.ts | 15 +++++++++------ .../EntityAirbrakeWidget.test.tsx | 2 +- .../EntityAirbrakeWidget/EntityAirbrakeWidget.tsx | 4 ++-- plugins/airbrake/src/components/useProjectSlug.ts | 4 ++-- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx index bd8079420f..8437559baf 100644 --- a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -35,7 +35,9 @@ export const ApiBar = () => { value.setProjectId?.(parseInt(e.target.value, 10))} + onChange={e => + value.setProjectId?.(parseInt(e.target.value, 10) || undefined) + } />
- + @@ -57,9 +57,7 @@ createDevApp() {value => ( - + )} diff --git a/plugins/airbrake/src/api/mock/mock-entity.ts b/plugins/airbrake/src/api/mock/mock-entity.ts index b169938004..7891478f51 100644 --- a/plugins/airbrake/src/api/mock/mock-entity.ts +++ b/plugins/airbrake/src/api/mock/mock-entity.ts @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AIRBRAKE_PROJECT_SLUG_ANNOTATION } from '../../components/useProjectSlug'; +import { AIRBRAKE_PROJECT_ID_ANNOTATION } from '../../components/useProjectSlug'; import { Entity } from '@backstage/catalog-model'; -export const createEntity = (name?: string) => - ({ +export const createEntity = (projectId?: number) => { + const projectIdString = projectId?.toString(); + + return { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { annotations: { - [AIRBRAKE_PROJECT_SLUG_ANNOTATION]: name, + [AIRBRAKE_PROJECT_ID_ANNOTATION]: projectIdString, }, - name: name, + name: projectIdString, }, - } as Entity); + } as Entity; +}; diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 3c63f84f19..5fd1aa9bf5 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -23,7 +23,7 @@ import { createEntity } from '../../api/mock/mock-entity'; describe('EntityAirbrakeContent', () => { it('renders all errors sent from Airbrake', async () => { const table = await renderInTestApp( - , + , ); expect(exampleData.groups.length).toBeGreaterThan(0); for (const group of exampleData.groups) { diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 53f7d63f13..543ebbefc0 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -29,7 +29,7 @@ import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { airbrakeApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { - AIRBRAKE_PROJECT_SLUG_ANNOTATION, + AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectSlug, } from '../useProjectSlug'; @@ -64,7 +64,7 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { {!loading && !projectId && ( )} diff --git a/plugins/airbrake/src/components/useProjectSlug.ts b/plugins/airbrake/src/components/useProjectSlug.ts index 4c6337b49e..07ba527c00 100644 --- a/plugins/airbrake/src/components/useProjectSlug.ts +++ b/plugins/airbrake/src/components/useProjectSlug.ts @@ -16,8 +16,8 @@ import { Entity } from '@backstage/catalog-model'; -export const AIRBRAKE_PROJECT_SLUG_ANNOTATION = 'airbrake.io/project-slug'; +export const AIRBRAKE_PROJECT_ID_ANNOTATION = 'airbrake.io/project-id'; export const useProjectSlug = (entity: Entity) => { - return entity?.metadata.annotations?.[AIRBRAKE_PROJECT_SLUG_ANNOTATION] ?? ''; + return entity?.metadata.annotations?.[AIRBRAKE_PROJECT_ID_ANNOTATION] ?? ''; }; From 6b5b9f2f1ba0c30a3344b687b9160e41fcb2c8aa Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 27 Jan 2022 14:21:27 +0000 Subject: [PATCH 15/42] Rename useProjectSlug.ts to useProjectId.ts Signed-off-by: Karan Shah --- plugins/airbrake/src/api/mock/mock-entity.ts | 2 +- .../EntityAirbrakeWidget/EntityAirbrakeWidget.tsx | 7 ++----- .../src/components/{useProjectSlug.ts => useProjectId.ts} | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) rename plugins/airbrake/src/components/{useProjectSlug.ts => useProjectId.ts} (93%) diff --git a/plugins/airbrake/src/api/mock/mock-entity.ts b/plugins/airbrake/src/api/mock/mock-entity.ts index 7891478f51..13534f2458 100644 --- a/plugins/airbrake/src/api/mock/mock-entity.ts +++ b/plugins/airbrake/src/api/mock/mock-entity.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AIRBRAKE_PROJECT_ID_ANNOTATION } from '../../components/useProjectSlug'; +import { AIRBRAKE_PROJECT_ID_ANNOTATION } from '../../components/useProjectId'; import { Entity } from '@backstage/catalog-model'; export const createEntity = (projectId?: number) => { diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 543ebbefc0..4564af6079 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -28,10 +28,7 @@ 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, - useProjectSlug, -} from '../useProjectSlug'; +import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; const useStyles = makeStyles(() => ({ multilineText: { @@ -42,7 +39,7 @@ const useStyles = makeStyles(() => ({ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { const classes = useStyles(); - const projectId = useProjectSlug(entity); + const projectId = useProjectId(entity); const errorApi = useApi(errorApiRef); const airbrakeApi = useApi(airbrakeApiRef); diff --git a/plugins/airbrake/src/components/useProjectSlug.ts b/plugins/airbrake/src/components/useProjectId.ts similarity index 93% rename from plugins/airbrake/src/components/useProjectSlug.ts rename to plugins/airbrake/src/components/useProjectId.ts index 07ba527c00..3402778b7b 100644 --- a/plugins/airbrake/src/components/useProjectSlug.ts +++ b/plugins/airbrake/src/components/useProjectId.ts @@ -18,6 +18,6 @@ import { Entity } from '@backstage/catalog-model'; export const AIRBRAKE_PROJECT_ID_ANNOTATION = 'airbrake.io/project-id'; -export const useProjectSlug = (entity: Entity) => { +export const useProjectId = (entity: Entity) => { return entity?.metadata.annotations?.[AIRBRAKE_PROJECT_ID_ANNOTATION] ?? ''; }; From f99d31d38526990abce378b7a1e61d2b8dbb158e Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 27 Jan 2022 15:23:42 +0000 Subject: [PATCH 16/42] Improved how the API Bar looks Signed-off-by: Karan Shah --- .../airbrake/dev/components/ApiBar/ApiBar.tsx | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx index 8437559baf..294e4506d1 100644 --- a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ import React from 'react'; -import { makeStyles, TextField } from '@material-ui/core'; +import { + createTheme, + makeStyles, + MuiThemeProvider, + TextField, +} from '@material-ui/core'; import { Context } from '../ContextProvider'; const useStyles = makeStyles({ @@ -25,6 +30,31 @@ const useStyles = makeStyles({ }, }); +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(); @@ -32,18 +62,22 @@ export const ApiBar = () => { {value => (
- - value.setProjectId?.(parseInt(e.target.value, 10) || undefined) - } - /> - value.setApiKey?.(e.target.value)} - /> + + + value.setProjectId?.(parseInt(e.target.value, 10) || undefined) + } + /> + value.setApiKey?.(e.target.value)} + /> +
)}
From 899b8d748fa414bbe1d0c5d1ea71cafda1a18431 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 31 Jan 2022 16:39:49 +0000 Subject: [PATCH 17/42] Connect up the production API Signed-off-by: Karan Shah --- .../airbrake/dev/components/ApiBar/ApiBar.tsx | 4 +-- .../ContextProvider/ContextProvider.tsx | 2 +- plugins/airbrake/dev/index.tsx | 34 ++++++++++++------- plugins/airbrake/src/api/airbrake-api.ts | 2 +- plugins/airbrake/src/api/production-api.ts | 13 +++---- .../EntityAirbrakeWidget.tsx | 4 +-- 6 files changed, 32 insertions(+), 27 deletions(-) diff --git a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx index 294e4506d1..eae00c2d32 100644 --- a/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx +++ b/plugins/airbrake/dev/components/ApiBar/ApiBar.tsx @@ -66,7 +66,7 @@ export const ApiBar = () => { value.setProjectId?.(parseInt(e.target.value, 10) || undefined) } @@ -74,7 +74,7 @@ export const ApiBar = () => { value.setApiKey?.(e.target.value)} /> diff --git a/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx b/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx index bbf72f3b99..6d7fe0f7f2 100644 --- a/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx +++ b/plugins/airbrake/dev/components/ContextProvider/ContextProvider.tsx @@ -23,7 +23,7 @@ import React, { interface ContextInterface { projectId?: number; setProjectId?: Dispatch>; - apiKey?: String; + apiKey?: string; setApiKey?: Dispatch>; } diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index c0a7c59fd9..7f32d30b4a 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -15,31 +15,33 @@ */ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; +import { TestApiProvider } from '@backstage/test-utils'; import { airbrakePlugin, EntityAirbrakeContent } from '../src'; -import { airbrakeApiRef, MockAirbrakeApi } from '../src/api'; +import { + 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/mock-entity'; import CloudOffIcon from '@material-ui/icons/CloudOff'; import CloudIcon from '@material-ui/icons/Cloud'; -import { ContextProvider, Context } from './components/ContextProvider'; +import { Context, ContextProvider } from './components/ContextProvider'; createDevApp() .registerPlugin(airbrakePlugin) - .registerApi({ - api: airbrakeApiRef, - deps: {}, - factory: () => new MockAirbrakeApi(), - }) .addPage({ element: (
- - - + + + + + ), @@ -57,9 +59,15 @@ createDevApp() {value => ( - - - + + + + + )} diff --git a/plugins/airbrake/src/api/airbrake-api.ts b/plugins/airbrake/src/api/airbrake-api.ts index 1a3cfa72e4..3e3fe123b9 100644 --- a/plugins/airbrake/src/api/airbrake-api.ts +++ b/plugins/airbrake/src/api/airbrake-api.ts @@ -22,5 +22,5 @@ export const airbrakeApiRef = createApiRef({ }); export interface AirbrakeApi { - fetchGroups(project: string): Promise; + fetchGroups(projectId: string): Promise; } diff --git a/plugins/airbrake/src/api/production-api.ts b/plugins/airbrake/src/api/production-api.ts index 37ac938752..cd8d2f95bf 100644 --- a/plugins/airbrake/src/api/production-api.ts +++ b/plugins/airbrake/src/api/production-api.ts @@ -16,20 +16,17 @@ import { Groups } from './airbrake-groups'; import { AirbrakeApi } from './airbrake-api'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; export class ProductionAirbrakeApi implements AirbrakeApi { - constructor(private readonly discoveryApi: DiscoveryApi) {} + constructor(private readonly apiKey?: string) {} - async fetchGroups(project: string): Promise { - const apiUrl = `${await this.discoveryApi.getBaseUrl( - 'proxy', - )}/airbrake/api`; + async fetchGroups(projectId: string): Promise { + const apiUrl = `https://api.airbrake.io/api/v4/projects/${projectId}/groups?key=${this.apiKey}`; - const response = await fetch(`${apiUrl}/v4/projects/${project}/groups`); + const response = await fetch(apiUrl); if (response.status >= 400 && response.status < 600) { - throw new Error('Failed fetching Airbrake issues'); + throw new Error('Failed fetching Airbrake groups'); } return (await response.json()) as Groups; diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 4564af6079..b11116aeef 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -45,7 +45,7 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { const { loading, value, error } = useAsync( () => airbrakeApi.fetchGroups(projectId), - [projectId], + [airbrakeApi, projectId], ); useEffect(() => { @@ -69,7 +69,7 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { )} From 3193d9d3bb1bdb1212726442bb0bdbf8de068684 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Feb 2022 11:03:33 +0000 Subject: [PATCH 18/42] Add back catalog model and catalog plugin react Signed-off-by: Karan Shah --- plugins/airbrake/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index e037e38009..2ccb7f31b8 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -20,8 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.9.10", "@backstage/core-components": "^0.8.6", "@backstage/core-plugin-api": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.12", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", From 6c3a7ad51fef61c550f7e47f4bb6a3b087ee6783 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Feb 2022 15:24:25 +0000 Subject: [PATCH 19/42] Remove Discovery API Ref Signed-off-by: Karan Shah --- plugins/airbrake/src/plugin.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugins/airbrake/src/plugin.ts b/plugins/airbrake/src/plugin.ts index 5a58f5b4b3..f008b48ad4 100644 --- a/plugins/airbrake/src/plugin.ts +++ b/plugins/airbrake/src/plugin.ts @@ -13,11 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - createApiFactory, - createPlugin, - discoveryApiRef, -} from '@backstage/core-plugin-api'; +import { createApiFactory, createPlugin } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; import { airbrakeApiRef, ProductionAirbrakeApi } from './api'; @@ -27,10 +23,8 @@ export const airbrakePlugin = createPlugin({ apis: [ createApiFactory({ api: airbrakeApiRef, - deps: { - discoveryApi: discoveryApiRef, - }, - factory: ({ discoveryApi }) => new ProductionAirbrakeApi(discoveryApi), + deps: {}, + factory: () => new ProductionAirbrakeApi(), }), ], routes: { From 9e505d20a340879289384684ccbdf24b0edb7464 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Feb 2022 15:56:46 +0000 Subject: [PATCH 20/42] Added a changeset Signed-off-by: Karan Shah --- .changeset/two-elephants-arrive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-elephants-arrive.md diff --git a/.changeset/two-elephants-arrive.md b/.changeset/two-elephants-arrive.md new file mode 100644 index 0000000000..6a1e713602 --- /dev/null +++ b/.changeset/two-elephants-arrive.md @@ -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. From 499f8f547eb613efc24d221f83b4bba0229d1871 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Feb 2022 17:18:22 +0000 Subject: [PATCH 21/42] Add some routes to the extensions test Signed-off-by: Karan Shah --- plugins/airbrake/src/extensions.test.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/airbrake/src/extensions.test.tsx b/plugins/airbrake/src/extensions.test.tsx index db8670ac50..5532621848 100644 --- a/plugins/airbrake/src/extensions.test.tsx +++ b/plugins/airbrake/src/extensions.test.tsx @@ -15,11 +15,23 @@ */ import React from 'react'; import { EntityAirbrakeContent } from './extensions'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { airbrakeApiRef, MockAirbrakeApi } from './api'; +import { rootRouteRef } from './routes'; describe('The Airbrake entity', () => { it('should render the content properly', async () => { - const rendered = await renderInTestApp(); + const rendered = await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + routeEntries: ['/'], + }, + ); expect(rendered.getByText('ChunkLoadError')).toBeInTheDocument(); }); }); From c0d61279af21ff72d5da138aed829a6916e3f743 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Fri, 4 Feb 2022 10:50:50 +0000 Subject: [PATCH 22/42] Fix extensions.test.tsx Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 2 +- plugins/airbrake/src/api/mock/mock-api.ts | 8 +++++++- plugins/airbrake/src/extensions.test.tsx | 18 +++++++++--------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index 7f32d30b4a..8fc43a73a1 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -37,7 +37,7 @@ createDevApp()
- + diff --git a/plugins/airbrake/src/api/mock/mock-api.ts b/plugins/airbrake/src/api/mock/mock-api.ts index df0293da7c..4acfa51778 100644 --- a/plugins/airbrake/src/api/mock/mock-api.ts +++ b/plugins/airbrake/src/api/mock/mock-api.ts @@ -19,9 +19,15 @@ import { AirbrakeApi } from '../airbrake-api'; import mockGroupsData from './airbrake-groups-api-mock.json'; export class MockAirbrakeApi implements AirbrakeApi { + waitTimeInMillis: number; + + constructor(waitTimeInMillis = 10) { + this.waitTimeInMillis = waitTimeInMillis; + } + fetchGroups(): Promise { return new Promise(resolve => { - setTimeout(() => resolve(mockGroupsData), 800); + setTimeout(() => resolve(mockGroupsData), this.waitTimeInMillis); }); } } diff --git a/plugins/airbrake/src/extensions.test.tsx b/plugins/airbrake/src/extensions.test.tsx index 5532621848..3b2d1927d9 100644 --- a/plugins/airbrake/src/extensions.test.tsx +++ b/plugins/airbrake/src/extensions.test.tsx @@ -15,23 +15,23 @@ */ 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 { rootRouteRef } from './routes'; +import { createEntity } from './api/mock/mock-entity'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('The Airbrake entity', () => { it('should render the content properly', async () => { const rendered = await renderInTestApp( - + + } /> + , - { - mountedRoutes: { - '/': rootRouteRef, - }, - routeEntries: ['/'], - }, ); - expect(rendered.getByText('ChunkLoadError')).toBeInTheDocument(); + await expect( + rendered.findByText('ChunkLoadError'), + ).resolves.toBeInTheDocument(); }); }); From f7ad6f0795c7f20c53856b9027aa72881cfe3796 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Fri, 4 Feb 2022 11:59:40 +0000 Subject: [PATCH 23/42] Fix EntityAirbrakeWidget.test.tsx Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 5fd1aa9bf5..7b3bd6595b 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -17,13 +17,16 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { createEntity } from '../../api/mock/mock-entity'; +import { airbrakeApiRef, MockAirbrakeApi } from '../../api'; describe('EntityAirbrakeContent', () => { it('renders all errors sent from Airbrake', async () => { const table = await renderInTestApp( - , + + + , ); expect(exampleData.groups.length).toBeGreaterThan(0); for (const group of exampleData.groups) { From e8731ffbc6ddd19d61901ffe1a417bde6f8f7157 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Fri, 4 Feb 2022 14:06:18 +0000 Subject: [PATCH 24/42] Added a test for the annotation missing component Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget.test.tsx | 12 ++++++++++++ plugins/airbrake/src/plugin.test.ts | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 7b3bd6595b..5baabe92f6 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -35,4 +35,16 @@ describe('EntityAirbrakeContent', () => { ).toBeInTheDocument(); } }); + + it('states that the annotation is missing if no annotation is provided', async () => { + const table = await renderInTestApp( + + + , + ); + expect(exampleData.groups.length).toBeGreaterThan(0); + await expect( + table.findByText('Missing Annotation'), + ).resolves.toBeInTheDocument(); + }); }); diff --git a/plugins/airbrake/src/plugin.test.ts b/plugins/airbrake/src/plugin.test.ts index c8e9c30e83..ad7865713f 100644 --- a/plugins/airbrake/src/plugin.test.ts +++ b/plugins/airbrake/src/plugin.test.ts @@ -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); + }); }); From e76d563fe0b586b7912a650041c513d4fa1194b3 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 7 Feb 2022 15:42:26 +0000 Subject: [PATCH 25/42] Added production-api.test.ts Signed-off-by: Karan Shah --- plugins/airbrake/package.json | 9 ++-- .../airbrake/src/api/production-api.test.ts | 50 +++++++++++++++++++ yarn.lock | 20 ++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 plugins/airbrake/src/api/production-api.test.ts diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 92e142c1a9..1c38212f36 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -28,14 +28,13 @@ "@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.6", "@backstage/cli": "^0.13.1", "@backstage/core-app-api": "^0.5.2", @@ -46,8 +45,10 @@ "@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.0.6", + "msw": "^0.35.0", + "nock": "^13.2.4", "react-router": "6.0.0-beta.0" }, "files": [ diff --git a/plugins/airbrake/src/api/production-api.test.ts b/plugins/airbrake/src/api/production-api.test.ts new file mode 100644 index 0000000000..89b7908f9a --- /dev/null +++ b/plugins/airbrake/src/api/production-api.test.ts @@ -0,0 +1,50 @@ +/* + * 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 './production-api'; +import nock from 'nock'; +import mockGroupsData from './mock/airbrake-groups-api-mock.json'; + +describe('The production Airbrake API', () => { + let productionApi: ProductionAirbrakeApi; + + beforeEach(() => { + productionApi = new ProductionAirbrakeApi('fakeApiKey'); + }); + + it('fetches groups using the provided project ID', async () => { + const scope = nock('https://api.airbrake.io') + .get('/api/v4/projects/123456/groups?key=fakeApiKey') + .reply(200, mockGroupsData); + expect(scope.isDone()).toBe(false); + + const groups = await productionApi.fetchGroups('123456'); + + expect(scope.isDone()).toBe(true); + expect(groups).toStrictEqual(mockGroupsData); + }); + + it('throws if fetching groups was unsuccessful', async () => { + const scope = nock('https://api.airbrake.io') + .get('/api/v4/projects/123456/groups?key=fakeApiKey') + .reply(500, mockGroupsData); + expect(scope.isDone()).toBe(false); + + await expect(productionApi.fetchGroups('123456')).rejects.toThrow(); + + expect(scope.isDone()).toBe(true); + }); +}); diff --git a/yarn.lock b/yarn.lock index 83e5796a6e..efe58ad8f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16155,6 +16155,11 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -17658,6 +17663,16 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +nock@^13.2.4: + version "13.2.4" + resolved "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz#43a309d93143ee5cdcca91358614e7bde56d20e1" + integrity sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-abort-controller@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" @@ -19759,6 +19774,11 @@ prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, object-assign "^4.1.1" react-is "^16.13.1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + properties-reader@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/properties-reader/-/properties-reader-2.2.0.tgz#41d837fe143d8d5f2386b6a869a1975c0b2c595c" From 82641a96d40fafadfa5ab61a8e11605756ffa653 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 7 Feb 2022 16:25:49 +0000 Subject: [PATCH 26/42] Fleshing out more tests with API failures Signed-off-by: Karan Shah --- .../airbrake/src/api/production-api.test.ts | 2 +- .../EntityAirbrakeWidget.test.tsx | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/plugins/airbrake/src/api/production-api.test.ts b/plugins/airbrake/src/api/production-api.test.ts index 89b7908f9a..3753cab592 100644 --- a/plugins/airbrake/src/api/production-api.test.ts +++ b/plugins/airbrake/src/api/production-api.test.ts @@ -40,7 +40,7 @@ describe('The production Airbrake API', () => { it('throws if fetching groups was unsuccessful', async () => { const scope = nock('https://api.airbrake.io') .get('/api/v4/projects/123456/groups?key=fakeApiKey') - .reply(500, mockGroupsData); + .reply(500); expect(scope.isDone()).toBe(false); await expect(productionApi.fetchGroups('123456')).rejects.toThrow(); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 5baabe92f6..5d3787b875 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -19,7 +19,12 @@ import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { createEntity } from '../../api/mock/mock-entity'; -import { airbrakeApiRef, MockAirbrakeApi } from '../../api'; +import { + airbrakeApiRef, + MockAirbrakeApi, + ProductionAirbrakeApi, +} from '../../api'; +import nock from 'nock'; describe('EntityAirbrakeContent', () => { it('renders all errors sent from Airbrake', async () => { @@ -36,15 +41,34 @@ describe('EntityAirbrakeContent', () => { } }); - it('states that the annotation is missing if no annotation is provided', async () => { + it('states that the annotation is missing if no project ID annotation is provided', async () => { const table = await renderInTestApp( , ); - expect(exampleData.groups.length).toBeGreaterThan(0); await expect( table.findByText('Missing Annotation'), ).resolves.toBeInTheDocument(); }); + + it('states that an error occurred if API call fails', async () => { + const scope = nock('https://api.airbrake.io') + .get('/api/v4/projects/123/groups?key=fakeApiKey') + .reply(500); + expect(scope.isDone()).toBe(false); + + const table = await renderInTestApp( + + + , + ); + + await expect( + table.findByText('*there was an issue communicating with Airbrake*'), + ).resolves.toBeInTheDocument(); + expect(scope.isDone()).toBe(true); + }); }); From db4ed55d9d380ff9851cad327a799d86c059202b Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 8 Feb 2022 10:03:15 +0000 Subject: [PATCH 27/42] Error test now passes Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget.test.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 5d3787b875..2415aa1bdf 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -17,7 +17,11 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + MockErrorApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { createEntity } from '../../api/mock/mock-entity'; import { airbrakeApiRef, @@ -25,6 +29,7 @@ import { ProductionAirbrakeApi, } from '../../api'; import nock from 'nock'; +import { errorApiRef } from '@backstage/core-plugin-api'; describe('EntityAirbrakeContent', () => { it('renders all errors sent from Airbrake', async () => { @@ -57,18 +62,26 @@ describe('EntityAirbrakeContent', () => { .get('/api/v4/projects/123/groups?key=fakeApiKey') .reply(500); expect(scope.isDone()).toBe(false); + const mockErrorApi = new MockErrorApi({ collect: true }); const table = await renderInTestApp( , ); await expect( - table.findByText('*there was an issue communicating with Airbrake*'), + table.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', + ); expect(scope.isDone()).toBe(true); }); }); From 81e19c612ae1e067c573afedd0f67b47661ad60c Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 8 Feb 2022 10:04:39 +0000 Subject: [PATCH 28/42] Minor refactoring to better name variables Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget.test.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 2415aa1bdf..5317d1469c 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -33,7 +33,7 @@ import { errorApiRef } from '@backstage/core-plugin-api'; describe('EntityAirbrakeContent', () => { it('renders all errors sent from Airbrake', async () => { - const table = await renderInTestApp( + const widget = await renderInTestApp( , @@ -41,30 +41,30 @@ describe('EntityAirbrakeContent', () => { expect(exampleData.groups.length).toBeGreaterThan(0); for (const group of exampleData.groups) { expect( - await table.getByText(group.errors[0].message), + await widget.getByText(group.errors[0].message), ).toBeInTheDocument(); } }); it('states that the annotation is missing if no project ID annotation is provided', async () => { - const table = await renderInTestApp( + const widget = await renderInTestApp( , ); await expect( - table.findByText('Missing Annotation'), + widget.findByText('Missing Annotation'), ).resolves.toBeInTheDocument(); }); - it('states that an error occurred if API call fails', async () => { + it('states that an error occurred if the API call fails', async () => { const scope = nock('https://api.airbrake.io') .get('/api/v4/projects/123/groups?key=fakeApiKey') .reply(500); expect(scope.isDone()).toBe(false); const mockErrorApi = new MockErrorApi({ collect: true }); - const table = await renderInTestApp( + const widget = await renderInTestApp( { ); await expect( - table.findByText(/.*there was an issue communicating with Airbrake.*/), + widget.findByText(/.*there was an issue communicating with Airbrake.*/), ).resolves.toBeInTheDocument(); expect(mockErrorApi.getErrors().length).toBe(1); expect(mockErrorApi.getErrors()[0].error.message).toStrictEqual( From 821bebdff781fcccb5380d5577a565f90d3fa9cd Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 7 Feb 2022 16:21:09 +0100 Subject: [PATCH 29/42] complete instructions on setting up custom homepage Signed-off-by: Himanshu Mishra --- .../getting-started/sidebar-with-catalog.png | Bin 0 -> 18065 bytes .../sidebar-without-catalog.png | Bin 0 -> 16689 bytes .../getting-started/simple-homepage.png | Bin 0 -> 79587 bytes docs/getting-started/app-custom-theme.md | 69 -------- docs/getting-started/homepage.md | 150 ++++++++++++++++++ ...2022-01-25-backstage-homepage-templates.md | 2 +- microsite/sidebars.json | 3 +- 7 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 docs/assets/getting-started/sidebar-with-catalog.png create mode 100644 docs/assets/getting-started/sidebar-without-catalog.png create mode 100644 docs/assets/getting-started/simple-homepage.png create mode 100644 docs/getting-started/homepage.md diff --git a/docs/assets/getting-started/sidebar-with-catalog.png b/docs/assets/getting-started/sidebar-with-catalog.png new file mode 100644 index 0000000000000000000000000000000000000000..d80346c21189e3905a96f3ff67fc6d87ee8734ac GIT binary patch literal 18065 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 Rx*y)$RhCzmE0r-1{U5w- literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/sidebar-without-catalog.png b/docs/assets/getting-started/sidebar-without-catalog.png new file mode 100644 index 0000000000000000000000000000000000000000..bfeab13235d1961443cf547629c7c89b54329783 GIT binary patch literal 16689 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©pEI=z_AO~w7$J0dl%;cu73>Q{TiPF(haM>C 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 RMuC6Zw;6BE*ld6De*iqZpGW`z literal 0 HcmV?d00001 diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index fc68008ae8..b9b74f92dd 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -253,72 +253,3 @@ const LogoFull = () => { return ; }; ``` - -## Custom Homepage - -In addition to a custom theme, a custom logo, you can also customize the -homepage of your app. To do that we need to go through a few steps. - -### Setting up the Home Page - -1. Create a Home Page Component that will be used for composition. - -`packages/app/src/components/home/HomePage.tsx` - -```tsx -import React from 'react'; - -export const HomePage = () => { - return { - /* TODO: Compose a Home Page here */ - }; -}; -``` - -2. Add a route where the homepage will live, presumably `/`. - -`packages/app/src/App.tsx` - -```tsx -import { HomepageCompositionRoot } from '@backstage/plugin-home'; -import { HomePage } from './components/home/HomePage'; - -// ... -}> - -; -// ... -``` - -### Composing your Home Page - -Composing a Home Page is no different from creating a regular React Component, -i.e. the App Integrator is free to include whatever content they like. However, -there are components developed with the Home Page in mind. If you are looking -for components to use when composing your homepage, you can take a look at the -[collection of Homepage components](https://backstage.io/?path=/story/plugins-home-components) -in storybook. If you don't find a component that suits your needs but want to -contribute, check the -[Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing). - -> If you want to use one of the available homepage templates you can find the -> [templates](https://backstage.io/storybook/?path=/story/plugins-home-templates) -> in the storybook under the "Home" plugin. And if you would like to contribute -> a template, please see the -> [Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing) - -```tsx -import React from 'react'; -import Grid from '@material-ui/core/Grid'; -import { HomePageCompanyLogo } from '@backstage/plugin-home'; - -export const HomePage = () => { - return ( - - - - - - ); -}; -``` diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md new file mode 100644 index 0000000000..175d539d68 --- /dev/null +++ b/docs/getting-started/homepage.md @@ -0,0 +1,150 @@ +--- +id: homepage +title: Backstage Home Page - Setup and Customization +description: Documentation on setting up and customizing Backstage homepage +--- + +## Home Page + +Having a good Backstage home page can significantly improve the discoverability of the platform. You want your users to find all the things they need right from the home page and never have to remember direct URLs in Backstage. The [Home plugin](https://github.com/backstage/backstage/tree/master/plugins/home) introduces a system for composing a home page for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any home page. + +For App Integrators, the system is designed to be composable to give total freedom in designing a Home Page that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Home Pages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. + +Let's get started by installing the home plugin and creating a simple home page for your Backstage app. + +### Setup home page + +#### 1. Install the plugin + +``` +# From the Backstage repository root +cd packages/app +yarn add @backstage/plugin-home +``` + +#### 2. Create a new HomePage component + +Inside your `packages/app`, create a new file where our new Home Page component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code + +```tsx +import React from 'react'; + +export const HomePage = () => { + /* We will shortly compose a pretty Home Page here. */ + return

Welcome to Backstage!

; +}; +``` + +#### 3. Update router for the root `/` route + +If you don't have a home page already, most likely you have a redirect setup to use the Catalog home page as a home page. + +Inside your `packages/app/src/App.tsx`, look for + +```tsx +const routes = ( + + +``` + +Let's replace the `` line and use the new component we created in the previous step as the new home page. + +```diff +// File: packages/app/src/App.tsx + ++ import { HomepageCompositionRoot } from '@backstage/plugin-home'; ++ import { HomePage } from './components/home/HomePage'; + +// ... +const routes = ( + +- ++ }> ++ ++ +// ... + +) +``` + +### 4. Update sidebar items + +Let's update the route for "Home" in the Backstage sidebar to point to the new home page. We'll also add a Sidebar item to quickly open Catalog. + + + + + + + + +
Sidebar without Catalog + Sidebar with Catalog +
BeforeAfter
+ +The code for the Backstage sidebar is most likely inside your [`packages/app/src/components/Root/Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx). + +Let's make the following changes + +```diff +// Other imports ++ import CategoryIcon from '@material-ui/icons/Category'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + + # ... + }> + {/* Global nav, not org-specific */} +- ++ ++ + + + + + {/* End global nav */} + +``` + +That's it! You should now have _(although slightly boring)_ a home page! + +Screenshot of a blank homepage + +In the next steps, we will make it interesting and useful! There is a default home page template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our home page. + +#### 2. Use the default template + +### Composing your Home Page + +Composing a Home Page is no different from creating a regular React Component, +i.e. the App Integrator is free to include whatever content they like. However, +there are components developed with the Home Page in mind. If you are looking +for components to use when composing your homepage, you can take a look at the +[collection of Homepage components](https://backstage.io/?path=/story/plugins-home-components) +in storybook. If you don't find a component that suits your needs but want to +contribute, check the +[Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing). + +> If you want to use one of the available homepage templates you can find the +> [templates](https://backstage.io/storybook/?path=/story/plugins-home-templates) +> in the storybook under the "Home" plugin. And if you would like to contribute +> a template, please see the +> [Contributing documentation](https://github.com/backstage/backstage/blob/master/plugins/home/README.md#contributing) + +```tsx +import React from 'react'; +import Grid from '@material-ui/core/Grid'; +import { HomePageCompanyLogo } from '@backstage/plugin-home'; + +export const HomePage = () => { + return ( + + + + + + ); +}; +``` diff --git a/microsite/blog/2022-01-25-backstage-homepage-templates.md b/microsite/blog/2022-01-25-backstage-homepage-templates.md index 7ed5ee92e3..8ca8b7373e 100644 --- a/microsite/blog/2022-01-25-backstage-homepage-templates.md +++ b/microsite/blog/2022-01-25-backstage-homepage-templates.md @@ -33,7 +33,7 @@ _Example of the default homepage template in Backstage Storybook_ ## How to get started -To get started, you first need to set up your homepage plugin in the app if you have not already, then you can move on to composing your homepage. You can find our step-by-step documentation of how to do this under the [App Configuration - Customize the look-and-feel of your App documentation](https://backstage.io/docs/getting-started/app-custom-theme#custom-homepage). You can also access the [Backstage UI Kit in Figma](https://www.figma.com/file/nUaAw56hTgC0RIOYkuLSrB/Backstage-Design-System?node-id=2185%3A2978) to duplicate your own version and play around with a fitting homepage for your own organization. +To get started, you first need to set up your homepage plugin in the app if you have not already, then you can move on to composing your homepage. You can find our step-by-step documentation of how to do this under the [App Configuration - Customize the look-and-feel of your App documentation](https://backstage.io/docs/getting-started/homepage). You can also access the [Backstage UI Kit in Figma](https://www.figma.com/file/nUaAw56hTgC0RIOYkuLSrB/Backstage-Design-System?node-id=2185%3A2978) to duplicate your own version and play around with a fitting homepage for your own organization. ![Example of homepage components in the Backstage UI Kit in Figma](assets/22-01-25/homepage-components-figma.png) _Example of the homepage components in the Backstage UI Kit in Figma_ diff --git a/microsite/sidebars.json b/microsite/sidebars.json index cb7c0d5036..5369182013 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -22,7 +22,8 @@ "label": "App configuration", "ids": [ "getting-started/configure-app-with-plugins", - "getting-started/app-custom-theme" + "getting-started/app-custom-theme", + "getting-started/homepage" ] }, "getting-started/keeping-backstage-updated", From e0ca377789da85a0e25bcc6ecec80581bc81416b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 7 Feb 2022 16:28:53 +0100 Subject: [PATCH 30/42] add section about the homepage template Signed-off-by: Himanshu Mishra --- docs/getting-started/homepage.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 175d539d68..4b48929b9a 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -112,9 +112,13 @@ That's it! You should now have _(although slightly boring)_ a home page! Screenshot of a blank homepage -In the next steps, we will make it interesting and useful! There is a default home page template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our home page. +In the next steps, we will make it interesting and useful! -#### 2. Use the default template +### Use the default template + +There is a default home page template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our home page. Checkout the [blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) about the Backstage home page templates for more information. + + ### Composing your Home Page From f87a740a6a97562e9fff66412686dd7ab5ecbd6a Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 9 Feb 2022 10:54:51 +0100 Subject: [PATCH 31/42] keep the custom homepage section in app custom theme page Signed-off-by: Himanshu Mishra --- docs/getting-started/app-custom-theme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index b9b74f92dd..526150d184 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -253,3 +253,8 @@ const LogoFull = () => { return ; }; ``` + +## Custom Home Page + +In addition to a custom theme, a custom logo, you can also customize the +homepage of your app. Read the full guide on the [next page](homepage.md). From 805f8acfd0cf45f808377384b69623818e1005ad Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 9 Feb 2022 10:59:54 +0100 Subject: [PATCH 32/42] `packages/app` directory Signed-off-by: Himanshu Mishra --- docs/getting-started/homepage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 4b48929b9a..cb3a43b1fd 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -24,7 +24,7 @@ yarn add @backstage/plugin-home #### 2. Create a new HomePage component -Inside your `packages/app`, create a new file where our new Home Page component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code +Inside your `packages/app` directory, create a new file where our new Home Page component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code ```tsx import React from 'react'; From d3fcf47a962a19a7e6de47731814efcf5587f00d Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 9 Feb 2022 11:02:54 +0100 Subject: [PATCH 33/42] docs: use Backstage application directory Signed-off-by: Himanshu Mishra --- docs/getting-started/homepage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index cb3a43b1fd..c497610e6e 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -17,7 +17,7 @@ Let's get started by installing the home plugin and creating a simple home page #### 1. Install the plugin ``` -# From the Backstage repository root +# From your Backstage application directory cd packages/app yarn add @backstage/plugin-home ``` From cc274eb7baa41ff736189c9bc82eae6f32389d43 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 9 Feb 2022 11:49:28 +0100 Subject: [PATCH 34/42] add pre-reqs and expectations of the tutorial Signed-off-by: Himanshu Mishra --- docs/getting-started/homepage.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index c497610e6e..87cf741b15 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -10,7 +10,19 @@ Having a good Backstage home page can significantly improve the discoverability For App Integrators, the system is designed to be composable to give total freedom in designing a Home Page that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Home Pages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. -Let's get started by installing the home plugin and creating a simple home page for your Backstage app. +At the end of this tutorial, you can expect: + +- Your Backstage app to have a dedicated home page instead of Software Catalog. +- Understand the composability of home page and how to start customizing it for your own organization. + +### Prerequisites + +Before we begin, make sure + +- You have created your own standalone Backstage app using [`@backstage/create-app`](index.md#create-your-backstage-app) and not using a fork of the [backstage](https://github.com/backstage/backstage) repository. +- You do not have an existing home page, and by default you are redirected to Software Catalog when you open Backstage. + +Now, let's get started by installing the home plugin and creating a simple home page for your Backstage app. ### Setup home page @@ -118,7 +130,7 @@ In the next steps, we will make it interesting and useful! There is a default home page template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our home page. Checkout the [blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) about the Backstage home page templates for more information. - + ### Composing your Home Page From 6cb193343af1a84bf2b0022ec12c04bbd0611ff7 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 9 Feb 2022 16:04:08 +0000 Subject: [PATCH 35/42] Update yarn.lock Signed-off-by: Karan Shah --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index d7521577c4..7461cc24f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,7 +1440,7 @@ "@material-ui/lab" "4.0.0-alpha.57" react-use "^17.2.4" -"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5": +"@backstage/plugin-catalog-react@^0.6.12", "@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5": version "0.6.13" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.13.tgz#b325eae501d3edeb8b7caef5d9615f2e632f5430" integrity sha512-XBwop7PwAZqfongx3KP6jAJar+MEscLSp8nLuHYX5XxA+suQNiBgi96uO3SEQmvtae+hvsRM7c0WHSxbYiXsDA== From 7a8ac1db0bc78ca3ef4d7e89ec483c4f7f816780 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 9 Feb 2022 16:07:15 +0000 Subject: [PATCH 36/42] Revert ADOPTERS.md Signed-off-by: Karan Shah --- ADOPTERS.md | 188 ++++++++++++++++++++++++++-------------------------- 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index d70ffe4cc0..beb161d9ee 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,94 +1,94 @@ -| Organization | Contact | Description of Use | -| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | -| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | -| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | -| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | -| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | -| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | -| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | -| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | -| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | -| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | -| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | -| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | -| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | -| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | -| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | -| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. | -| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | -| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | -| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | -| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | -| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | -| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | -| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | -| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | -| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | -| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | -| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | -| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | -| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | -| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | -| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | -| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | -| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | -| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | -| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | -| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | -| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | -| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | -| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | -| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | -| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time | -| [Power Home Remodeling](https://www.techatpower.com) | [Ben Langfeld](https://github.com/benlangfeld) | Developer portal to our internal services, build on open-source software (including Kubernetes) in our own datacenters. Our Portal allows our team members to navigate inherant complexity and standardise. | -| [Livspace](https://www.livspace.com) | [Praveen Kumar](https://github.com/praveen-livspace) | Developer portal, service catalog, tech docs, API docs and plugins | -| [Just Eat Takeaway](https://www.justeattakeaway.com) | [Kim Wilson](https://github.com/kwilson541) | Our developer portal which centralises applications, reduces cognitive load and provides teams insights. | -| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. | -| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. | -| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform | -| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. | -| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal | -| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality | -| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | -| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. | -| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. | +| Organization | Contact | Description of Use | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | +| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | +| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | +| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | +| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | +| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | +| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | +| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. | +| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | +| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | +| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | +| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | +| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | +| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | +| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | +| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | +| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | +| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | +| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | +| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | +| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | +| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | +| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | +| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | +| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | +| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | +| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | +| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | +| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | +| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | +| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time | +| [Power Home Remodeling](https://www.techatpower.com) | [Ben Langfeld](https://github.com/benlangfeld) | Developer portal to our internal services, build on open-source software (including Kubernetes) in our own datacenters. Our Portal allows our team members to navigate inherant complexity and standardise. | +| [Livspace](https://www.livspace.com) | [Praveen Kumar](https://github.com/praveen-livspace) | Developer portal, service catalog, tech docs, API docs and plugins | +| [Just Eat Takeaway](https://www.justeattakeaway.com) | [Kim Wilson](https://github.com/kwilson541) | Our developer portal which centralises applications, reduces cognitive load and provides teams insights. | +| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. | +| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. | +| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform | +| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. +| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal | +| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality | +| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | +| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. | +| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. | From b9e83843e276f3313c627737b7d4fcd2f47257ef Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 9 Feb 2022 16:19:58 +0000 Subject: [PATCH 37/42] Stop using snake case Signed-off-by: Karan Shah --- plugins/airbrake/dev/index.tsx | 2 +- plugins/airbrake/package.json | 5 ++--- .../airbrake/src/api/{airbrake-api.ts => AirbrakeApi.ts} | 2 +- .../api/{production-api.test.ts => ProductionApi.test.ts} | 4 ++-- .../src/api/{production-api.ts => ProductionApi.ts} | 4 ++-- .../src/api/{airbrake-groups.d.ts => airbrakeGroups.d.ts} | 0 plugins/airbrake/src/api/index.ts | 8 ++++---- plugins/airbrake/src/api/mock/{mock-api.ts => MockApi.ts} | 6 +++--- .../src/api/mock/{mock-entity.ts => MockEntity.ts} | 0 ...ke-groups-api-mock.json => airbrakeGroupsApiMock.json} | 0 plugins/airbrake/src/api/mock/index.ts | 2 +- .../EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx | 4 ++-- plugins/airbrake/src/extensions.test.tsx | 2 +- 13 files changed, 19 insertions(+), 20 deletions(-) rename plugins/airbrake/src/api/{airbrake-api.ts => AirbrakeApi.ts} (95%) rename plugins/airbrake/src/api/{production-api.test.ts => ProductionApi.test.ts} (92%) rename plugins/airbrake/src/api/{production-api.ts => ProductionApi.ts} (92%) rename plugins/airbrake/src/api/{airbrake-groups.d.ts => airbrakeGroups.d.ts} (100%) rename plugins/airbrake/src/api/mock/{mock-api.ts => MockApi.ts} (85%) rename plugins/airbrake/src/api/mock/{mock-entity.ts => MockEntity.ts} (100%) rename plugins/airbrake/src/api/mock/{airbrake-groups-api-mock.json => airbrakeGroupsApiMock.json} (100%) diff --git a/plugins/airbrake/dev/index.tsx b/plugins/airbrake/dev/index.tsx index 8fc43a73a1..cd0d29301f 100644 --- a/plugins/airbrake/dev/index.tsx +++ b/plugins/airbrake/dev/index.tsx @@ -25,7 +25,7 @@ import { 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/mock-entity'; +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'; diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 220873dbea..1ebd99434e 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -23,7 +23,9 @@ "@backstage/catalog-model": "^0.9.10", "@backstage/core-components": "^0.8.8-next.0", "@backstage/core-plugin-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.21-next.0", "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/test-utils": "^0.2.4", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,12 +37,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@types/object-hash": "^2.2.1", "@backstage/app-defaults": "^0.1.7-next.0", "@backstage/cli": "^0.13.2-next.0", "@backstage/core-app-api": "^0.5.2", - "@backstage/dev-utils": "^0.2.21-next.0", - "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/airbrake/src/api/airbrake-api.ts b/plugins/airbrake/src/api/AirbrakeApi.ts similarity index 95% rename from plugins/airbrake/src/api/airbrake-api.ts rename to plugins/airbrake/src/api/AirbrakeApi.ts index 3e3fe123b9..a29aa3eca7 100644 --- a/plugins/airbrake/src/api/airbrake-api.ts +++ b/plugins/airbrake/src/api/AirbrakeApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Groups } from './airbrake-groups'; +import { Groups } from './airbrakeGroups'; import { createApiRef } from '@backstage/core-plugin-api'; export const airbrakeApiRef = createApiRef({ diff --git a/plugins/airbrake/src/api/production-api.test.ts b/plugins/airbrake/src/api/ProductionApi.test.ts similarity index 92% rename from plugins/airbrake/src/api/production-api.test.ts rename to plugins/airbrake/src/api/ProductionApi.test.ts index 3753cab592..d978cd82a0 100644 --- a/plugins/airbrake/src/api/production-api.test.ts +++ b/plugins/airbrake/src/api/ProductionApi.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ProductionAirbrakeApi } from './production-api'; +import { ProductionAirbrakeApi } from './ProductionApi'; import nock from 'nock'; -import mockGroupsData from './mock/airbrake-groups-api-mock.json'; +import mockGroupsData from './mock/airbrakeGroupsApiMock.json'; describe('The production Airbrake API', () => { let productionApi: ProductionAirbrakeApi; diff --git a/plugins/airbrake/src/api/production-api.ts b/plugins/airbrake/src/api/ProductionApi.ts similarity index 92% rename from plugins/airbrake/src/api/production-api.ts rename to plugins/airbrake/src/api/ProductionApi.ts index cd8d2f95bf..76416bc173 100644 --- a/plugins/airbrake/src/api/production-api.ts +++ b/plugins/airbrake/src/api/ProductionApi.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Groups } from './airbrake-groups'; -import { AirbrakeApi } from './airbrake-api'; +import { Groups } from './airbrakeGroups'; +import { AirbrakeApi } from './AirbrakeApi'; export class ProductionAirbrakeApi implements AirbrakeApi { constructor(private readonly apiKey?: string) {} diff --git a/plugins/airbrake/src/api/airbrake-groups.d.ts b/plugins/airbrake/src/api/airbrakeGroups.d.ts similarity index 100% rename from plugins/airbrake/src/api/airbrake-groups.d.ts rename to plugins/airbrake/src/api/airbrakeGroups.d.ts diff --git a/plugins/airbrake/src/api/index.ts b/plugins/airbrake/src/api/index.ts index 7bad94e298..2d92789653 100644 --- a/plugins/airbrake/src/api/index.ts +++ b/plugins/airbrake/src/api/index.ts @@ -15,7 +15,7 @@ */ export * from './mock'; -export type { AirbrakeApi } from './airbrake-api'; -export { airbrakeApiRef } from './airbrake-api'; -export type { Groups } from './airbrake-groups'; -export { ProductionAirbrakeApi } from './production-api'; +export type { AirbrakeApi } from './AirbrakeApi'; +export { airbrakeApiRef } from './AirbrakeApi'; +export type { Groups } from './airbrakeGroups'; +export { ProductionAirbrakeApi } from './ProductionApi'; diff --git a/plugins/airbrake/src/api/mock/mock-api.ts b/plugins/airbrake/src/api/mock/MockApi.ts similarity index 85% rename from plugins/airbrake/src/api/mock/mock-api.ts rename to plugins/airbrake/src/api/mock/MockApi.ts index 4acfa51778..dbb1d5c049 100644 --- a/plugins/airbrake/src/api/mock/mock-api.ts +++ b/plugins/airbrake/src/api/mock/MockApi.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Groups } from '../airbrake-groups'; -import { AirbrakeApi } from '../airbrake-api'; -import mockGroupsData from './airbrake-groups-api-mock.json'; +import { Groups } from '../airbrakeGroups'; +import { AirbrakeApi } from '../AirbrakeApi'; +import mockGroupsData from './airbrakeGroupsApiMock.json'; export class MockAirbrakeApi implements AirbrakeApi { waitTimeInMillis: number; diff --git a/plugins/airbrake/src/api/mock/mock-entity.ts b/plugins/airbrake/src/api/mock/MockEntity.ts similarity index 100% rename from plugins/airbrake/src/api/mock/mock-entity.ts rename to plugins/airbrake/src/api/mock/MockEntity.ts diff --git a/plugins/airbrake/src/api/mock/airbrake-groups-api-mock.json b/plugins/airbrake/src/api/mock/airbrakeGroupsApiMock.json similarity index 100% rename from plugins/airbrake/src/api/mock/airbrake-groups-api-mock.json rename to plugins/airbrake/src/api/mock/airbrakeGroupsApiMock.json diff --git a/plugins/airbrake/src/api/mock/index.ts b/plugins/airbrake/src/api/mock/index.ts index f084268951..0588d45fa6 100644 --- a/plugins/airbrake/src/api/mock/index.ts +++ b/plugins/airbrake/src/api/mock/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { MockAirbrakeApi } from './mock-api'; +export { MockAirbrakeApi } from './MockApi'; diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 5317d1469c..c242ff77f2 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -16,13 +16,13 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; -import exampleData from '../../api/mock/airbrake-groups-api-mock.json'; +import exampleData from '../../api/mock/airbrakeGroupsApiMock.json'; import { MockErrorApi, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; -import { createEntity } from '../../api/mock/mock-entity'; +import { createEntity } from '../../api/mock/MockEntity'; import { airbrakeApiRef, MockAirbrakeApi, diff --git a/plugins/airbrake/src/extensions.test.tsx b/plugins/airbrake/src/extensions.test.tsx index 3b2d1927d9..d677d939a4 100644 --- a/plugins/airbrake/src/extensions.test.tsx +++ b/plugins/airbrake/src/extensions.test.tsx @@ -18,7 +18,7 @@ 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/mock-entity'; +import { createEntity } from './api/mock/MockEntity'; import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('The Airbrake entity', () => { From 71a6648e74adfb0886ee80e733e9ad3e840c3ca4 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 10 Feb 2022 09:39:11 +0000 Subject: [PATCH 38/42] Move tests to MSW from Nock Signed-off-by: Karan Shah --- plugins/airbrake/package.json | 1 - .../airbrake/src/api/ProductionApi.test.ts | 42 +++++++++++-------- .../EntityAirbrakeWidget.test.tsx | 20 ++++++--- yarn.lock | 20 --------- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 1ebd99434e..36eb40f60b 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -48,7 +48,6 @@ "@types/object-hash": "^2.2.1", "cross-fetch": "^3.0.6", "msw": "^0.35.0", - "nock": "^13.2.4", "react-router": "6.0.0-beta.0" }, "files": [ diff --git a/plugins/airbrake/src/api/ProductionApi.test.ts b/plugins/airbrake/src/api/ProductionApi.test.ts index d978cd82a0..04bb73caf7 100644 --- a/plugins/airbrake/src/api/ProductionApi.test.ts +++ b/plugins/airbrake/src/api/ProductionApi.test.ts @@ -15,36 +15,44 @@ */ import { ProductionAirbrakeApi } from './ProductionApi'; -import nock from 'nock'; +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', () => { - let productionApi: ProductionAirbrakeApi; - - beforeEach(() => { - productionApi = new ProductionAirbrakeApi('fakeApiKey'); - }); + const productionApi = new ProductionAirbrakeApi('fakeApiKey'); + const worker = setupServer(); + setupRequestMockHandlers(worker); it('fetches groups using the provided project ID', async () => { - const scope = nock('https://api.airbrake.io') - .get('/api/v4/projects/123456/groups?key=fakeApiKey') - .reply(200, mockGroupsData); - expect(scope.isDone()).toBe(false); + 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(scope.isDone()).toBe(true); expect(groups).toStrictEqual(mockGroupsData); }); it('throws if fetching groups was unsuccessful', async () => { - const scope = nock('https://api.airbrake.io') - .get('/api/v4/projects/123456/groups?key=fakeApiKey') - .reply(500); - expect(scope.isDone()).toBe(false); + 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(); - - expect(scope.isDone()).toBe(true); }); }); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index c242ff77f2..ef1ac229c9 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -20,6 +20,7 @@ import exampleData from '../../api/mock/airbrakeGroupsApiMock.json'; import { MockErrorApi, renderInTestApp, + setupRequestMockHandlers, TestApiProvider, } from '@backstage/test-utils'; import { createEntity } from '../../api/mock/MockEntity'; @@ -28,10 +29,14 @@ import { MockAirbrakeApi, ProductionAirbrakeApi, } from '../../api'; -import nock from 'nock'; 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( @@ -58,10 +63,14 @@ describe('EntityAirbrakeContent', () => { }); it('states that an error occurred if the API call fails', async () => { - const scope = nock('https://api.airbrake.io') - .get('/api/v4/projects/123/groups?key=fakeApiKey') - .reply(500); - expect(scope.isDone()).toBe(false); + 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( @@ -82,6 +91,5 @@ describe('EntityAirbrakeContent', () => { expect(mockErrorApi.getErrors()[0].error.message).toStrictEqual( 'Failed fetching Airbrake groups', ); - expect(scope.isDone()).toBe(true); }); }); diff --git a/yarn.lock b/yarn.lock index 7461cc24f2..cbe90e7e83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16250,11 +16250,6 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -17738,16 +17733,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nock@^13.2.4: - version "13.2.4" - resolved "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz#43a309d93143ee5cdcca91358614e7bde56d20e1" - integrity sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - lodash.set "^4.3.2" - propagate "^2.0.0" - node-abort-controller@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" @@ -19804,11 +19789,6 @@ prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, object-assign "^4.1.1" react-is "^16.13.1" -propagate@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" - integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== - properties-reader@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/properties-reader/-/properties-reader-2.2.0.tgz#41d837fe143d8d5f2386b6a869a1975c0b2c595c" From c13bb6fc29ca8c332af98543600a30691ab6a63e Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 10 Feb 2022 09:57:55 +0000 Subject: [PATCH 39/42] Updated yarn.lock after fixing conflict with master Signed-off-by: Karan Shah --- yarn.lock | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6988edda3f..b0eabdb265 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,7 +1440,7 @@ "@material-ui/lab" "4.0.0-alpha.57" react-use "^17.2.4" -"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5", "@backstage/plugin-catalog-react@^0.6.9": +"@backstage/plugin-catalog-react@^0.6.12", "@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5", "@backstage/plugin-catalog-react@^0.6.9": version "0.6.13" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.13.tgz#b325eae501d3edeb8b7caef5d9615f2e632f5430" integrity sha512-XBwop7PwAZqfongx3KP6jAJar+MEscLSp8nLuHYX5XxA+suQNiBgi96uO3SEQmvtae+hvsRM7c0WHSxbYiXsDA== @@ -1835,14 +1835,7 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/luxon@1.x": - version "1.3.13" - resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" - integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== - dependencies: - "@date-io/core" "^1.3.13" - -"@date-io/luxon@^1.3.13": +"@date-io/luxon@1.x", "@date-io/luxon@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== From aab863538baf61520669b48dc31e7d1634826fe1 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 10 Feb 2022 15:38:40 +0000 Subject: [PATCH 40/42] Update package.json dependencies to the latest versions of Backstage dependencies Signed-off-by: Karan Shah --- plugins/airbrake/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 4c6ab0c21e..915762896a 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -23,8 +23,8 @@ "@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-next.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@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", From 029a5650fd7e026e848a7f6f2bd766e736627ec2 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 11 Feb 2022 12:32:14 +0100 Subject: [PATCH 41/42] use homepage consistently Signed-off-by: Himanshu Mishra --- docs/getting-started/homepage.md | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 87cf741b15..b7f7b88c6f 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -1,30 +1,30 @@ --- id: homepage -title: Backstage Home Page - Setup and Customization +title: Backstage homepage - Setup and Customization description: Documentation on setting up and customizing Backstage homepage --- -## Home Page +## Homepage -Having a good Backstage home page can significantly improve the discoverability of the platform. You want your users to find all the things they need right from the home page and never have to remember direct URLs in Backstage. The [Home plugin](https://github.com/backstage/backstage/tree/master/plugins/home) introduces a system for composing a home page for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any home page. +Having a good Backstage homepage can significantly improve the discoverability of the platform. You want your users to find all the things they need right from the homepage and never have to remember direct URLs in Backstage. The [Home plugin](https://github.com/backstage/backstage/tree/master/plugins/home) introduces a system for composing a homepage for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any homepage. -For App Integrators, the system is designed to be composable to give total freedom in designing a Home Page that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Home Pages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. +For App Integrators, the system is designed to be composable to give total freedom in designing a Homepage that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Homepages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. At the end of this tutorial, you can expect: -- Your Backstage app to have a dedicated home page instead of Software Catalog. -- Understand the composability of home page and how to start customizing it for your own organization. +- Your Backstage app to have a dedicated homepage instead of Software Catalog. +- Understand the composability of homepage and how to start customizing it for your own organization. ### Prerequisites Before we begin, make sure - You have created your own standalone Backstage app using [`@backstage/create-app`](index.md#create-your-backstage-app) and not using a fork of the [backstage](https://github.com/backstage/backstage) repository. -- You do not have an existing home page, and by default you are redirected to Software Catalog when you open Backstage. +- You do not have an existing homepage, and by default you are redirected to Software Catalog when you open Backstage. -Now, let's get started by installing the home plugin and creating a simple home page for your Backstage app. +Now, let's get started by installing the home plugin and creating a simple homepage for your Backstage app. -### Setup home page +### Setup homepage #### 1. Install the plugin @@ -36,20 +36,20 @@ yarn add @backstage/plugin-home #### 2. Create a new HomePage component -Inside your `packages/app` directory, create a new file where our new Home Page component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code +Inside your `packages/app` directory, create a new file where our new homepage component is going to live. Create `packages/app/src/components/home/HomePage.tsx` with the following initial code ```tsx import React from 'react'; export const HomePage = () => { - /* We will shortly compose a pretty Home Page here. */ + /* We will shortly compose a pretty homepage here. */ return

Welcome to Backstage!

; }; ``` #### 3. Update router for the root `/` route -If you don't have a home page already, most likely you have a redirect setup to use the Catalog home page as a home page. +If you don't have a homepage already, most likely you have a redirect setup to use the Catalog homepage as a homepage. Inside your `packages/app/src/App.tsx`, look for @@ -59,7 +59,7 @@ const routes = ( ``` -Let's replace the `` line and use the new component we created in the previous step as the new home page. +Let's replace the `` line and use the new component we created in the previous step as the new homepage. ```diff // File: packages/app/src/App.tsx @@ -81,7 +81,7 @@ const routes = ( ### 4. Update sidebar items -Let's update the route for "Home" in the Backstage sidebar to point to the new home page. We'll also add a Sidebar item to quickly open Catalog. +Let's update the route for "Home" in the Backstage sidebar to point to the new homepage. We'll also add a Sidebar item to quickly open Catalog. @@ -120,7 +120,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( ``` -That's it! You should now have _(although slightly boring)_ a home page! +That's it! You should now have _(although slightly boring)_ a homepage! Screenshot of a blank homepage @@ -128,15 +128,15 @@ In the next steps, we will make it interesting and useful! ### Use the default template -There is a default home page template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our home page. Checkout the [blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) about the Backstage home page templates for more information. +There is a default homepage template ([storybook link](https://backstage.io/storybook/?path=/story/plugins-home-templates--default-template)) which we will use to set up our homepage. Checkout the [blog post announcement](https://backstage.io/blog/2022/01/25/backstage-homepage-templates) about the Backstage homepage templates for more information. -### Composing your Home Page +### Composing your homepage -Composing a Home Page is no different from creating a regular React Component, +Composing a homepage is no different from creating a regular React Component, i.e. the App Integrator is free to include whatever content they like. However, -there are components developed with the Home Page in mind. If you are looking +there are components developed with the homepage in mind. If you are looking for components to use when composing your homepage, you can take a look at the [collection of Homepage components](https://backstage.io/?path=/story/plugins-home-components) in storybook. If you don't find a component that suits your needs but want to From 1208397ea62a18b2546311e1f20835d61498d93b Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 16 Feb 2022 12:33:35 +0100 Subject: [PATCH 42/42] Home page -> homepage for consistency Signed-off-by: Himanshu Mishra --- docs/getting-started/app-custom-theme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 526150d184..34eca2429a 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -254,7 +254,7 @@ const LogoFull = () => { }; ``` -## Custom Home Page +## Custom Homepage In addition to a custom theme, a custom logo, you can also customize the homepage of your app. Read the full guide on the [next page](homepage.md).