Migrate sentry plugin to new frontend system (#1163)

* add entity content

Signed-off-by: Alex McKay <amckay@spotify.com>

* add entity card

Signed-off-by: Alex McKay <amckay@spotify.com>

* add changeset & api-report

Signed-off-by: Alex McKay <amckay@spotify.com>

* add example apps

Signed-off-by: Alex McKay <amckay@spotify.com>

* add documentation & re-run api-reports

Signed-off-by: Alex McKay <amckay@spotify.com>

* Update workspaces/sentry/plugins/sentry/README.md

Fixing typo

Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>

---------

Signed-off-by: Alex McKay <amckay@spotify.com>
Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>
Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>
This commit is contained in:
Alex McKay 2024-09-09 09:18:06 -04:00 committed by GitHub
parent 53c3cc3ab1
commit 2909069cf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 10529 additions and 396 deletions

View File

@ -0,0 +1,5 @@
---
'@backstage-community/plugin-sentry': major
---
Removed parameters for the 'Router', calling useEntity() within the router itself instead. Also adds support for Backstage's new frontend system, available via the `/alpha` sub-path export.

View File

@ -0,0 +1,73 @@
app:
title: Sentry Example App
baseUrl: http://localhost:3000
experimental:
packages: 'all'
extensions:
- entity-card:catalog/about
- entity-card:catalog/labels
- entity-card:catalog/links:
config:
filter: kind:component has:links
- entity-card:sentry/sentry-issues
- entity-content:sentry/sentry-issues
organization:
name: Sentry Example
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
csp:
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
database:
client: better-sqlite3
connection: ':memory:'
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
techdocs:
builder: 'local'
generator:
runIn: 'local'
publisher:
type: 'local'
auth:
providers:
guest: {}
proxy:
'/sentry/api':
target: https://sentry.io/api/
allowedMethods: ['GET']
headers:
Authorization: Bearer ${SENTRY_TOKEN}
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
target: ../../examples/entities.yaml
# Local example organizational data
- type: file
target: ../../examples/org.yaml
rules:
- allow: [User, Group]
sentry:
organization: your-organization

View File

@ -0,0 +1,43 @@
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: examples
spec:
owner: guests
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-website
annotations:
sentry.io/project-slug: YOUR_PROJECT_SLUG
spec:
type: website
lifecycle: experimental
owner: guests
system: examples
providesApis: [example-grpc-api]
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: example-grpc-api
spec:
type: grpc
lifecycle: experimental
owner: guests
system: examples
definition: |
syntax = "proto3";
service Exampler {
rpc Example (ExampleMessage) returns (ExampleMessage) {};
}
message ExampleMessage {
string example = 1;
};

View File

@ -0,0 +1,17 @@
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: guest
spec:
memberOf: [guests]
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: guests
spec:
type: team
children: []

View File

@ -6,6 +6,12 @@
"node": "18 || 20"
},
"scripts": {
"dev": "concurrently \"yarn start\" \"yarn start-backend\"",
"dev-next": "concurrently \"yarn start-next\" \"yarn start-backend\"",
"start": "yarn workspace app start",
"start-next": "yarn workspace app-next start",
"start-backend": "yarn workspace backend start",
"build:backend": "yarn workspace backend build",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"build:all": "backstage-cli repo build --all",
@ -38,6 +44,7 @@
"@backstage/repo-tools": "^0.9.5",
"@changesets/cli": "^2.27.1",
"@spotify/prettier-config": "^15.0.0",
"concurrently": "^8.0.0",
"node-gyp": "^10.0.0",
"prettier": "^2.3.2",
"typescript": "~5.3.0"

View File

@ -0,0 +1,9 @@
# The Packages Folder
This is where your own applications and centrally managed libraries live, each
in a separate folder of its own.
From the start there's an `app` folder (for the frontend) and a `backend` folder
(for the Node backend), but you can also add more modules in here that house
your core additions and adaptations, such as themes, common React component
libraries, utilities, and similar.

View File

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@ -0,0 +1,77 @@
{
"name": "app-next",
"version": "0.0.1",
"private": true,
"bundled": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/sentry/packages/app-next"
},
"backstage": {
"role": "frontend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"test": "backstage-cli package test",
"lint": "backstage-cli package lint"
},
"dependencies": {
"@backstage-community/plugin-sentry": "workspace:^",
"@backstage/app-defaults": "^1.5.10",
"@backstage/catalog-model": "^1.6.0",
"@backstage/cli": "^0.27.0",
"@backstage/core-app-api": "^1.14.2",
"@backstage/core-compat-api": "^0.2.8",
"@backstage/core-components": "^0.14.10",
"@backstage/core-plugin-api": "^1.9.3",
"@backstage/frontend-app-api": "^0.8.0",
"@backstage/frontend-plugin-api": "^0.7.0",
"@backstage/integration-react": "^1.1.30",
"@backstage/plugin-api-docs": "^0.11.6",
"@backstage/plugin-catalog": "^1.22.0",
"@backstage/plugin-catalog-common": "^1.0.26",
"@backstage/plugin-catalog-graph": "^0.4.8",
"@backstage/plugin-catalog-import": "^0.12.2",
"@backstage/plugin-catalog-react": "^1.12.3",
"@backstage/plugin-org": "^0.6.28",
"@backstage/plugin-user-settings": "^0.8.11",
"@backstage/theme": "^0.5.6",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4",
"styled-components": "^6.1.8"
},
"devDependencies": {
"@backstage/test-utils": "^1.5.6",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/react-dom": "*",
"cross-env": "^7.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,29 @@
import { renderWithEffects } from '@backstage/test-utils';
// Rarely, and only in windows CI, do these tests take slightly more than the
// default five seconds
jest.setTimeout(15_000);
describe('App', () => {
it('should render', async () => {
process.env = {
NODE_ENV: 'test',
APP_CONFIG: [
{
data: {
app: {
title: 'Test',
support: { url: 'http://localhost:7007/support' },
},
backend: { baseUrl: 'http://localhost:7007' },
},
context: 'test',
},
] as any,
};
const { default: app } = await import('./App');
const rendered = await renderWithEffects(app);
expect(rendered.baseElement).toBeInTheDocument();
});
});

View File

@ -0,0 +1,144 @@
import React from 'react';
import { FlatRoutes } from '@backstage/core-app-api';
import { convertLegacyApp } from '@backstage/core-compat-api';
import { createApp } from '@backstage/frontend-app-api';
import {
configApiRef,
createApiExtension,
createApiFactory,
createExtensionOverrides,
createPageExtension,
createSignInPageExtension,
createThemeExtension,
ApiBlueprint,
} from '@backstage/frontend-plugin-api';
import {
ScmAuth,
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { ApiExplorerPage } from '@backstage/plugin-api-docs';
import catalogPlugin from '@backstage/plugin-catalog/alpha';
import catalogImportPlugin from '@backstage/plugin-catalog-import/alpha';
import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
import {
createUnifiedTheme,
createBaseThemeOptions,
pageTheme as defaultPageThemes,
palettes,
genPageTheme,
colorVariants,
shapes,
UnifiedTheme,
UnifiedThemeProvider,
} from '@backstage/theme';
import { Navigate, Route } from 'react-router';
import sentryPlugin from '@backstage-community/plugin-sentry/alpha';
import {
MockSentryApi,
sentryApiRef,
} from '@backstage-community/plugin-sentry';
import { SignInPage } from './components/auth/SignInPage';
const pageTheme = {
...defaultPageThemes,
dataset: genPageTheme({
colors: colorVariants.purpleSky,
shape: shapes.wave,
}),
};
export const lightTheme: UnifiedTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: { ...palettes.light },
}),
pageTheme,
});
export const darkTheme: UnifiedTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: { ...palettes.dark },
}),
pageTheme,
});
const lightThemeExtension = createThemeExtension({
id: 'mui-light',
title: 'MUI Light',
variant: 'light',
Provider: ({ children }) => (
<UnifiedThemeProvider theme={lightTheme} children={children} />
),
});
const darkThemeExtension = createThemeExtension({
id: 'mui-dark',
title: 'MUI Dark',
variant: 'dark',
Provider: ({ children }) => (
<UnifiedThemeProvider theme={darkTheme} children={children} />
),
});
const homePageExtension = createPageExtension({
namespace: 'home',
defaultPath: '/',
loader: () => Promise.resolve(<Navigate to="catalog" />),
});
const signInPage = createSignInPageExtension({
name: 'signInPage',
loader: async () => props => <SignInPage {...props} />,
});
const collectedLegacyPlugins = convertLegacyApp(
<FlatRoutes>
<Route path="/api-docs" element={<ApiExplorerPage />} />
</FlatRoutes>,
);
const scmAuthExtension = createApiExtension({
factory: ScmAuth.createDefaultApiFactory(),
});
const scmIntegrationApi = createApiExtension({
factory: createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
});
const sentryMockApi = ApiBlueprint.make({
name: 'sentry',
params: {
factory: createApiFactory({
api: sentryApiRef,
deps: {},
factory: () => new MockSentryApi(),
}),
},
});
export const app = createApp({
features: [
catalogPlugin,
catalogImportPlugin,
userSettingsPlugin,
sentryPlugin,
...collectedLegacyPlugins,
createExtensionOverrides({
extensions: [
signInPage,
darkThemeExtension,
lightThemeExtension,
homePageExtension,
scmAuthExtension,
scmIntegrationApi,
sentryMockApi,
],
}),
],
});
export default app.createRoot();

