repo: promote app-next to main example app

This renames packages to make the new frontend system the default:

- packages/app → packages/app-legacy (example-app-legacy)
- packages/app-next → packages/app (example-app)
- packages/app-next-example-plugin → packages/app-example-plugin

Updated all related configuration, scripts, and documentation.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Patrik Oldsberg
2026-02-05 23:12:06 +01:00
parent 9848734ce6
commit be7ebadb21
93 changed files with 15461 additions and 15241 deletions
+5
View File
@@ -0,0 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'@backstage/no-top-level-material-ui-4-imports': 'error',
},
});
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
# example-app-legacy
This package is an example of a Backstage application using the old (legacy) frontend system.
**NOTE:** This is the legacy frontend system. For new projects, use `packages/app` which uses the new frontend system.
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-app-legacy
title: example-app-legacy
spec:
lifecycle: experimental
type: backstage-frontend
owner: framework-maintainers
+29
View File
@@ -0,0 +1,29 @@
# Knip report
## Unused dependencies (8)
| Name | Location | Severity |
| :------------------------------ | :----------- | :------- |
| @backstage/plugin-search-common | packages/app-legacy/package.json | error |
| @backstage/plugin-auth-react | packages/app-legacy/package.json | error |
| @backstage/frontend-app-api | packages/app-legacy/package.json | error |
| @material-ui/lab | packages/app-legacy/package.json | error |
| zen-observable | packages/app-legacy/package.json | error |
| @octokit/rest | packages/app-legacy/package.json | error |
| react-router | packages/app-legacy/package.json | error |
| history | packages/app-legacy/package.json | error |
## Unused devDependencies (3)
| Name | Location | Severity |
| :-------------------------- | :----------- | :------- |
| @testing-library/user-event | packages/app-legacy/package.json | error |
| @types/zen-observable | packages/app-legacy/package.json | error |
| @types/jquery | packages/app-legacy/package.json | error |
## Unlisted dependencies (1)
| Name | Location | Severity |
| :---------- | :------------------------------------------------------- | :------- |
| @rjsf/utils | packages/app/src/components/scaffolder/customScaffolderExtensions.tsx | error |
+104
View File
@@ -0,0 +1,104 @@
{
"name": "example-app-legacy",
"version": "0.2.118-next.1",
"backstage": {
"role": "frontend"
},
"private": true,
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/app-legacy"
},
"license": "Apache-2.0",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"start": "cross-env EXPERIMENTAL_LAZY_COMPILATION=1 backstage-cli package start",
"test": "backstage-cli package test"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@backstage/app-defaults": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-app-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-api-docs": "workspace:^",
"@backstage/plugin-auth-react": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-graph": "workspace:^",
"@backstage/plugin-catalog-import": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-catalog-unprocessed-entities": "workspace:^",
"@backstage/plugin-devtools": "workspace:^",
"@backstage/plugin-home": "workspace:^",
"@backstage/plugin-home-react": "workspace:^",
"@backstage/plugin-kubernetes": "workspace:^",
"@backstage/plugin-kubernetes-cluster": "workspace:^",
"@backstage/plugin-mui-to-bui": "workspace:^",
"@backstage/plugin-notifications": "workspace:^",
"@backstage/plugin-org": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/plugin-scaffolder": "workspace:^",
"@backstage/plugin-scaffolder-react": "workspace:^",
"@backstage/plugin-search": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-search-react": "workspace:^",
"@backstage/plugin-signals": "workspace:^",
"@backstage/plugin-techdocs": "workspace:^",
"@backstage/plugin-techdocs-module-addons-contrib": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/plugin-user-settings": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router": "^6.30.2",
"react-router-dom": "^6.30.2",
"react-use": "^17.2.4",
"zen-observable": "^0.10.0"
},
"devDependencies": {
"@backstage/test-utils": "workspace:^",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/jquery": "^3.3.34",
"@types/react": "*",
"@types/react-dom": "*",
"@types/zen-observable": "^0.8.0",
"axios": "^1.13.0",
"cross-env": "^10.0.0",
"msw": "^1.0.0"
},
"bundled": true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 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

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="favicon" viewBox="0 0 337.46 428.5"><title>Backstage favicon</title><style>#favicon path{fill:#121212}@media (prefers-color-scheme:dark){#favicon path{fill:#7df3e1}}</style><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>

After

Width:  |  Height:  |  Size: 3.0 KiB

+101
View File
@@ -0,0 +1,101 @@
<!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="color-scheme" content="light dark" />
<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>
<% if (config.has('app.datadogRum')) { %>
<script>
(function (h, o, u, n, d) {
h = h[d] = h[d] || {
q: [],
onReady: function (c) {
h.q.push(c);
},
};
d = o.createElement(u);
d.async = 1;
d.src = n;
n = o.getElementsByTagName(u)[0];
n.parentNode.insertBefore(d, n);
})(
window,
document,
'script',
'https://www.datadoghq-browser-agent.com/datadog-rum-v3.js',
'DD_RUM',
);
DD_RUM.onReady(function () {
DD_RUM.init({
clientToken: '<%= config.getString("app.datadogRum.clientToken") %>',
applicationId:
'<%= config.getString("app.datadogRum.applicationId") %>',
site: '<%= config.getOptionalString("app.datadogRum.site") || "datadoghq.com" %>',
service: 'backstage',
env: '<%= config.getString("app.datadogRum.env") %>',
sampleRate:
'<%= config.getOptionalNumber("app.datadogRum.sessionSampleRate") || 100 %>',
sessionReplaySampleRate:
'<%= config.getOptionalNumber("app.datadogRum.sessionReplaySampleRate") || 0 %>',
trackInteractions: true,
});
});
</script>
<% } %>
</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>
+15
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"
}
+2
View File
@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
@@ -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

