This commit is contained in:
2022-12-22 15:01:37 +01:00
commit 1a4ef09d25
59 changed files with 2126 additions and 0 deletions

9
packages/README.md Normal file
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,6 @@
{
"baseUrl": "http://localhost:3001",
"fixturesFolder": false,
"pluginsFile": false,
"retries": 3
}

View File

@@ -0,0 +1,21 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": [
"error",
{
"assertFunctionNames": ["expect", "cy.contains"]
}
],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true,
"optionalDependencies": true,
"peerDependencies": true,
"bundledDependencies": true
}
]
}
}

View File

@@ -0,0 +1,6 @@
describe('App', () => {
it('should render the catalog', () => {
cy.visit('/');
cy.contains('My Company Catalog');
});
});

83
packages/app/package.json Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "app",
"version": "0.0.0",
"private": true,
"bundled": true,
"backstage": {
"role": "frontend"
},
"dependencies": {
"@backstage/app-defaults": "^1.0.3",
"@backstage/catalog-model": "^1.0.3",
"@backstage/cli": "^0.17.2",
"@backstage/core-app-api": "^1.0.3",
"@backstage/core-components": "^0.9.5",
"@backstage/core-plugin-api": "^1.0.3",
"@backstage/integration-react": "^1.1.1",
"@backstage/plugin-api-docs": "^0.8.6",
"@backstage/plugin-catalog": "^1.3.0",
"@backstage/plugin-catalog-common": "^1.0.3",
"@backstage/plugin-catalog-graph": "^0.2.18",
"@backstage/plugin-catalog-import": "^0.8.9",
"@backstage/plugin-catalog-react": "^1.1.1",
"@backstage/plugin-github-actions": "^0.5.6",
"@backstage/plugin-org": "^0.5.6",
"@backstage/plugin-permission-react": "^0.4.2",
"@backstage/plugin-scaffolder": "^1.3.0",
"@backstage/plugin-search": "^0.9.0",
"@backstage/plugin-search-react": "^0.2.1",
"@backstage/plugin-tech-radar": "^0.5.13",
"@backstage/plugin-techdocs": "^1.2.0",
"@backstage/plugin-techdocs-react": "^1.0.1",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.1",
"@backstage/plugin-user-settings": "^0.4.5",
"@backstage/theme": "^0.2.15",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/test-utils": "^1.1.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react-dom": "*",
"cross-env": "^7.0.0",
"cypress": "^9.7.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
"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",
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
"cy:dev": "cypress open",
"cy:run": "cypress run --browser chrome"
},
"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,79 @@
<!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 platform for building developer portals"
/>
<link rel="apple-touch-icon" href="<%= publicPath %>/logo192.png" />
<!--
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.getString('app.title') %></title>
<% if (config.has('app.googleAnalyticsTrackingId')) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=<%= config.getString('app.googleAnalyticsTrackingId') %>"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag(
'config',
'<%= config.getString("app.googleAnalyticsTrackingId") %>',
);
</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 `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `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,26 @@
import React from 'react';
import { renderWithEffects } from '@backstage/test-utils';
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 = await renderWithEffects(<App />);
expect(rendered.baseElement).toBeInTheDocument();
});
});

107
packages/app/src/App.tsx Normal file
View File

@@ -0,0 +1,107 @@
import React from 'react';
import { Navigate, Route } from 'react-router';
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/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 } from '@backstage/core-components';
import { createApp } from '@backstage/app-defaults';
import { FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { PermissionedRoute } 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,
});
bind(apiDocsPlugin.externalRoutes, {
registerApi: catalogImportPlugin.routes.importPage,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
});
},
});
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const routes = (
<FlatRoutes>
<Navigate key="/" 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} />}
/>
<PermissionedRoute
path="/catalog-import"
permission={catalogEntityCreatePermission}
element={<CatalogImportPage />}
/>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
</FlatRoutes>
);
const App = () => (
<AppProvider>
<AlertDisplay />
<OAuthRequestDialog />
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</AppProvider>
);
export default App;