View File

@ -0,0 +1,54 @@
import { compatWrapper } from '@backstage/core-compat-api';
import {
Sidebar,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarSpace,
} from '@backstage/core-components';
import {
coreExtensionData,
createExtension,
} from '@backstage/frontend-plugin-api';
import {
Settings,
UserSettingsSignInAvatar,
} from '@backstage/plugin-user-settings';
import ExtensionIcon from '@material-ui/icons/Extension';
import HomeIcon from '@material-ui/icons/Home';
import MenuIcon from '@material-ui/icons/Menu';
import React from 'react';
export const navigationExtension = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
output: {
element: coreExtensionData.reactElement,
},
factory() {
return {
element: compatWrapper(
<Sidebar>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
{/* End global nav */}
<SidebarDivider />
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
label="Settings"
icon={<UserSettingsSignInAvatar />}
to="/settings"
>
<Settings />
</SidebarGroup>
</Sidebar>,
),
};
},
});

View File

@ -0,0 +1,7 @@
import { SignInPage as BackstageSignInPage } from '@backstage/core-components';
import { SignInPageProps } from '@backstage/core-plugin-api';
import React from 'react';
export const SignInPage = (props: SignInPageProps) => {
return <BackstageSignInPage {...props} auto providers={['guest']} />;
};

