refactor: update rollbar to use composability api

This commit is contained in:
Andrew Thauer
2021-02-02 21:17:06 -05:00
parent 2aeb2ee94e
commit 9d6ef14bcd
19 changed files with 84 additions and 404 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'example-app': patch
'@backstage/plugin-rollbar': patch
---
Migrated to new composability API, exporting the plugin instance as `rollbarPlugin`, the entity page content as `EntityRollbarContent`, and entity conditional as `isRollbarAvailable`. Updated the `EntityPage` for the `example-app` to include a composite `ErrorsSwitcher` component that works with both `Sentry` & `Rollbar`. Also removed the unused and undocumented `RollbarHome` related components.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ApiEntity,
Entity,
@@ -64,6 +65,10 @@ import {
isPluginApplicableToEntity as isPagerDutyAvailable,
PagerDutyCard,
} from '@backstage/plugin-pagerduty';
import {
isRollbarAvailable,
Router as RollbarRouter,
} from '@backstage/plugin-rollbar';
import { Router as SentryRouter } from '@backstage/plugin-sentry';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { Button, Grid } from '@material-ui/core';
@@ -153,6 +158,15 @@ const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
);
};
export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => {
switch (true) {
case isRollbarAvailable(entity):
return <RollbarRouter entity={entity} />;
default:
return <SentryRouter entity={entity} />;
}
};
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
@@ -212,9 +226,9 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
element={<CICDSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/sentry"
title="Sentry"
element={<SentryRouter entity={entity} />}
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/api/*"
@@ -267,9 +281,9 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
element={<LighthouseRouter entity={entity} />}
/>
<EntityPageLayout.Content
path="/sentry"
title="Sentry"
element={<SentryRouter entity={entity} />}
path="/errors/*"
title="Errors"
element={<ErrorsSwitcher entity={entity} />}
/>
<EntityPageLayout.Content
path="/docs/*"
+4 -14
View File
@@ -19,17 +19,7 @@ yarn add @backstage/plugin-rollbar
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
```
4. Add plugin API to your Backstage instance:
```ts
// packages/app/src/api.ts
import { RollbarClient, rollbarApiRef } from '@backstage/plugin-rollbar';
// ...
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
```
5. Add to the app `EntityPage` component:
4. Add to the app `EntityPage` component:
```ts
// packages/app/src/components/catalog/EntityPage.tsx
@@ -48,7 +38,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
);
```
6. Setup the `app.config.yaml` and account token environment variable
5. Setup the `app.config.yaml` and account token environment variable
```yaml
# app.config.yaml
@@ -59,7 +49,7 @@ rollbar:
$env: ROLLBAR_ACCOUNT_TOKEN
```
7. Annotate entities with the rollbar project slug
6. Annotate entities with the rollbar project slug
```yaml
# pump-station-catalog-component.yaml
@@ -71,7 +61,7 @@ metadata:
rollbar.com/project-slug: project-name
```
8. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity
7. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity
## Features
+2 -2
View File
@@ -15,6 +15,6 @@
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
import { rollbarPlugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
createDevApp().registerPlugin(rollbarPlugin).render();
@@ -14,14 +14,18 @@
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import React from 'react';
import { RollbarProject } from '../RollbarProject/RollbarProject';
type Props = {
/** @deprecated The entity is now grabbed from context instead */
entity: Entity;
};
export const EntityPageRollbar = ({ entity }: Props) => {
export const EntityPageRollbar = (_props: Props) => {
const { entity } = useEntity();
return <RollbarProject entity={entity} />;
};
@@ -1,70 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 * as React from 'react';
import {
ApiProvider,
ApiRegistry,
ConfigApi,
configApiRef,
} from '@backstage/core';
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
import { RollbarProject } from '../../api/types';
import { RollbarHome } from './RollbarHome';
describe('RollbarHome component', () => {
const projects: RollbarProject[] = [
{ id: 123, name: 'abc', accountId: 1, status: 'enabled' },
{ id: 456, name: 'xyz', accountId: 1, status: 'enabled' },
];
const rollbarApi: Partial<RollbarApi> = {
getAllProjects: () => Promise.resolve(projects),
};
const config: Partial<ConfigApi> = {
getString: () => 'foo',
getOptionalString: () => 'foo',
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<ApiProvider
apis={ApiRegistry.from([
[rollbarApiRef, rollbarApi],
[configApiRef, config],
[
catalogApiRef,
({
async getEntities() {
return { items: [] };
},
} as Partial<CatalogApi>) as CatalogApi,
],
])}
>
{children}
</ApiProvider>,
),
);
it('should render rollbar landing page', async () => {
const rendered = renderWrapped(<RollbarHome />);
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
});
});
@@ -1,40 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Content, Header, Page } from '@backstage/core';
import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable';
import { useRollbarEntities } from '../../hooks/useRollbarEntities';
export const RollbarHome = () => {
const { entities, loading, error } = useRollbarEntities();
return (
<Page themeId="tool">
<Header
title="Rollbar"
subtitle="Real-time error tracking & debugging tools for developers"
/>
<Content>
<RollbarProjectTable
entities={entities || []}
loading={loading}
error={error}
/>
</Content>
</Page>
);
};
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import React from 'react';
import { useTopActiveItems } from '../../hooks/useTopActiveItems';
import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable';
@@ -1,86 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 {
ApiProvider,
ApiRegistry,
ConfigApi,
configApiRef,
} from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import * as React from 'react';
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
import { RollbarTopActiveItem } from '../../api/types';
import { RollbarProjectPage } from './RollbarProjectPage';
describe('RollbarProjectPage component', () => {
const items: RollbarTopActiveItem[] = [
{
item: {
id: 9898989,
counter: 1234,
environment: 'production',
framework: 2,
lastOccurrenceTimestamp: new Date().getTime() / 1000,
level: 50,
occurrences: 100,
projectId: 12345,
title: 'error occurred',
uniqueOccurrences: 10,
},
counts: [10, 10, 10, 10, 10, 50],
},
];
const rollbarApi: Partial<RollbarApi> = {
getTopActiveItems: () => Promise.resolve(items),
};
const config: Partial<ConfigApi> = {
getString: () => 'foo',
getOptionalString: () => 'foo',
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<ApiProvider
apis={ApiRegistry.from([
[rollbarApiRef, rollbarApi],
[configApiRef, config],
[
catalogApiRef,
({
async getEntityByName() {
return {
metadata: { name: 'foo' },
spec: { owner: 'bar', lifecycle: 'experimental' },
} as any;
},
} as Partial<CatalogApi>) as CatalogApi,
],
])}
>
{children}
</ApiProvider>,
),
);
it('should render rollbar project page', async () => {
const rendered = renderWrapped(<RollbarProjectPage />);
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
});
});
@@ -1,36 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Content, Header, HeaderLabel, Page } from '@backstage/core';
import { useCatalogEntity } from '../../hooks/useCatalogEntity';
import { RollbarProject } from '../RollbarProject/RollbarProject';
export const RollbarProjectPage = () => {
const { entity } = useCatalogEntity();
return (
<Page themeId="tool">
<Header title={entity?.metadata?.name} subtitle="Rollbar Project">
<HeaderLabel label="Owner" value={entity?.spec?.owner} />
<HeaderLabel label="Lifecycle" value={entity?.spec?.lifecycle} />
</Header>
<Content>
{entity ? <RollbarProject entity={entity} /> : 'Loading'}
</Content>
</Page>
);
};
@@ -1,85 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { Table, TableColumn } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { Link } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { entityRouteRef } from '../../routes';
const columns: TableColumn[] = [
{
title: 'Name',
field: 'metadata.name',
type: 'string',
highlight: true,
render: (entity: any) => (
<Link
component={RouterLink}
to={generatePath(entityRouteRef.path, {
optionalNamespaceAndName: [
entity.metadata.namespace,
entity.metadata.name,
]
.filter(Boolean)
.join(':'),
kind: entity.kind,
})}
>
{entity.metadata.name}
</Link>
),
},
{
title: 'Description',
field: 'metadata.description',
},
];
type Props = {
entities: Entity[];
loading: boolean;
error?: any;
};
export const RollbarProjectTable = ({ entities, loading, error }: Props) => {
if (error) {
return (
<div>
<Alert severity="error">
Error encountered while fetching rollbar projects. {error.toString()}
</Alert>
</div>
);
}
return (
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 10,
showEmptyDataSourceMessage: !loading,
}}
title="Projects"
data={entities}
/>
);
};
@@ -17,8 +17,8 @@
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import * as React from 'react';
import { RollbarTopItemsTable } from './RollbarTopItemsTable';
import { RollbarTopActiveItem } from '../../api/types';
import { RollbarTopItemsTable } from './RollbarTopItemsTable';
const items: RollbarTopActiveItem[] = [
{
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import React from 'react';
import { Table, TableColumn } from '@backstage/core';
import { Box, Link, Typography } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import {
RollbarFrameworkId,
RollbarLevel,
+15 -8
View File
@@ -14,29 +14,36 @@
* limitations under the License.
*/
import React from 'react';
import { Routes, Route } from 'react-router';
import { Entity } from '@backstage/catalog-model';
import { MissingAnnotationEmptyState } from '@backstage/core';
import { catalogRouteRef } from '../routes';
import { useEntity } from '@backstage/plugin-catalog-react';
import React from 'react';
import { Route, Routes } from 'react-router';
import { ROLLBAR_ANNOTATION } from '../constants';
import { rootRouteRef } from '../plugin';
import { EntityPageRollbar } from './EntityPageRollbar/EntityPageRollbar';
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[ROLLBAR_ANNOTATION]);
type Props = {
/** @deprecated The entity is now grabbed from context instead */
entity: Entity;
};
export const Router = ({ entity }: Props) =>
!isPluginApplicableToEntity(entity) ? (
<MissingAnnotationEmptyState annotation={ROLLBAR_ANNOTATION} />
) : (
export const Router = (_props: Props) => {
const { entity } = useEntity();
if (!isPluginApplicableToEntity(entity)) {
<MissingAnnotationEmptyState annotation={ROLLBAR_ANNOTATION} />;
}
return (
<Routes>
<Route
path={`/${catalogRouteRef.path}`}
path={`/${rootRouteRef.path}`}
element={<EntityPageRollbar entity={entity} />}
/>
</Routes>
);
};
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { TrendGraph } from './TrendGraph';
describe('TrendGraph component', () => {
+6 -4
View File
@@ -14,10 +14,12 @@
* limitations under the License.
*/
export { plugin } from './plugin';
export * from './api';
export * from './routes';
export { Router } from './components/Router';
export { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
export { EntityPageRollbar } from './components/EntityPageRollbar/EntityPageRollbar';
export {
isPluginApplicableToEntity,
isPluginApplicableToEntity as isRollbarAvailable,
Router,
} from './components/Router';
export { ROLLBAR_ANNOTATION } from './constants';
export { rollbarPlugin as plugin, rollbarPlugin } from './plugin';
+2 -2
View File
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { plugin } from './plugin';
import { rollbarPlugin } from './plugin';
describe('rollbar', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
expect(rollbarPlugin).toBeDefined();
});
});
+18 -8
View File
@@ -15,17 +15,21 @@
*/
import {
createPlugin,
createApiFactory,
createPlugin,
createRoutableExtension,
createRouteRef,
discoveryApiRef,
} from '@backstage/core';
import { rootRouteRef, entityRouteRef } from './routes';
import { RollbarHome } from './components/RollbarHome/RollbarHome';
import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
import { rollbarApiRef } from './api/RollbarApi';
import { RollbarClient } from './api/RollbarClient';
export const plugin = createPlugin({
export const rootRouteRef = createRouteRef({
path: '',
title: 'Rollbar',
});
export const rollbarPlugin = createPlugin({
id: 'rollbar',
apis: [
createApiFactory({
@@ -34,8 +38,14 @@ export const plugin = createPlugin({
factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }),
}),
],
register({ router }) {
router.addRoute(rootRouteRef, RollbarHome);
router.addRoute(entityRouteRef, RollbarProjectPage);
routes: {
entityContent: rootRouteRef,
},
});
export const EntityRollbarContent = rollbarPlugin.provide(
createRoutableExtension({
component: () => import('./components/Router').then(m => m.Router),
mountPoint: rootRouteRef,
}),
);
-36
View File
@@ -1,36 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core';
const NoIcon = () => null;
export const rootRouteRef = createRouteRef({
icon: NoIcon,
path: '/rollbar',
title: 'Rollbar Home',
});
export const entityRouteRef = createRouteRef({
path: '/rollbar/:optionalNamespaceAndName',
title: 'Rollbar',
});
export const catalogRouteRef = createRouteRef({
icon: NoIcon,
path: '',
title: 'Rollbar',
});