19
packages/app/src/apis.ts Normal file
View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { 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,112 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { PropsWithChildren } from 'react';
import { Link, 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 { NavLink } from 'react-router-dom';
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,
} 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
component={NavLink}
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,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';

View File

@@ -0,0 +1,390 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { 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,
EntitySwitch,
EntityOrphanWarning,
EntityProcessingErrorsPanel,
isComponentType,
isKind,
hasCatalogProcessingErrors,
isOrphan,
} from '@backstage/plugin-catalog';
import {
isGithubActionsAvailable,
EntityGithubActionsContent,
} from '@backstage/plugin-github-actions';
import {
EntityUserProfileCard,
EntityGroupProfileCard,
EntityMembersListCard,
EntityOwnershipCard,
} 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';
const techdocsContent = (
<EntityTechdocsContent>
<TechDocsAddons>
<ReportIssue />
</TechDocsAddons>
</EntityTechdocsContent>
);
const cicdContent = (
// 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={isGithubActionsAvailable}>
<EntityGithubActionsContent />
</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={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={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="/ci-cd" title="CI/CD">
{cicdContent}
</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="/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>
);
/**
* 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}>
<EntityMembersListCard />
</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={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>
</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,156 @@
import React from 'react';
import { makeStyles, Theme, Grid, List, 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 {
DefaultResultListItem,
SearchBar,
SearchFilter,
SearchResult,
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}>
<SearchResult>
{({ results }) => (
<List>
{results.map(({ type, document, highlight, rank }) => {
switch (type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={document.location}
result={document}
highlight={highlight}
rank={rank}
/>
);
case 'techdocs':
return (
<TechDocsSearchResultListItem
key={document.location}
result={document}
highlight={highlight}
rank={rank}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
highlight={highlight}
rank={rank}
/>
);
}
})}
</List>
)}
</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';
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 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@@ -0,0 +1,34 @@
# This dockerfile builds an image for the backend package.
# It should be executed with the root of the repo as docker context.
#
# Before building this image, be sure to have run the following commands in the repo root:
#
# yarn install
# yarn tsc
# yarn build
#
# Once the commands have been run, you can build the image using `yarn build-image`
FROM node:16-bullseye-slim
WORKDIR /app
# install sqlite3 dependencies, you can skip this if you don't use sqlite3 in the image
RUN apt-get update && \
apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \
rm -rf /var/lib/apt/lists/* && \
yarn config set python /usr/bin/python3
# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
# The skeleton contains the package.json of each package in the monorepo,
# and along with yarn.lock and the root package.json, that's enough to run yarn install.
COPY yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)"
# Then copy the rest of the backend bundle, along with any other files we might want.
COPY packages/backend/dist/bundle.tar.gz app-config*.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]

View File

@@ -0,0 +1,66 @@
# 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
yarn tsc
yarn build
```
You should only need to do this once.
After that, go to the `packages/backend` directory and run
```bash
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
LOG_LEVEL=debug \
yarn start
```
Substitute `x` for actual values, or leave them as dummy values just to try out
the backend without using the auth or sentry features.
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/software-catalog-overview#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://github.com/backstage/backstage/blob/master/docs/README.md)

View File

@@ -0,0 +1,53 @@
{
"name": "backend",
"version": "0.0.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"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": {
"app": "link:../app",
"@backstage/backend-common": "^0.14.0",
"@backstage/backend-tasks": "^0.3.2",
"@backstage/catalog-model": "^1.0.3",
"@backstage/catalog-client": "^1.0.3",
"@backstage/config": "^1.0.1",
"@backstage/plugin-app-backend": "^0.3.33",
"@backstage/plugin-auth-backend": "^0.14.1",
"@backstage/plugin-catalog-backend": "^1.2.0",
"@backstage/plugin-permission-common": "^0.6.2",
"@backstage/plugin-permission-node": "^0.6.2",
"@backstage/plugin-proxy-backend": "^0.2.27",
"@backstage/plugin-scaffolder-backend": "^1.3.0",
"@backstage/plugin-search-backend": "^0.5.3",
"@backstage/plugin-search-backend-module-pg": "^0.3.4",
"@backstage/plugin-search-backend-node": "^0.6.2",
"@backstage/plugin-techdocs-backend": "^1.1.2",
"dockerode": "^3.3.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"pg": "^8.3.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.17.2",
"@types/dockerode": "^3.3.0",
"@types/express-serve-static-core": "^4.17.5",
"@types/express": "^4.17.6",
"@types/luxon": "^2.0.4",
"better-sqlite3": "^7.5.0"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,8 @@
import { PluginEnvironment } from './types';
describe('test', () => {
it('unbreaks the test runner', () => {
const unbreaker = {} as PluginEnvironment;
expect(unbreaker).toBeTruthy();
});
});

View File

@@ -0,0 +1,109 @@
/*
* Hi!
*
* Note that this is an EXAMPLE Backstage backend. Please check the README.
*
* Happy hacking!
*/
import Router from 'express-promise-router';
import {
createServiceBuilder,
loadBackendConfig,
getRootLogger,
useHotMemoize,
notFoundHandler,
CacheManager,
DatabaseManager,
SingleHostDiscovery,
UrlReaders,
ServerTokenManager,
} from '@backstage/backend-common';
import { TaskScheduler } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import app from './plugins/app';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import scaffolder from './plugins/scaffolder';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import search from './plugins/search';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
function makeCreateEnv(config: Config) {
const root = getRootLogger();
const reader = UrlReaders.default({ logger: root, config });
const discovery = SingleHostDiscovery.fromConfig(config);
const cacheManager = CacheManager.fromConfig(config);
const databaseManager = DatabaseManager.fromConfig(config);
const tokenManager = ServerTokenManager.noop();
const taskScheduler = TaskScheduler.fromConfig(config);
const permissions = ServerPermissionClient.fromConfig(config, {
discovery,
tokenManager,
});
root.info(`Created UrlReader ${reader}`);
return (plugin: string): PluginEnvironment => {
const logger = root.child({ type: 'plugin', plugin });
const database = databaseManager.forPlugin(plugin);
const cache = cacheManager.forPlugin(plugin);
const scheduler = taskScheduler.forPlugin(plugin);
return {
logger,
database,
cache,
config,
reader,
discovery,
tokenManager,
scheduler,
permissions,
};
};
}
async function main() {
const config = await loadBackendConfig({
argv: process.argv,
logger: getRootLogger(),
});
const createEnv = makeCreateEnv(config);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/search', await search(searchEnv));
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('/api', apiRouter)
.addRouter('', await app(appEnv));
await service.start().catch(err => {
console.log(err);
process.exit(1);
});
}
module.hot?.accept();
main().catch(error => {
console.error(`Backend failed to start up, ${error}`);
process.exit(1);
});

View File

@@ -0,0 +1,14 @@
import { createRouter } from '@backstage/plugin-app-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
appPackageName: 'app',
});
}