View File

@ -0,0 +1,5 @@
import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom';
import app from './App';
ReactDOM.render(app, document.getElementById('root'));

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1 @@
public

View File

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@ -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 { test, expect } from '@playwright/test';
test('App should render the welcome page', async ({ page }) => {
await page.goto('/');
const enterButton = page.getByRole('button', { name: 'Enter' });
await expect(enterButton).toBeVisible();
await enterButton.click();
await expect(page.getByText('My Company Catalog')).toBeVisible();
});

View File

@ -0,0 +1,74 @@
{
"name": "app",
"version": "0.0.1",
"private": true,
"bundled": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/sentry/packages/app"
},
"backstage": {
"role": "frontend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"test": "backstage-cli package test",
"lint": "backstage-cli package lint"
},
"dependencies": {
"@backstage-community/plugin-sentry": "workspace:^",
"@backstage-community/plugin-tech-radar": "^0.7.4",
"@backstage/app-defaults": "^1.5.10",
"@backstage/catalog-model": "^1.6.0",
"@backstage/cli": "^0.27.0",
"@backstage/core-app-api": "^1.14.2",
"@backstage/core-components": "^0.14.10",
"@backstage/core-plugin-api": "^1.9.3",
"@backstage/integration-react": "^1.1.30",
"@backstage/plugin-api-docs": "^0.11.8",
"@backstage/plugin-catalog": "^1.22.0",
"@backstage/plugin-catalog-common": "^1.0.26",
"@backstage/plugin-catalog-graph": "^0.4.8",
"@backstage/plugin-catalog-import": "^0.12.2",
"@backstage/plugin-catalog-react": "^1.12.3",
"@backstage/plugin-org": "^0.6.28",
"@backstage/plugin-permission-react": "^0.4.25",
"@backstage/plugin-scaffolder": "^1.24.0",
"@backstage/plugin-search": "^1.4.15",
"@backstage/plugin-search-react": "^1.7.14",
"@backstage/plugin-techdocs": "^1.10.8",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.13",
"@backstage/plugin-techdocs-react": "^1.2.7",
"@backstage/plugin-user-settings": "^0.8.11",
"@backstage/theme": "^0.5.6",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@playwright/test": "^1.32.3",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/react-dom": "*"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"files": [
"dist"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Backstage is an open source framework for building developer portals"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link
rel="manifest"
href="<%= publicPath %>/manifest.json"
crossorigin="use-credentials"
/>
<link rel="icon" href="<%= publicPath %>/favicon.ico" />
<link rel="shortcut icon" href="<%= publicPath %>/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="<%= publicPath %>/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="<%= publicPath %>/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="<%= publicPath %>/favicon-16x16.png"
/>
<link
rel="mask-icon"
href="<%= publicPath %>/safari-pinned-tab.svg"
color="#5bbad5"
/>
<title><%= config.getOptionalString('app.title') ?? 'Backstage' %></title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `yarn start`.
To create a production bundle, use `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "Backstage",
"name": "Backstage",
"icons": [
{
"src": "favicon.ico",
"sizes": "48x48",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="682.667" height="682.667" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 512 512"><metadata>Created by potrace 1.11, written by Peter Selinger 2001-2013</metadata><g fill="#000" stroke="none"><path d="M492 4610 c-4 -3 -8 -882 -7 -1953 l0 -1948 850 2 c898 1 945 3 1118 49 505 134 823 531 829 1037 2 136 -9 212 -44 323 -40 125 -89 218 -163 310 -35 43 -126 128 -169 157 -22 15 -43 30 -46 33 -12 13 -131 70 -188 91 l-64 22 60 28 c171 77 317 224 403 404 64 136 92 266 91 425 -5 424 -245 770 -642 923 -79 30 -105 39 -155 50 -11 3 -38 10 -60 15 -22 6 -60 13 -85 17 -25 3 -58 9 -75 12 -36 8 -1643 11 -1653 3z m1497 -743 c236 -68 352 -254 305 -486 -26 -124 -110 -224 -232 -277 -92 -40 -151 -46 -439 -49 l-283 -3 -1 27 c-1 36 -1 760 0 790 l1 23 298 -5 c226 -4 310 -9 351 -20z m-82 -1538 c98 -3 174 -19 247 -52 169 -78 257 -212 258 -395 0 -116 -36 -221 -100 -293 -64 -72 -192 -135 -314 -155 -23 -3 -181 -7 -350 -8 l-308 -2 -1 26 c-6 210 1 874 9 879 9 5 366 6 559 0z" transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"/><path d="M4160 1789 c-275 -24 -499 -263 -503 -536 -1 -115 21 -212 66 -292 210 -369 697 -402 950 -65 77 103 110 199 111 329 0 50 -6 113 -13 140 -16 58 -62 155 -91 193 -33 43 -122 132 -132 132 -5 0 -26 11 -46 25 -85 56 -219 85 -342 74z" transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,29 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import App from './App';
describe('App', () => {
it('should render', async () => {
process.env = {
NODE_ENV: 'test',
APP_CONFIG: [
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7007' },
techdocs: {
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
},
},
context: 'test',
},
] as any,
};
const rendered = render(<App />);
await waitFor(() => {
expect(rendered.baseElement).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,114 @@
import React from 'react';
import { Navigate, Route } from 'react-router-dom';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
} from '@backstage/plugin-catalog';
import {
CatalogImportPage,
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { orgPlugin } from '@backstage/plugin-org';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage-community/plugin-tech-radar';
import {
TechDocsIndexPage,
techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
import { apis } from './apis';
import { entityPage } from './components/catalog/EntityPage';
import { searchPage } from './components/search/SearchPage';
import { Root } from './components/Root';
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { createApp } from '@backstage/app-defaults';
import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
const app = createApp({
apis,
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
viewTechDoc: techdocsPlugin.routes.docRoot,
createFromTemplate: scaffolderPlugin.routes.selectedTemplate,
});
bind(apiDocsPlugin.externalRoutes, {
registerApi: catalogImportPlugin.routes.importPage,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
});
},
components: {
SignInPage: props => <SignInPage {...props} auto providers={['guest']} />,
},
});
const routes = (
<FlatRoutes>
<Route path="/" element={<Navigate to="catalog" />} />
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
<Route path="/docs" element={<TechDocsIndexPage />} />
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
<TechDocsAddons>
<ReportIssue />
</TechDocsAddons>
</Route>
<Route path="/create" element={<ScaffolderPage />} />
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route
path="/tech-radar"
element={<TechRadarPage width={1500} height={800} />}
/>
<Route
path="/catalog-import"
element={
<RequirePermission permission={catalogEntityCreatePermission}>
<CatalogImportPage />
</RequirePermission>
}
/>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
</FlatRoutes>
);
export default app.createRoot(
<>
<AlertDisplay />
<OAuthRequestDialog />
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</>,
);

View File

@ -0,0 +1,24 @@
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
} from '@backstage/core-plugin-api';
import {
MockSentryApi,
sentryApiRef,
} from '@backstage-community/plugin-sentry';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
createApiFactory(sentryApiRef, new MockSentryApi()),
];

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 28,
},
path: {
fill: '#7df3e1',
},
});
const LogoIcon = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 337.46 428.5"
>
<path
className={classes.path}
d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"
/>
</svg>
);
};
export default LogoIcon;

View File

@ -0,0 +1,90 @@
import React, { PropsWithChildren } from 'react';
import { makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
import MapIcon from '@material-ui/icons/MyLocation';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Settings as SidebarSettings,
UserSettingsSignInAvatar,
} from '@backstage/plugin-user-settings';
import { SidebarSearchModal } from '@backstage/plugin-search';
import {
Sidebar,
sidebarConfig,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebarOpenState,
Link,
} from '@backstage/core-components';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
const useSidebarLogoStyles = makeStyles({
root: {
width: sidebarConfig.drawerWidthClosed,
height: 3 * sidebarConfig.logoHeight,
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
marginBottom: -14,
},
link: {
width: sidebarConfig.drawerWidthClosed,
marginLeft: 24,
},
});
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useSidebarOpenState();
return (
<div className={classes.root}>
<Link to="/" underline="none" className={classes.link} aria-label="Home">
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
</div>
);
};
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
<SidebarSearchModal />
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
{/* End global nav */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
</SidebarScrollWrapper>
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
label="Settings"
icon={<UserSettingsSignInAvatar />}
to="/settings"
>
<SidebarSettings />
</SidebarGroup>
</Sidebar>
{children}
</SidebarPage>
);

View File

@ -0,0 +1 @@
export { Root } from './Root';

View File

@ -0,0 +1,396 @@
import React from 'react';
import { Button, Grid } from '@material-ui/core';
import {
EntityApiDefinitionCard,
EntityConsumedApisCard,
EntityConsumingComponentsCard,
EntityHasApisCard,
EntityProvidedApisCard,
EntityProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasResourcesCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLayout,
EntityLinksCard,
EntityOrphanWarning,
EntityProcessingErrorsPanel,
EntityRelationWarning,
EntitySwitch,
hasCatalogProcessingErrors,
hasRelationWarnings,
isComponentType,
isKind,
isOrphan,
} from '@backstage/plugin-catalog';
import {
EntityGroupProfileCard,
EntityMembersListCard,
EntityOwnershipCard,
EntityUserProfileCard,
} from '@backstage/plugin-org';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
import { EmptyState } from '@backstage/core-components';
import {
Direction,
EntityCatalogGraphCard,
} from '@backstage/plugin-catalog-graph';
import {
RELATION_API_CONSUMED_BY,
RELATION_API_PROVIDED_BY,
RELATION_CONSUMES_API,
RELATION_DEPENDENCY_OF,
RELATION_DEPENDS_ON,
RELATION_HAS_PART,
RELATION_PART_OF,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
import {
isSentryAvailable,
EntitySentryContent,
EntitySentryCard,
} from '@backstage-community/plugin-sentry';
const techdocsContent = (
<EntityTechdocsContent>
<TechDocsAddons>
<ReportIssue />
</TechDocsAddons>
</EntityTechdocsContent>
);
const sentryContent = (
// This is an example of how you can implement your company's logic in entity page.
// You can for example enforce that all components of type 'service' should use GitHubActions
<EntitySwitch>
<EntitySwitch.Case if={isSentryAvailable}>
<EntitySentryContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
missing="info"
description="You need to add an annotation to your component if you want to enable CI/CD for it. You can read more about annotations in Backstage by clicking the button below."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
}
/>
</EntitySwitch.Case>
</EntitySwitch>
);
const entityWarningContent = (
<>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
</>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={6}>
<EntitySentryCard />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
</Grid>
);
const serviceEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/sentry" title="Sentry">
{sentryContent}
</EntityLayout.Route>
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityProvidedApisCard />
</Grid>
<Grid item md={6}>
<EntityConsumedApisCard />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
</EntityLayout>
);
const websiteEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/sentry" title="Sentry">
{sentryContent}
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
</EntityLayout>
);
/**
* NOTE: This page is designed to work on small screens such as mobile devices.
* This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`,
* since this does not default. If no breakpoints are used, the items will equitably share the available space.
* https://material-ui.com/components/grid/#basic-grid.
*/
const defaultEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
</EntityLayout>
);
const componentPage = (
<EntitySwitch>
<EntitySwitch.Case if={isComponentType('service')}>
{serviceEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
const apiPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/definition" title="Definition">
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityApiDefinitionCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const userPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const groupPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item xs={12} md={6}>
<EntityGroupProfileCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityMembersListCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityLinksCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const systemPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<Grid item md={8}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
<EntityCatalogGraphCard
variant="gridItem"
direction={Direction.TOP_BOTTOM}
title="System Diagram"
height={700}
relations={[
RELATION_PART_OF,
RELATION_HAS_PART,
RELATION_API_CONSUMED_BY,
RELATION_API_PROVIDED_BY,
RELATION_CONSUMES_API,
RELATION_PROVIDES_API,
RELATION_DEPENDENCY_OF,
RELATION_DEPENDS_ON,
]}
unidirectional={false}
/>
</EntityLayout.Route>
</EntityLayout>
);
const domainPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
<EntitySwitch.Case if={isKind('user')} children={userPage} />
<EntitySwitch.Case if={isKind('system')} children={systemPage} />
<EntitySwitch.Case if={isKind('domain')} children={domainPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);

View File

@ -0,0 +1,124 @@
import React from 'react';
import { makeStyles, Theme, Grid, Paper } from '@material-ui/core';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
import {
catalogApiRef,
CATALOG_FILTER_EXISTS,
} from '@backstage/plugin-catalog-react';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { SearchType } from '@backstage/plugin-search';
import {
SearchBar,
SearchFilter,
SearchResult,
SearchPagination,
useSearch,
} from '@backstage/plugin-search-react';
import {
CatalogIcon,
Content,
DocsIcon,
Header,
Page,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
padding: theme.spacing(1, 0),
},
filters: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
filter: {
'& + &': {
marginTop: theme.spacing(2.5),
},
},
}));
const SearchPage = () => {
const classes = useStyles();
const { types } = useSearch();
const catalogApi = useApi(catalogApiRef);
return (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper className={classes.bar}>
<SearchBar />
</Paper>
</Grid>
<Grid item xs={3}>
<SearchType.Accordion
name="Result Type"
defaultValue="software-catalog"
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
icon: <CatalogIcon />,
},
{
value: 'techdocs',
name: 'Documentation',
icon: <DocsIcon />,
},
]}
/>
<Paper className={classes.filters}>
{types.includes('techdocs') && (
<SearchFilter.Select
className={classes.filter}
label="Entity"
name="name"
values={async () => {
// Return a list of entities which are documented.
const { items } = await catalogApi.getEntities({
fields: ['metadata.name'],
filter: {
'metadata.annotations.backstage.io/techdocs-ref':
CATALOG_FILTER_EXISTS,
},
});
const names = items.map(entity => entity.metadata.name);
names.sort();
return names;
}}
/>
)}
<SearchFilter.Select
className={classes.filter}
label="Kind"
name="kind"
values={['Component', 'Template']}
/>
<SearchFilter.Checkbox
className={classes.filter}
label="Lifecycle"
name="lifecycle"
values={['experimental', 'production']}
/>
</Paper>
</Grid>
<Grid item xs={9}>
<SearchPagination />
<SearchResult>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
};
export const searchPage = <SearchPage />;