+50
View File
@@ -0,0 +1,50 @@
/*
* 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 { 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',
support: { url: 'http://localhost:7007/support' },
},
backend: { baseUrl: 'http://localhost:7007' },
lighthouse: {
baseUrl: 'http://localhost:3003',
},
techdocs: {
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
},
},
context: 'test',
},
] as any,
};
const rendered = render(<App />);
await waitFor(() => {
expect(rendered.baseElement).toBeInTheDocument();
});
});
});
+226
View File
@@ -0,0 +1,226 @@
/*
* 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 { createApp } from '@backstage/app-defaults';
import { AppRouter, FeatureFlagged, FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { ApiExplorerPage } from '@backstage/plugin-api-docs';
import { CatalogEntityPage, CatalogIndexPage } from '@backstage/plugin-catalog';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
import { HomepageCompositionRoot, VisitListener } from '@backstage/plugin-home';
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
import {
ScaffolderFieldExtensions,
ScaffolderLayouts,
} from '@backstage/plugin-scaffolder-react';
import { SearchPage } from '@backstage/plugin-search';
import {
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import {
ExpandableNavigation,
LightBox,
ReportIssue,
TextSize,
} from '@backstage/plugin-techdocs-module-addons-contrib';
import {
SettingsLayout,
UserSettingsPage,
} from '@backstage/plugin-user-settings';
import { AdvancedSettings } from './components/advancedSettings';
import AlarmIcon from '@material-ui/icons/Alarm';
import { Navigate, Route } from 'react-router-dom';
import { apis } from './apis';
import { entityPage } from './components/catalog/EntityPage';
import { Root } from './components/Root';
import { DelayingComponentFieldExtension } from './components/scaffolder/customScaffolderExtensions';
import { defaultPreviewTemplate } from './components/scaffolder/defaultPreviewTemplate';
import { searchPage } from './components/search/SearchPage';
import { providers } from './identityProviders';
import { SignalsDisplay } from '@backstage/plugin-signals';
import { techDocsPage } from './components/techdocs/TechDocsPage';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
import { DevToolsPage } from '@backstage/plugin-devtools';
import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
import {
NotificationsPage,
UserNotificationSettingsCard,
} from '@backstage/plugin-notifications';
import { CustomizableHomePage } from './components/home/CustomizableHomePage';
import { HomePage } from './components/home/HomePage';
import { BuiThemerPage } from '@backstage/plugin-mui-to-bui';
const app = createApp({
apis,
icons: {
// Custom icon example
alert: AlarmIcon,
},
featureFlags: [
{
name: 'scaffolder-next-preview',
description: 'Preview the new Scaffolder Next',
pluginId: '',
},
],
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
);
},
},
});
const routes = (
<FlatRoutes>
<Route path="/" element={<Navigate to="catalog" />} />
{/* TODO(rubenl): Move this to / once its more mature and components exist */}
<FeatureFlagged with="customizable-home-page-preview">
<Route path="/home" element={<HomepageCompositionRoot />}>
<CustomizableHomePage />
</Route>
</FeatureFlagged>
<FeatureFlagged without="customizable-home-page-preview">
<Route path="/home" element={<HomepageCompositionRoot />}>
<HomePage />
</Route>
</FeatureFlagged>
<Route
path="/catalog"
element={<CatalogIndexPage pagination={{ mode: 'offset', limit: 20 }} />}
/>
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
>
{entityPage}
</Route>
<Route
path="/catalog-unprocessed-entities"
element={<CatalogUnprocessedEntitiesPage />}
/>
<Route
path="/catalog-import"
element={
<RequirePermission permission={catalogEntityCreatePermission}>
<CatalogImportPage />
</RequirePermission>
}
/>
<Route
path="/catalog-graph"
element={
<CatalogGraphPage
initialState={{
selectedKinds: ['component', 'domain', 'system', 'api', 'group'],
}}
/>
}
/>
<Route
path="/docs"
element={<TechDocsIndexPage pagination={{ mode: 'offset', limit: 20 }} />}
/>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
{techDocsPage}
<TechDocsAddons>
<ExpandableNavigation />
<ReportIssue />
<TextSize />
<LightBox />
</TechDocsAddons>
</Route>
<Route
path="/create"
element={
<ScaffolderPage
defaultPreviewTemplate={defaultPreviewTemplate}
groups={[
{
title: 'Recommended',
filter: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
]}
/>
}
>
<ScaffolderFieldExtensions>
<DelayingComponentFieldExtension />
</ScaffolderFieldExtensions>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
<Route path="/settings" element={<UserSettingsPage />}>
<SettingsLayout.Route path="/advanced" title="Advanced">
<AdvancedSettings />
</SettingsLayout.Route>
<SettingsLayout.Route path="/notifications" title="Notifications">
<UserNotificationSettingsCard
originNames={{ 'plugin:scaffolder': 'Scaffolder' }}
/>
</SettingsLayout.Route>
</Route>
<Route path="/devtools" element={<DevToolsPage />}>
{customDevToolsPage}
</Route>
<Route path="/notifications" element={<NotificationsPage />} />
<Route path="/mui-to-bui" element={<BuiThemerPage />} />
</FlatRoutes>
);
export default app.createRoot(
<>
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<SignalsDisplay />
<AppRouter>
<VisitListener />
<Root>{routes}</Root>
</AppRouter>
</>,
);
@@ -0,0 +1,57 @@
/*
* 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 { Config } from '@backstage/config';
import { UrlPatternDiscovery } from '@backstage/core-app-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
export class AuthProxyDiscoveryApi implements DiscoveryApi {
private urlPatternDiscovery: UrlPatternDiscovery;
private readonly isAuthProxyingEnabled?: boolean;
private readonly authProxyUrl?: string;
constructor(
baseUrl: string,
isAuthProxyingEnabled?: boolean,
authProxyUrl?: string,
) {
this.isAuthProxyingEnabled = isAuthProxyingEnabled;
this.authProxyUrl = authProxyUrl;
this.urlPatternDiscovery = UrlPatternDiscovery.compile(
`${baseUrl}/api/{{ pluginId }}`,
);
}
async getBaseUrl(pluginId: string) {
if (
pluginId === 'auth' &&
this.isAuthProxyingEnabled &&
this.authProxyUrl
) {
return this.authProxyUrl;
}
return this.urlPatternDiscovery.getBaseUrl(pluginId);
}
static fromConfig(config: Config) {
return new AuthProxyDiscoveryApi(
config.getString('backend.baseUrl'),
config.getOptionalBoolean('auth.proxy.enabled'),
config.getOptionalString('auth.proxy.url'),
);
}
}
+77
View File
@@ -0,0 +1,77 @@
/*
* 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 {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
discoveryApiRef,
fetchApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { AuthProxyDiscoveryApi } from './AuthProxyDiscoveryApi';
import { formDecoratorsApiRef } from '@backstage/plugin-scaffolder/alpha';
import { DefaultScaffolderFormDecoratorsApi } from '@backstage/plugin-scaffolder/alpha';
import { mockDecorator } from './components/scaffolder/decorators';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { ScaffolderClient } from '@backstage/plugin-scaffolder';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => AuthProxyDiscoveryApi.fromConfig(configApi),
}),
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
createApiFactory({
api: scaffolderApiRef,
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi, fetchApi, scmIntegrationsApi, identityApi }) =>
new ScaffolderClient({
useLongPollingLogs: true,
discoveryApi,
fetchApi,
scmIntegrationsApi,
identityApi,
}),
}),
createApiFactory({
api: formDecoratorsApiRef,
deps: {},
factory: () =>
DefaultScaffolderFormDecoratorsApi.create({
decorators: [mockDecorator],
}),
}),
ScmAuth.createDefaultApiFactory(),
];
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
/*
* 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.
*/
// @ts-check
// NOTE: This file is intentionally .jsx, so that there is one file in this repo where we make sure .jsx files work.
import { makeStyles } from '@material-ui/core/styles';
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;
@@ -0,0 +1,175 @@
/*
* 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 { PropsWithChildren } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import HomeIcon from '@material-ui/icons/Home';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import SearchIcon from '@material-ui/icons/Search';
import MenuIcon from '@material-ui/icons/Menu';
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 {
Link,
Sidebar,
sidebarConfig,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
SidebarSubmenu,
SidebarSubmenuItem,
useSidebarOpenState,
} from '@backstage/core-components';
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import { SearchModal } from '../search/SearchModal';
import { useApp } from '@backstage/core-plugin-api';
import BuildIcon from '@material-ui/icons/Build';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
import UpdateIcon from '@material-ui/icons/Update';
import CategoryIcon from '@material-ui/icons/Category';
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>
{({ toggleModal }) => <SearchModal toggleModal={toggleModal} />}
</SidebarSearchModal>
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="home" text="Home" />
<SidebarItem icon={CategoryIcon} to="/" text="Catalog">
<SidebarSubmenu title="Catalog">
<SidebarSubmenuItem
title="Domains"
to="catalog?filters[kind]=domain"
icon={useApp().getSystemIcon('kind:domain')}
/>
<SidebarSubmenuItem
title="Systems"
to="catalog?filters[kind]=system"
icon={useApp().getSystemIcon('kind:system')}
/>
<SidebarSubmenuItem
title="Components"
to="catalog?filters[kind]=component"
icon={useApp().getSystemIcon('kind:component')}
/>
<SidebarSubmenuItem
title="APIs"
to="catalog?filters[kind]=api"
icon={useApp().getSystemIcon('kind:api')}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Resources"
to="catalog?filters[kind]=resource"
icon={useApp().getSystemIcon('kind:resource')}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Groups"
to="catalog?filters[kind]=group"
icon={useApp().getSystemIcon('kind:group')}
/>
<SidebarSubmenuItem
title="Users"
to="catalog?filters[kind]=user"
icon={useApp().getSystemIcon('kind:user')}
/>
</SidebarSubmenu>
</SidebarItem>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={useApp().getSystemIcon('group')!}
/>
<SidebarItem
icon={useApp().getSystemIcon('kind:api')!}
to="api-docs"
text="APIs"
/>
<SidebarItem
icon={useApp().getSystemIcon('docs')!}
to="docs"
text="Docs"
/>
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
{/* End global nav */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem
icon={UpdateIcon}
to="catalog-unprocessed-entities"
text="Unprocessed Entities"
/>
</SidebarScrollWrapper>
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<NotificationsSidebarItem />
<SidebarDivider />
<SidebarGroup
label="Settings"
icon={<UserSettingsSignInAvatar />}
to="/settings"
>
<SidebarSettings />
<SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
</SidebarGroup>
</Sidebar>
{children}
</SidebarPage>
);
@@ -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 { Root } from './Root';
@@ -0,0 +1,61 @@
/*
* 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 { ChangeEvent } from 'react';
import { InfoCard } from '@backstage/core-components';
import List from '@material-ui/core/List';
import Grid from '@material-ui/core/Grid';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import Switch from '@material-ui/core/Switch';
import useLocalStorage from 'react-use/esm/useLocalStorage';
export function AdvancedSettings() {
const [value, setValue] = useLocalStorage<'on' | 'off'>(
'advanced-option',
'off',
);
const toggleValue = (ev: ChangeEvent<HTMLInputElement>) => {
setValue(ev.currentTarget.checked ? 'on' : 'off');
};
return (
<Grid container direction="row" spacing={3}>
<Grid item xs={12} md={6}>
<InfoCard title="Advanced settings" variant="gridItem">
<List>
<ListItem>
<ListItemText
primary="Advanced user option"
secondary="An extra settings tab to further customize the experience"
/>
<ListItemSecondaryAction>
<Switch
color="primary"
value={value}
onChange={toggleValue}
name="advanced"
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</InfoCard>
</Grid>
</Grid>
);
}
@@ -0,0 +1,17 @@
/*
* 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 * from './AdvancedSettings';
@@ -0,0 +1,83 @@
/*
* 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 { EntityLayout, catalogPlugin } from '@backstage/plugin-catalog';
import {
EntityProvider,
starredEntitiesApiRef,
MockStarredEntitiesApi,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { cicdContent } from './EntityPage';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
describe('EntityPage Test', () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'ExampleComponent',
annotations: {
'github.com/project-slug': 'example/project',
},
},
spec: {
owner: 'guest',
type: 'service',
lifecycle: 'production',
},
};
const rootRouteRef = catalogPlugin.routes.catalogIndex;
describe('cicdContent', () => {
it('Should render GitHub Actions View', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, mockApis.permission()],
[catalogApiRef, catalogApiMock()],
]}
>
<EntityProvider entity={entity}>
<EntityLayout>
<EntityLayout.Route path="/ci-cd" title="CI-CD">
{cicdContent}
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog': rootRouteRef,
},
},
);
expect(rendered.getByText('ExampleComponent')).toBeInTheDocument();
await expect(
rendered.findByText('No CI/CD available for this entity'),
).resolves.toBeInTheDocument();
});
});
});
@@ -0,0 +1,472 @@
/*
* 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 {
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 { EmptyState } from '@backstage/core-components';
import {
EntityApiDefinitionCard,
EntityConsumedApisCard,
EntityConsumingComponentsCard,
EntityHasApisCard,
EntityProvidedApisCard,
EntityProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasResourcesCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLayout,
EntityLinksCard,
EntityLabelsCard,
EntityOrphanWarning,
EntityProcessingErrorsPanel,
EntitySwitch,
hasCatalogProcessingErrors,
isComponentType,
isKind,
isOrphan,
hasLabels,
hasRelationWarnings,
EntityRelationWarning,
} from '@backstage/plugin-catalog';
import {
Direction,
EntityCatalogGraphCard,
} from '@backstage/plugin-catalog-graph';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
import {
isKubernetesClusterAvailable,
EntityKubernetesClusterContent,
} from '@backstage/plugin-kubernetes-cluster';
import {
EntityGroupProfileCard,
EntityMembersListCard,
EntityOwnershipCard,
EntityUserProfileCard,
} from '@backstage/plugin-org';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import { ReactNode } from 'react';
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import {
TextSize,
ReportIssue,
LightBox,
} from '@backstage/plugin-techdocs-module-addons-contrib';
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
const customEntityFilterKind = ['Component', 'API', 'System'];
const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
return (
<EntityLayout
parentEntityRelations={['partOf', 'memberOf', 'childOf']}
UNSTABLE_contextMenuOptions={{
disableUnregister: 'visible',
}}
>
{props.children}
</EntityLayout>
);
};
const techdocsContent = (
<EntityTechdocsContent>
<TechDocsAddons>
<TextSize />
<ReportIssue />
<LightBox />
</TechDocsAddons>
</EntityTechdocsContent>
);
/**
* 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.
*/
export const cicdContent = (
<EntitySwitch>
<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} xs={12}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<EntitySwitch>
<EntitySwitch.Case if={hasLabels}>
<Grid item md={4} xs={12}>
<EntityLabelsCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
</Grid>
);
const serviceEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<EntityProvidedApisCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityConsumedApisCard />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const websiteEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</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.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const defaultEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
</EntityLayoutWrapper>
);
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 = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item md={6} xs={12}>
<EntityAboutCard />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item xs={12}>
<Grid container>
<Grid item xs={12} md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/definition" title="Definition">
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityApiDefinitionCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const userPage = (
<EntityLayoutWrapper>
<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"
entityFilterKind={customEntityFilterKind}
/>
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const groupPage = (
<EntityLayoutWrapper>
<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"
entityFilterKind={customEntityFilterKind}
/>
</Grid>
<Grid item xs={12} md={6}>
<EntityMembersListCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityLinksCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayoutWrapper>
);
const systemPage = (
<EntityLayoutWrapper>
<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}>
<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>
</EntityLayoutWrapper>
);
const domainPage = (
<EntityLayoutWrapper>
<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>
</EntityLayoutWrapper>
);
const resourcePage = (
<EntityLayoutWrapper>
<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.Route
path="/kubernetes-cluster"
title="Kubernetes Cluster"
if={isKubernetesClusterAvailable}
>
<EntityKubernetesClusterContent />
</EntityLayout.Route>
</EntityLayoutWrapper>
);
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 if={isKind('resource')} children={resourcePage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);
@@ -0,0 +1,54 @@
/*
* 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 {
ConfigContent,
ExternalDependenciesContent,
InfoContent,
ScheduledTasksContent,
} from '@backstage/plugin-devtools';
import { DevToolsLayout } from '@backstage/plugin-devtools';
import { UnprocessedEntitiesContent } from '@backstage/plugin-catalog-unprocessed-entities';
const DevToolsPage = () => {
return (
<DevToolsLayout>
<DevToolsLayout.Route path="info" title="Info">
<InfoContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route path="config" title="Config">
<ConfigContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route path="scheduled-tasks" title="Scheduled Tasks">
<ScheduledTasksContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route
path="external-dependencies"
title="External Dependencies"
>
<ExternalDependenciesContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route
path="unprocessed-entities"
title="Unprocessed Entities"
>
<UnprocessedEntitiesContent />
</DevToolsLayout.Route>
</DevToolsLayout>
);
};
export const customDevToolsPage = <DevToolsPage />;
@@ -0,0 +1,97 @@
/*
* Copyright 2025 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 { Page, Content } from '@backstage/core-components';
import {
HomePageCompanyLogo,
TemplateBackstageLogo,
HomePageStarredEntities,
HomePageToolkit,
CustomHomepageGrid,
HomePageRandomJoke,
HomePageTopVisited,
HomePageRecentlyVisited,
} from '@backstage/plugin-home';
import { HomePageSearchBar } from '@backstage/plugin-search';
import Grid from '@material-ui/core/Grid';
import { tools, useLogoStyles } from './shared';
import { WelcomeTitle } from '@backstage/plugin-home';
const defaultConfig = [
{
component: 'HomePageSearchBar',
x: 0,
y: 0,
width: 24,
height: 2,
deletable: false,
},
{
component: 'HomePageRecentlyVisited',
x: 0,
y: 1,
width: 5,
height: 4,
},
{
component: 'HomePageTopVisited',
x: 5,
y: 1,
width: 5,
height: 4,
},
{
component: 'HomePageStarredEntities',
x: 0,
y: 2,
width: 6,
height: 4,
},
{
component: 'HomePageToolkit',
x: 6,
y: 6,
width: 4,
height: 4,
},
];
export const CustomizableHomePage = () => {
const { svg, path, container } = useLogoStyles();
return (
<Page themeId="home">
<Content>
<Grid container justifyContent="center">
<HomePageCompanyLogo
className={container}
logo={<TemplateBackstageLogo classes={{ svg, path }} />}
/>
</Grid>
<CustomHomepageGrid config={defaultConfig}>
<WelcomeTitle variant="h1" />
<HomePageSearchBar />
<HomePageRecentlyVisited />
<HomePageTopVisited />
<HomePageToolkit tools={tools} />
<HomePageStarredEntities />
<HomePageRandomJoke />
</CustomHomepageGrid>
</Content>
</Page>
);
};
@@ -0,0 +1,125 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page, Content, Header } from '@backstage/core-components';
import {
HomePageCompanyLogo,
TemplateBackstageLogo,
HomePageStarredEntities,
HomePageToolkit,
HomePageTopVisited,
HomePageRecentlyVisited,
WelcomeTitle,
HeaderWorldClock,
ClockConfig,
} from '@backstage/plugin-home';
import { HomePageSearchBar } from '@backstage/plugin-search';
import { SearchContextProvider } from '@backstage/plugin-search-react';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';
import { tools, useLogoStyles } from './shared';
const useStyles = makeStyles(theme => ({
searchBarInput: {
maxWidth: '60vw',
margin: 'auto',
backgroundColor: theme.palette.background.paper,
borderRadius: '50px',
boxShadow: theme.shadows[1],
},
searchBarOutline: {
borderStyle: 'none',
},
}));
const clockConfigs: ClockConfig[] = [
{
label: 'NYC',
timeZone: 'America/New_York',
},
{
label: 'UTC',
timeZone: 'UTC',
},
{
label: 'STO',
timeZone: 'Europe/Stockholm',
},
{
label: 'TYO',
timeZone: 'Asia/Tokyo',
},
];
const timeFormat: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
hour12: false,
};
export const HomePage = () => {
const classes = useStyles();
const { svg, path, container } = useLogoStyles();
return (
<SearchContextProvider>
<Page themeId="home">
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
<HeaderWorldClock
clockConfigs={clockConfigs}
customTimeFormat={timeFormat}
/>
</Header>
<Content>
<Grid container justifyContent="center" spacing={2}>
<HomePageCompanyLogo
className={container}
logo={<TemplateBackstageLogo classes={{ svg, path }} />}
/>
<Grid container item xs={12} justifyContent="center">
<HomePageSearchBar
InputProps={{
classes: {
root: classes.searchBarInput,
notchedOutline: classes.searchBarOutline,
},
}}
placeholder="Search"
/>
</Grid>
<Grid container item xs={12}>
<Grid item xs={12} md={6}>
<HomePageTopVisited />
</Grid>
<Grid item xs={12} md={6}>
<HomePageRecentlyVisited />
</Grid>
</Grid>
<Grid container item xs={12}>
<Grid item xs={7}>
<HomePageStarredEntities />
</Grid>
<Grid item xs={5}>
<HomePageToolkit tools={tools} />
</Grid>
</Grid>
</Grid>
</Content>
</Page>
</SearchContextProvider>
);
};
@@ -0,0 +1,59 @@
/*
* Copyright 2025 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 { TemplateBackstageLogoIcon } from '@backstage/plugin-home';
import { makeStyles } from '@material-ui/core/styles';
export const useLogoStyles = makeStyles(theme => ({
container: {
margin: theme.spacing(5, 0),
},
svg: {
width: 'auto',
height: 100,
},
path: {
fill: '#7df3e1',
},
}));
export const tools = [
{
url: 'https://backstage.io/docs',
label: 'Docs',
icon: <TemplateBackstageLogoIcon />,
},
{
url: 'https://github.com/backstage/backstage',
label: 'GitHub',
icon: <TemplateBackstageLogoIcon />,
},
{
url: 'https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md',
label: 'Contributing',
icon: <TemplateBackstageLogoIcon />,
},
{
url: 'https://backstage.io/plugins',
label: 'Plugins Directory',
icon: <TemplateBackstageLogoIcon />,
},
{
url: 'https://github.com/backstage/backstage/issues/new/choose',
label: 'Submit New Issue',
icon: <TemplateBackstageLogoIcon />,
},
];
@@ -0,0 +1,149 @@
/*
* Copyright 2023 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 {
HomePageStarredEntities,
HomePageRandomJoke,
CustomHomepageGrid,
} from '@backstage/plugin-home';
import {
starredEntitiesApiRef,
MockStarredEntitiesApi,
entityRouteRef,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
import { configApiRef } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/config';
import { searchApiRef } from '@backstage/plugin-search-react';
import { HomePageSearchBar, searchPlugin } from '@backstage/plugin-search';
import { ComponentType } from 'react';
const entities = [
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity',
title: 'Mock Starred Entity!',
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity-2',
title: 'Mock Starred Entity 2!',
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity-3',
title: 'Mock Starred Entity 3!',
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity-4',
title: 'Mock Starred Entity 4!',
},
},
];
const catalogApi = catalogApiMock({ entities });
const starredEntitiesApi = new MockStarredEntitiesApi();
starredEntitiesApi.toggleStarred('component:default/example-starred-entity');
starredEntitiesApi.toggleStarred('component:default/example-starred-entity-2');
starredEntitiesApi.toggleStarred('component:default/example-starred-entity-3');
starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4');
export default {
title: 'Plugins/Home/Templates',
tags: ['!manifest'],
decorators: [
(Story: ComponentType<{}>) =>
wrapInTestApp(
<>
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[starredEntitiesApiRef, starredEntitiesApi],
[searchApiRef, { query: () => Promise.resolve({ results: [] }) }],
[
configApiRef,
new ConfigReader({
backend: {
baseUrl: 'https://localhost:7007',
},
}),
],
]}
>
<Story />
</TestApiProvider>
</>,
{
mountedRoutes: {
'/hello-company': searchPlugin.routes.root,
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
),
],
};
export const CustomizableTemplate = () => {
// This is the default configuration that is shown to the user
// when first arriving to the homepage.
const defaultConfig = [
{
component: 'HomePageSearchBar',
x: 0,
y: 0,
width: 12,
height: 5,
},
{
component: 'HomePageRandomJoke',
x: 0,
y: 2,
width: 6,
height: 16,
},
{
component: 'HomePageStarredEntities',
x: 6,
y: 2,
width: 6,
height: 12,
},
];
return (
<CustomHomepageGrid config={defaultConfig} rowHeight={10}>
// Insert the allowed widgets inside the grid. User can add, organize and
// remove the widgets as they want.
<HomePageSearchBar />
<HomePageRandomJoke />
<HomePageStarredEntities />
</CustomHomepageGrid>
);
};
@@ -0,0 +1,197 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
HomePageToolkit,
HomePageCompanyLogo,
HomePageStarredEntities,
TemplateBackstageLogo,
TemplateBackstageLogoIcon,
} from '@backstage/plugin-home';
import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
import { Content, Page, InfoCard } from '@backstage/core-components';
import {
starredEntitiesApiRef,
MockStarredEntitiesApi,
entityRouteRef,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { configApiRef } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/config';
import { HomePageSearchBar, searchPlugin } from '@backstage/plugin-search';
import {
searchApiRef,
SearchContextProvider,
} from '@backstage/plugin-search-react';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';
import { ComponentType, PropsWithChildren } from 'react';
const entities = [
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity',
title: 'Mock Starred Entity!',
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity-2',
title: 'Mock Starred Entity 2!',
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity-3',
title: 'Mock Starred Entity 3!',
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'mock-starred-entity-4',
title: 'Mock Starred Entity 4!',
},
},
];
const catalogApi = catalogApiMock({ entities });
const starredEntitiesApi = new MockStarredEntitiesApi();
starredEntitiesApi.toggleStarred('component:default/example-starred-entity');
starredEntitiesApi.toggleStarred('component:default/example-starred-entity-2');
starredEntitiesApi.toggleStarred('component:default/example-starred-entity-3');
starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4');
export default {
title: 'Plugins/Home/Templates',
tags: ['!manifest'],
decorators: [
(Story: ComponentType<PropsWithChildren<{}>>) =>
wrapInTestApp(
<>
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[starredEntitiesApiRef, starredEntitiesApi],
[searchApiRef, { query: () => Promise.resolve({ results: [] }) }],
[
configApiRef,
new ConfigReader({
stackoverflow: {
baseUrl: 'https://api.stackexchange.com/2.2',
},
}),
],
]}
>
<Story />
</TestApiProvider>
</>,
{
mountedRoutes: {
'/hello-company': searchPlugin.routes.root,
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
),
],
};
const useStyles = makeStyles(theme => ({
searchBarInput: {
maxWidth: '60vw',
margin: 'auto',
backgroundColor: theme.palette.background.paper,
borderRadius: '50px',
boxShadow: theme.shadows[1],
},
searchBarOutline: {
borderStyle: 'none',
},
}));
const useLogoStyles = makeStyles(theme => ({
container: {
margin: theme.spacing(5, 0),
},
svg: {
width: 'auto',
height: 100,
},
path: {
fill: '#7df3e1',
},
}));
export const DefaultTemplate = () => {
const classes = useStyles();
const { svg, path, container } = useLogoStyles();
return (
<SearchContextProvider>
<Page themeId="home">
<Content>
<Grid container justifyContent="center" spacing={6}>
<HomePageCompanyLogo
className={container}
logo={<TemplateBackstageLogo classes={{ svg, path }} />}
/>
<Grid container item xs={12} justifyContent="center">
<HomePageSearchBar
InputProps={{
classes: {
root: classes.searchBarInput,
notchedOutline: classes.searchBarOutline,
},
}}
placeholder="Search"
/>
</Grid>
<Grid container item xs={12}>
<Grid item xs={12} md={6}>
<HomePageStarredEntities />
</Grid>
<Grid item xs={12} md={6}>
<HomePageToolkit
tools={Array(8).fill({
url: '#',
label: 'link',
icon: <TemplateBackstageLogoIcon />,
})}
/>
</Grid>
<Grid item xs={12} md={6}>
<InfoCard title="Composable Section">
{/* placeholder for content */}
<div style={{ height: 370 }} />
</InfoCard>
</Grid>
</Grid>
</Grid>
</Content>
</Page>
</SearchContextProvider>
);
};
@@ -0,0 +1,96 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { FieldValidation } from '@rjsf/utils';
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import TextField from '@material-ui/core/TextField';
import {
createScaffolderFieldExtension,
FieldExtensionComponentProps,
} from '@backstage/plugin-scaffolder-react';
const TextValuePicker = (props: FieldExtensionComponentProps<string>) => {
const {
onChange,
required,
schema: { title, description },
rawErrors,
formData,
uiSchema: { 'ui:autofocus': autoFocus },
idSchema,
placeholder,
} = props;
return (
<TextField
id={idSchema?.$id}
label={title}
placeholder={placeholder}
helperText={description}
required={required}
value={formData ?? ''}
onChange={({ target: { value } }) => onChange(value)}
margin="normal"
error={rawErrors?.length > 0 && !formData}
inputProps={{ autoFocus }}
/>
);
};
export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'LowerCaseValuePicker',
component: TextValuePicker,
validation: (value: string, validation: FieldValidation) => {
if (value.toLocaleLowerCase('en-US') !== value) {
validation.addError('Only lowercase values are allowed.');
}
},
}),
);
const MockDelayComponent = (
props: FieldExtensionComponentProps<{ test?: string }>,
) => {
const { onChange, formData, rawErrors = [] } = props;
return (
<TextField
label="test"
helperText="description"
value={formData?.test ?? ''}
onChange={({ target: { value } }) => onChange({ test: value })}
margin="normal"
error={rawErrors?.length > 0 && !formData}
/>
);
};
export const DelayingComponentFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'DelayingComponent',
component: MockDelayComponent,
validation: async (
value: { test?: string },
validation: FieldValidation,
) => {
// delay 2 seconds
await new Promise(resolve => setTimeout(resolve, 2000));
if (value.test !== 'pass') {
validation.addError('value was not equal to pass');
}
},
}),
);
@@ -0,0 +1,52 @@
/*
* 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 {
createScaffolderLayout,
LayoutTemplate,
scaffolderPlugin,
} from '@backstage/plugin-scaffolder';
import Grid from '@material-ui/core/Grid';
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
const mid = Math.ceil(properties.length / 2);
const left = properties.slice(0, mid);
const right = properties.slice(mid);
return (
<>
<h1>{title}</h1>
<Grid container justifyContent="flex-end">
{left.map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
{right.map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
</Grid>
{description}
</>
);
};
export const TwoColumnLayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'TwoColumn',
component: TwoColumn,
}),
);
@@ -0,0 +1,36 @@
/*
* Copyright 2024 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 { githubAuthApiRef } from '@backstage/core-plugin-api';
import { createScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
export const mockDecorator = createScaffolderFormDecorator({
id: 'mock-decorator',
schema: {
input: {
test: z => z.string(),
},
},
deps: {
githubApi: githubAuthApiRef,
},
decorator: async (
{ setSecrets, setFormState, input: { test } },
{ githubApi: _githubApi },
) => {
setFormState(state => ({ ...state, test, mock: 'MOCK' }));
setSecrets(state => ({ ...state, GITHUB_TOKEN: 'MOCK_TOKEN' }));
},
});
@@ -0,0 +1,55 @@
/*
* 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 const defaultPreviewTemplate = `# Edit the template parameters below to see how they will render in the scaffolder form UI
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
required:
- name
properties:
name:
title: Name
type: string
description: Unique name of the component
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
- title: Choose a location
required:
- repoUrl
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
- title: Custom Fields
required:
- lowerCaseValue
properties:
lowerCaseValue:
title: Lower Cased Value
type: string
ui:field: LowerCaseValuePicker
`;
@@ -0,0 +1,222 @@
/*
* 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 { CatalogIcon, DocsIcon } from '@backstage/core-components';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
CATALOG_FILTER_EXISTS,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { searchPlugin, SearchType } from '@backstage/plugin-search';
import {
SearchBar,
SearchFilter,
SearchResult,
SearchResultPager,
useSearch,
} from '@backstage/plugin-search-react';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
import Box from '@material-ui/core/Box';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import CloseIcon from '@material-ui/icons/Close';
import { useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
const useStyles = makeStyles(theme => ({
dialogTitle: {
gap: theme.spacing(1),
display: 'grid',
alignItems: 'center',
gridTemplateColumns: '1fr auto',
'&> button': {
marginTop: theme.spacing(1),
},
},
container: {
borderRadius: 30,
display: 'flex',
height: '2.4em',
padding: theme.spacing(1),
},
filter: {
'& + &': {
marginTop: theme.spacing(2.5),
},
},
filters: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
input: {
flex: 1,
},
button: {
'&:hover': {
background: 'none',
},
},
dialogActionsContainer: { padding: theme.spacing(1, 3) },
viewResultsLink: { verticalAlign: '0.5em' },
}));
const rootRouteRef = searchPlugin.routes.root;
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
const classes = useStyles();
const navigate = useNavigate();
const catalogApi = useApi(catalogApiRef);
const { types } = useSearch();
const searchRootRoute = useRouteRef(rootRouteRef)();
const searchBarRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
searchBarRef?.current?.focus();
});
// This handler is called when "enter" is pressed
const handleSearchBarSubmit = useCallback(() => {
toggleModal();
// Using ref to get the current field value without waiting for a query debounce
const query = searchBarRef.current?.value ?? '';
navigate(`${searchRootRoute}?query=${query}`);
}, [navigate, toggleModal, searchRootRoute]);
return (
<>
<DialogTitle>
<Box className={classes.dialogTitle}>
<SearchBar
className={classes.input}
inputProps={{ ref: searchBarRef }}
onSubmit={handleSearchBarSubmit}
/>
<IconButton aria-label="close" onClick={toggleModal}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Grid container direction="column">
<Grid item>
<SearchType.Tabs
defaultValue="software-catalog"
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
},
{
value: 'techdocs',
name: 'Documentation',
},
{
value: 'tools',
name: 'Tools',
},
]}
/>
</Grid>
<Grid item container>
{types.includes('techdocs') && (
<Grid item xs={2}>
<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', 'metadata.title'],
filter: {
'metadata.annotations.backstage.io/techdocs-ref':
CATALOG_FILTER_EXISTS,
},
});
const names = items.map(entity => ({
value: entity.metadata.name,
label: entity.metadata.title ?? entity.metadata.name,
}));
names.sort((a, b) => a.label.localeCompare(b.label));
return names;
}}
/>
</Grid>
)}
<Grid item xs={2}>
<SearchFilter.Select
className={classes.filter}
label="Kind"
name="kind"
values={['Component', 'Template']}
/>
</Grid>
<Grid item xs={2}>
<SearchFilter.Select
className={classes.filter}
label="Lifecycle"
name="lifecycle"
values={['experimental', 'production']}
/>
</Grid>
<Grid
item
xs={types.includes('techdocs') ? 6 : 8}
container
direction="row-reverse"
justifyContent="flex-start"
alignItems="center"
>
<Grid item>
<Button
className={classes.button}
color="primary"
endIcon={<ArrowForwardIcon />}
onClick={handleSearchBarSubmit}
disableRipple
>
View Full Results
</Button>
</Grid>
</Grid>
</Grid>
<Grid item xs>
<SearchResult>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
</SearchResult>
</Grid>
</Grid>
</DialogContent>
<DialogActions className={classes.dialogActionsContainer}>
<Grid container direction="row">
<Grid item xs={12}>
<SearchResultPager />
</Grid>
</Grid>
</DialogActions>
</>
);
};
@@ -0,0 +1,146 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CatalogIcon,
Content,
DocsIcon,
Header,
Page,
useSidebarPinState,
} from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
import {
CATALOG_FILTER_EXISTS,
catalogApiRef,
} from '@backstage/plugin-catalog-react';
import { SearchType } from '@backstage/plugin-search';
import {
SearchBar,
SearchFilter,
SearchPagination,
SearchResult,
SearchResultPager,
useSearch,
} from '@backstage/plugin-search-react';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import { Theme } from '@material-ui/core/styles/createTheme';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
filter: {
'& + &': {
marginTop: theme.spacing(2.5),
},
},
filters: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
}));
const SearchPage = () => {
const classes = useStyles();
const { isMobile } = useSidebarPinState();
const { types } = useSearch();
const catalogApi = useApi(catalogApiRef);
const configApi = useApi(configApiRef);
return (
<Page themeId="home">
{!isMobile && <Header title="Search" />}
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar debounceTime={100} />
</Grid>
{!isMobile && (
<Grid item xs={3}>
<SearchType.Accordion
name="Result type"
defaultValue={configApi.getOptionalString('search.defaultType')}
showCounts
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', 'metadata.title'],
filter: {
'metadata.annotations.backstage.io/techdocs-ref':
CATALOG_FILTER_EXISTS,
},
});
const names = items.map(entity => ({
value: entity.metadata.name,
label: entity.metadata.title ?? entity.metadata.name,
}));
names.sort((a, b) => a.label.localeCompare(b.label));
return names;
}}
/>
)}
<SearchFilter.Select
className={classes.filter}
label="Kind"
name="kind"
values={['Component', 'Template']}
/>
<SearchFilter.Select
className={classes.filter}
label="Lifecycle"
name="lifecycle"
values={['experimental', 'production']}
/>
</Paper>
</Grid>
)}
<Grid item xs>
<SearchPagination />
<SearchResult>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
</SearchResult>
<SearchResultPager />
</Grid>
</Grid>
</Content>
</Page>
);
};
export const searchPage = <SearchPage />;
@@ -0,0 +1,34 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page } from '@backstage/core-components';
import {
TechDocsReaderPageHeader,
TechDocsReaderPageSubheader,
TechDocsReaderPageContent,
} from '@backstage/plugin-techdocs';
const DefaultTechDocsPage = () => {
return (
<Page themeId="documentation">
<TechDocsReaderPageHeader />
<TechDocsReaderPageSubheader />
<TechDocsReaderPageContent />
</Page>
);
};
export const techDocsPage = <DefaultTechDocsPage />;
@@ -0,0 +1,84 @@
/*
* 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 {
googleAuthApiRef,
gitlabAuthApiRef,
oktaAuthApiRef,
githubAuthApiRef,
microsoftAuthApiRef,
oneloginAuthApiRef,
bitbucketAuthApiRef,
bitbucketServerAuthApiRef,
openshiftAuthApiRef,
} from '@backstage/core-plugin-api';
export const providers = [
{
id: 'google-auth-provider',
title: 'Google',
message: 'Sign In using Google',
apiRef: googleAuthApiRef,
},
{
id: 'microsoft-auth-provider',
title: 'Microsoft',
message: 'Sign In using Microsoft Azure AD',
apiRef: microsoftAuthApiRef,
},
{
id: 'gitlab-auth-provider',
title: 'GitLab',
message: 'Sign In using GitLab',
apiRef: gitlabAuthApiRef,
},
{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign In using GitHub',
apiRef: githubAuthApiRef,
},
{
id: 'okta-auth-provider',
title: 'Okta',
message: 'Sign In using Okta',
apiRef: oktaAuthApiRef,
},
{
id: 'onelogin-auth-provider',
title: 'OneLogin',
message: 'Sign In using OneLogin',
apiRef: oneloginAuthApiRef,
},
{
id: 'bitbucket-auth-provider',
title: 'Bitbucket',
message: 'Sign In using Bitbucket',
apiRef: bitbucketAuthApiRef,
},
{
id: 'bitbucket-server-auth-provider',
title: 'Bitbucket Server',
message: 'Sign In using Bitbucket Server',
apiRef: bitbucketServerAuthApiRef,
},
{
id: 'openshift-auth-provider',
title: 'OpenShift',
message: 'Sign In using OpenShift',
apiRef: openshiftAuthApiRef,
},
];
@@ -0,0 +1,67 @@
/*
* 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 { createApp } from '@backstage/app-defaults';
import { AppRouter } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { CookieAuthRedirect } from '@backstage/plugin-auth-react';
import ReactDOM from 'react-dom/client';
import { providers } from '../src/identityProviders';
import {
configApiRef,
createApiFactory,
discoveryApiRef,
} from '@backstage/core-plugin-api';
import { AuthProxyDiscoveryApi } from '../src/AuthProxyDiscoveryApi';
import '@backstage/ui/css/styles.css';
const app = createApp({
apis: [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => AuthProxyDiscoveryApi.fromConfig(configApi),
}),
],
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
);
},
},
});
const App = app.createRoot(
<>
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<AppRouter>
<CookieAuthRedirect />
</AppRouter>
</>,
);
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
+22
View File
@@ -0,0 +1,22 @@
/*
* 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 '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
+15
View File
@@ -0,0 +1,15 @@
/*
* 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.
*/
+21
View File
@@ -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.
*/
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
@@ -0,0 +1,109 @@
/*
* Copyright 2024 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 { registerMswTestHooks } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import axios from 'axios';
// eslint-disable-next-line no-restricted-imports
import http from 'node:http';
// eslint-disable-next-line no-restricted-imports
import https from 'node:https';
const errorMsg = 'Network requests are not allowed in tests';
// These test relates to the @backstage/cli Jest configuration. It makes sure
// that network requests are properly rejected in JSDom environments.
describe('without msw', () => {
it('should reject network requests', async () => {
await expect(fetch('https://example.com')).rejects.toThrow(errorMsg);
await expect(axios('https://example.com')).rejects.toThrow(errorMsg);
expect(() => http.get('http://example.com')).toThrow(errorMsg);
expect(() => https.get('https://example.com')).toThrow(errorMsg);
await expect(
new Promise(resolve => {
const ws = new WebSocket('ws://example.com');
ws.addEventListener('error', () => resolve('error'));
}),
).resolves.toBe('error');
expect(typeof EventSource).toBe('undefined');
expect(() => new XMLHttpRequest()).toThrow(errorMsg);
});
});
// This makes sure that MSW mocks still work as expected
describe('with msw', () => {
const server = setupServer();
registerMswTestHooks(server);
it('should mock network requests', async () => {
server.use(
rest.get('http://example.com', (_, res, ctx) =>
res(ctx.json({ ok: true })),
),
rest.get('https://example.com', (_, res, ctx) =>
res(ctx.json({ ok: true })),
),
);
await expect(
fetch('https://example.com').then(res => res.json()),
).resolves.toEqual({ ok: true });
await expect(
axios('https://example.com').then(res => res.data),
).resolves.toEqual({ ok: true });
await expect(
new Promise(resolve => {
const req = http.get('http://example.com');
req.on('response', res => {
res.on('data', data => {
resolve(JSON.parse(data.toString()));
});
});
}),
).resolves.toEqual({
ok: true,
});
await expect(
new Promise(resolve => {
const req = https.get('https://example.com');
req.on('response', res => {
res.on('data', data => {
resolve(JSON.parse(data.toString()));
});
});
}),
).resolves.toEqual({
ok: true,
});
await expect(
new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com');
xhr.onload = () => {
resolve(JSON.parse(xhr.responseText));
};
xhr.send();
}),
).resolves.toEqual({ ok: true });
});
});