View File

@@ -0,0 +1,54 @@
import {
createRouter,
providers,
defaultAuthProviderFactories,
} from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
discovery: env.discovery,
tokenManager: env.tokenManager,
providerFactories: {
...defaultAuthProviderFactories,
// This replaces the default GitHub auth provider with a customized one.
// The `signIn` option enables sign-in for this provider, using the
// identity resolution logic that's provided in the `resolver` callback.
//
// This particular resolver makes all users share a single "guest" identity.
// It should only be used for testing and trying out Backstage.
//
// If you want to use a production ready resolver you can switch to the
// the one that is commented out below, it looks up a user entity in the
// catalog using the GitHub username of the authenticated user.
// That resolver requires you to have user entities populated in the catalog,
// for example using https://backstage.io/docs/integrations/github/org
//
// There are other resolvers to choose from, and you can also create
// your own, see the auth documentation for more details:
//
// https://backstage.io/docs/auth/identity-resolver
github: providers.github.create({
signIn: {
resolver(_, ctx) {
const userRef = 'user:default/guest'; // Must be a full entity reference
return ctx.issueToken({
claims: {
sub: userRef, // The user's own identity
ent: [userRef], // A list of identities that the user claims ownership through
},
});
},
// resolver: providers.github.resolvers.usernameMatchingUserEntityName(),
},
}),
},
});
}