View File

@ -0,0 +1,6 @@
import '@backstage/cli/asset-types';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@ -0,0 +1,59 @@
# example-backend
This package is an EXAMPLE of a Backstage backend.
The main purpose of this package is to provide a test bed for Backstage plugins
that have a backend part. Feel free to experiment locally or within your fork by
adding dependencies and routes to this backend, to try things out.
Our goal is to eventually amend the create-app flow of the CLI, such that a
production ready version of a backend skeleton is made alongside the frontend
app. Until then, feel free to experiment here!
## Development
To run the example backend, first go to the project root and run
```bash
yarn install
```
You should only need to do this once.
After that, go to the `packages/backend` directory and run
```bash
yarn start
```
If you want to override any configuration locally, for example adding any secrets,
you can do so in `app-config.local.yaml`.
The backend starts up on port 7007 per default.
## Populating The Catalog
If you want to use the catalog functionality, you need to add so called
locations to the backend. These are places where the backend can find some
entity descriptor data to consume and serve. For more information, see
[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog).
To get started quickly, this template already includes some statically configured example locations
in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you
like, and also override them for local development in `app-config.local.yaml`.
## Authentication
We chose [Passport](http://www.passportjs.org/) as authentication platform due
to its comprehensive set of supported authentication
[strategies](http://www.passportjs.org/packages/).
Read more about the
[auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md)
and
[how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md)
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)

View File

@ -0,0 +1,45 @@
{
"name": "backend",
"version": "0.0.1",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/sentry/packages/backend"
},
"backstage": {
"role": "backend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"build-image": "docker build ../.. -f Dockerfile --tag backstage"
},
"dependencies": {
"@backstage/backend-defaults": "^0.4.3",
"@backstage/plugin-app-backend": "^0.3.72",
"@backstage/plugin-auth-backend": "^0.22.10",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.1.9",
"@backstage/plugin-catalog-backend": "^1.25.0",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.1.21",
"@backstage/plugin-permission-backend": "^0.5.47",
"@backstage/plugin-permission-backend-module-allow-all-policy": "^0.1.20",
"@backstage/plugin-proxy-backend": "^0.5.4",
"@backstage/plugin-scaffolder-backend": "^1.24.0",
"@backstage/plugin-search-backend": "^1.5.15",
"@backstage/plugin-search-backend-module-catalog": "^0.2.0",
"@backstage/plugin-search-backend-module-techdocs": "^0.2.0",
"@backstage/plugin-techdocs-backend": "^1.10.11"
},
"devDependencies": {
"@backstage/cli": "^0.27.0"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,41 @@
/*
* Hi!
*
* Note that this is an EXAMPLE Backstage backend. Please check the README.
*
* Happy hacking!
*/
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('@backstage/plugin-app-backend/alpha'));
backend.add(import('@backstage/plugin-proxy-backend/alpha'));
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
// auth plugin
backend.add(import('@backstage/plugin-auth-backend'));
// See https://backstage.io/docs/backend-system/building-backends/migrating#the-auth-plugin
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
// See https://backstage.io/docs/auth/guest/provider
// catalog plugin
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
// permission plugin
backend.add(import('@backstage/plugin-permission-backend/alpha'));
backend.add(
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
);
// search plugin
backend.add(import('@backstage/plugin-search-backend/alpha'));
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
backend.start();

View File

@ -13,7 +13,10 @@ The Sentry Plugin displays issues from [Sentry](https://sentry.io).
yarn --cwd packages/app add @backstage-community/plugin-sentry
```
2. Add the `EntitySentryCard` to the EntityPage:
2. Import the sentry card or content into the frontend. If using the standard Backstage frontend, follow step 2A, and if using
the [new alpha frontend system](https://backstage.io/docs/frontend-system/), follow step 2B instead.
A. Add the `EntitySentryCard` to the EntityPage:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
@ -49,6 +52,44 @@ const overviewContent = (
> );
> ```
B. [Install the plugin](https://backstage.io/docs/frontend-system/building-apps/index#install-features-manually) by updating `App.tsx` to include the plugin in the features block during app creation:
```tsx
// packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-app-api';
import sentryPlugin from '@axis-backstage/plugin-sentry/alpha';
...
const app = createApp({
features: [
...,
sentryPlugin,
],
});
export default app.createRoot();
```
The, [configure the extension](https://backstage.io/docs/frontend-system/building-apps/configuring-extensions) inside `app-config.yaml` to include the entity content & card:
```yaml
# app-config.yaml
app:
extensions:
- entity-content:sentry/sentry-issues
- entity-card:sentry/sentry-issues
```
You can also control which [entity kinds](https://backstage.io/docs/features/software-catalog/system-model) the sentry card appears on by adding a config underneath the entity-content, like so:
```yaml
# app-config.yaml
app:
extensions:
- entity-content:sentry/sentry-issues
config:
filter: kind:component,system,group
```
3. Add the proxy config:
```yaml
@ -104,3 +145,45 @@ export const apis = [
createApiFactory(sentryApiRef, new MockSentryApi()),
];
```
If using the [new frontend system](<(https://backstage.io/docs/frontend-system/)>), then use the mock api by modifying App.tsx instead;
```ts
// packages/app/src/App.tsx
import {
createApiFactory,
createExtensionOverrides,
ApiBlueprint,
} from '@backstage/frontend-plugin-api';
import {
MockSentryApi,
sentryApiRef,
} from '@backstage-community/plugin-sentry';
import sentryPlugin from '@axis-backstage/plugin-sentry/alpha';
const sentryMockApi = ApiBlueprint.make({
name: 'sentry',
params: {
factory: createApiFactory({
api: sentryApiRef,
deps: {},
factory: () => new MockSentryApi(),
}),
},
});
const app = createApp({
features: [
...,
sentryPlugin,
createExtensionOverrides({
extensions: [
sentryMockApi,
],
}),
],
});
...
```

View File

@ -0,0 +1,111 @@
## API Report File for "@backstage-community/plugin-sentry"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { AnyApiFactory } from '@backstage/core-plugin-api';
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
const _default: BackstagePlugin<
{
entityContent: RouteRef<undefined>;
},
{},
{
'api:sentry': ExtensionDefinition<
{},
{},
ConfigurableExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>,
{},
{
kind: 'api';
namespace: undefined;
name: undefined;
}
>;
'entity-card:sentry/sentry-issues': ExtensionDefinition<
{
filter: string | undefined;
},
{
filter?: string | undefined;
},
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>,
{},
{
kind: 'entity-card';
namespace: undefined;
name: 'sentry-issues';
}
>;
'entity-content:sentry/sentry-issues': ExtensionDefinition<
{
path: string | undefined;
title: string | undefined;
filter: string | undefined;
},
{
filter?: string | undefined;
title?: string | undefined;
path?: string | undefined;
},
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<string, 'catalog.entity-content-title', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>,
{},
{
kind: 'entity-content';
namespace: undefined;
name: 'sentry-issues';
}
>;
}
>;
export default _default;
// (No @packageDocumentation comment for this package)
```

View File

@ -51,7 +51,7 @@ export class ProductionSentryApi implements SentryApi {
}
// @public (undocumented)
export const Router: (props: { entity: Entity }) => React_2.JSX.Element;
export const Router: () => React_2.JSX.Element;
// @public (undocumented)
export interface SentryApi {
@ -124,6 +124,9 @@ export const SentryIssuesWidget: (props: {
query?: string;
}) => React_2.JSX.Element;
// @public (undocumented)
export const SentryIssuesWidgetCard: () => React_2.JSX.Element;
// @public (undocumented)
export type SentryPageProps = {
statsFor?: '24h' | '14d' | '';

View File

@ -10,9 +10,22 @@
]
},
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
"access": "public"
},
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha.ts",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
"src/alpha.ts"
],
"package.json": [
"package.json"
]
}
},
"keywords": [
"backstage",
@ -43,8 +56,10 @@
},
"dependencies": {
"@backstage/catalog-model": "^1.6.0",
"@backstage/core-compat-api": "^0.2.8",
"@backstage/core-components": "^0.14.10",
"@backstage/core-plugin-api": "^1.9.3",
"@backstage/frontend-plugin-api": "^0.7.0",
"@backstage/plugin-catalog-react": "^1.12.3",
"@date-io/core": "^1.3.13",
"@material-table/core": "^3.1.0",
@ -57,6 +72,7 @@
"devDependencies": {
"@backstage/cli": "^0.27.0",
"@backstage/dev-utils": "^1.0.37",
"@backstage/frontend-test-utils": "^0.1.12",
"@backstage/test-utils": "^1.5.10",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
@ -68,6 +84,9 @@
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"resolutions": {
"@microsoft/api-extractor": "7.36.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",

View File

@ -0,0 +1,29 @@
import { SENTRY_PROJECT_SLUG_ANNOTATION } from '../components/hooks';
export const sampleEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'backstage',
annotations: { [SENTRY_PROJECT_SLUG_ANNOTATION]: 'PROJECT_SLUG' },
},
spec: {
lifecycle: 'experimental',
type: 'library',
owner: 'cncf',
},
};
export const sampleEntityWithoutAnnotation = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'backstage',
annotations: {},
},
spec: {
lifecycle: 'experimental',
type: 'library',
owner: 'cncf',
},
};

View File

@ -0,0 +1,19 @@
import { convertLegacyRouteRefs } from '@backstage/core-compat-api';
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
import {
sentryApi,
entitySentryCard,
entitySentryContent,
} from './alpha/index';
import { rootRouteRef } from './plugin';
/**
* @alpha
*/
export default createFrontendPlugin({
id: 'sentry',
routes: convertLegacyRouteRefs({
entityContent: rootRouteRef,
}),
extensions: [sentryApi, entitySentryCard, entitySentryContent],
});

View File

@ -0,0 +1,30 @@
import {
configApiRef,
ApiBlueprint,
createApiFactory,
discoveryApiRef,
identityApiRef,
} from '@backstage/frontend-plugin-api';
import { sentryApiRef, ProductionSentryApi } from '../api';
/**
* @alpha
*/
export const sentryApi = ApiBlueprint.make({
params: {
factory: createApiFactory({
api: sentryApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new ProductionSentryApi(
discoveryApi,
configApi.getString('sentry.organization'),
identityApi,
),
}),
},
});

View File

@ -0,0 +1,50 @@
import { screen, waitFor } from '@testing-library/react';
import {
createExtensionTester,
renderInTestApp,
TestApiProvider,
} from '@backstage/frontend-test-utils';
import { entitySentryCard } from './entityCard';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { sentryApiRef, MockSentryApi } from '../api';
import {
sampleEntity,
sampleEntityWithoutAnnotation,
} from '../__fixtures__/entity';
import React from 'react';
describe('Entity content extension', () => {
it('should render Sentry issues card', async () => {
renderInTestApp(
<TestApiProvider apis={[[sentryApiRef, new MockSentryApi()]]}>
<EntityProvider entity={sampleEntity}>
{createExtensionTester(entitySentryCard).reactElement()}
</EntityProvider>
</TestApiProvider>,
);
await waitFor(
() => {
expect(screen.getByTestId('sentry-issues-grid')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
it('should render missing state without Sentry annotation', async () => {
renderInTestApp(
<TestApiProvider apis={[[sentryApiRef, new MockSentryApi()]]}>
<EntityProvider entity={sampleEntityWithoutAnnotation}>
{createExtensionTester(entitySentryCard).reactElement()}
</EntityProvider>
</TestApiProvider>,
);
await waitFor(
() => {
expect(screen.getByText('Missing Annotation')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
});

View File

@ -0,0 +1,17 @@
import React from 'react';
import { compatWrapper } from '@backstage/core-compat-api';
import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha';
/**
* @alpha
*/
export const entitySentryCard = EntityCardBlueprint.make({
name: 'sentry-issues',
params: {
filter: 'kind:component',
loader: () =>
import('../components/SentryIssuesWidget').then(m =>
compatWrapper(<m.SentryIssuesWidgetCard />),
),
},
});

View File

@ -0,0 +1,50 @@
import { screen, waitFor } from '@testing-library/react';
import {
createExtensionTester,
renderInTestApp,
TestApiProvider,
} from '@backstage/frontend-test-utils';
import { entitySentryContent } from './entityContent';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { sentryApiRef, MockSentryApi } from '../api';
import {
sampleEntity,
sampleEntityWithoutAnnotation,
} from '../__fixtures__/entity';
import React from 'react';
describe('Entity content extension', () => {
it('should render Sentry tab', async () => {
renderInTestApp(
<TestApiProvider apis={[[sentryApiRef, new MockSentryApi()]]}>
<EntityProvider entity={sampleEntity}>
{createExtensionTester(entitySentryContent).reactElement()}
</EntityProvider>
</TestApiProvider>,
);
await waitFor(
() => {
expect(screen.getByTestId('sentry-issues-grid')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
it('should render missing state without Sentry annotation', async () => {
renderInTestApp(
<TestApiProvider apis={[[sentryApiRef, new MockSentryApi()]]}>
<EntityProvider entity={sampleEntityWithoutAnnotation}>
{createExtensionTester(entitySentryContent).reactElement()}
</EntityProvider>
</TestApiProvider>,
);
await waitFor(
() => {
expect(screen.getByText('Missing Annotation')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
});

View File

@ -0,0 +1,22 @@
import React from 'react';
import {
compatWrapper,
convertLegacyRouteRef,
} from '@backstage/core-compat-api';
import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
import { rootRouteRef } from '../plugin';
/**
* @alpha
*/
export const entitySentryContent = EntityContentBlueprint.make({
name: 'sentry-issues',
params: {
defaultPath: '/sentry',
defaultTitle: 'Sentry',
filter: 'kind:component',
routeRef: convertLegacyRouteRef(rootRouteRef),
loader: () =>
import('../components/Router').then(m => compatWrapper(<m.Router />)),
},
});

View File

@ -0,0 +1,3 @@
export * from './api';
export * from './entityCard';
export * from './entityContent';

View File

@ -15,7 +15,7 @@
*/
import { SentryIssue } from './sentry-issue';
import { createApiRef } from '@backstage/core-plugin-api';
import { createApiRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
/** @public */

View File

@ -15,19 +15,20 @@
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Route, Routes } from 'react-router-dom';
import { SentryIssuesWidget } from './SentryIssuesWidget';
/** @public */
export const Router = (props: { entity: Entity }) => {
export const Router = () => {
const { entity } = useEntity();
return (
<Routes>
<Route
path="/"
element={
<SentryIssuesWidget
entity={props.entity}
entity={entity}
statsFor="24h"
tableOptions={{
padding: 'dense',
@ -38,7 +39,6 @@ export const Router = (props: { entity: Entity }) => {
/>
}
/>
)
</Routes>
);
};

View File

@ -108,7 +108,7 @@ const SentryIssuesTable = (props: SentryIssuesTableProps) => {
columns={columns}
options={tableOptions}
title={
<Grid container>
<Grid data-testid="sentry-issues-grid" container>
<Grid item>Sentry Issues</Grid>
<Grid item>
<FormControl variant="outlined" size="small">

View File

@ -20,7 +20,10 @@ import useAsync from 'react-use/esm/useAsync';
import { sentryApiRef } from '../../api';
import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable';
import { SENTRY_PROJECT_SLUG_ANNOTATION, useProjectSlug } from '../hooks';
import { MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react';
import {
MissingAnnotationEmptyState,
useEntity,
} from '@backstage/plugin-catalog-react';
import {
EmptyState,
@ -93,3 +96,20 @@ export const SentryIssuesWidget = (props: {
/>
);
};
/** @public */
export const SentryIssuesWidgetCard = () => {
const { entity } = useEntity();
return (
<SentryIssuesWidget
entity={entity}
statsFor="24h"
tableOptions={{
padding: 'dense',
paging: true,
search: false,
pageSize: 5,
}}
/>
);
};

View File

@ -14,4 +14,7 @@
* limitations under the License.
*/
export { SentryIssuesWidget } from './SentryIssuesWidget';
export {
SentryIssuesWidget,
SentryIssuesWidgetCard,
} from './SentryIssuesWidget';

File diff suppressed because it is too large Load Diff