View File

@@ -0,0 +1,14 @@
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
const { processingEngine, router } = await builder.build();
await processingEngine.start();
return router;
}

View File

@@ -0,0 +1,13 @@
import { createRouter } from '@backstage/plugin-proxy-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
discovery: env.discovery,
});
}

View File

@@ -0,0 +1,20 @@
import { CatalogClient } from '@backstage/catalog-client';
import { createRouter } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogClient = new CatalogClient({
discoveryApi: env.discovery,
});
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
reader: env.reader,
catalogClient,
});
}

View File

@@ -0,0 +1,66 @@
import { useHotCleanup } from '@backstage/backend-common';
import { createRouter } from '@backstage/plugin-search-backend';
import {
IndexBuilder,
LunrSearchEngine,
} from '@backstage/plugin-search-backend-node';
import { PluginEnvironment } from '../types';
import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
import { Router } from 'express';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// Initialize a connection to a search engine.
const searchEngine = new LunrSearchEngine({
logger: env.logger,
});
const indexBuilder = new IndexBuilder({
logger: env.logger,
searchEngine,
});
const schedule = env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 10 },
timeout: { minutes: 15 },
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
initialDelay: { seconds: 3 },
});
// Collators are responsible for gathering documents known to plugins. This
// collator gathers entities from the software catalog.
indexBuilder.addCollator({
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
// collator gathers entities from techdocs.
indexBuilder.addCollator({
schedule,
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
logger: env.logger,
tokenManager: env.tokenManager,
}),
});
// The scheduler controls when documents are gathered from collators and sent
// to the search engine for indexing.
const { scheduler } = await indexBuilder.build();
scheduler.start();
useHotCleanup(module, () => scheduler.stop());
return await createRouter({
engine: indexBuilder.getSearchEngine(),
types: indexBuilder.getDocumentTypes(),
permissions: env.permissions,
config: env.config,
logger: env.logger,
});
}

View File

@@ -0,0 +1,51 @@
import { DockerContainerRunner } from '@backstage/backend-common';
import {
createRouter,
Generators,
Preparers,
Publisher,
} from '@backstage/plugin-techdocs-backend';
import Docker from 'dockerode';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// Preparers are responsible for fetching source files for documentation.
const preparers = await Preparers.fromConfig(env.config, {
logger: env.logger,
reader: env.reader,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(env.config, {
logger: env.logger,
containerRunner,
});
// Publisher is used for
// 1. Publishing generated files to storage
// 2. Fetching files from storage and passing them to TechDocs frontend.
const publisher = await Publisher.fromConfig(env.config, {
logger: env.logger,
discovery: env.discovery,
});
// checks if the publisher is working and logs the result
await publisher.getReadiness();
return await createRouter({
preparers,
generators,
publisher,
logger: env.logger,
config: env.config,
discovery: env.discovery,
cache: env.cache,
});
}

View File

@@ -0,0 +1,23 @@
import { Logger } from 'winston';
import { Config } from '@backstage/config';
import {
PluginCacheManager,
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
UrlReader,
} from '@backstage/backend-common';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
export type PluginEnvironment = {
logger: Logger;
database: PluginDatabaseManager;
cache: PluginCacheManager;
config: Config;
reader: UrlReader;
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
scheduler: PluginTaskScheduler;
permissions: PermissionEvaluator;
};