New plugin workspace: manage (#2569)

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
Gustaf Räntilä 2025-02-24 21:27:07 +01:00 committed by GitHub
parent a8e2f2cad0
commit 78f2046a4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
167 changed files with 42724 additions and 0 deletions

1
.github/CODEOWNERS vendored
View File

@ -37,6 +37,7 @@ yarn.lock @backstage/community-plugins
/workspaces/keycloak @backstage/community-plugins-maintainers @AndrienkoAleksandr @schultzp2020 @dzemanov
/workspaces/kiali @backstage/community-plugins-maintainers @aljesusg @josunect @leandroberetta
/workspaces/linguist @backstage/community-plugins-maintainers @awanlin
/workspaces/manage @backstage/community-plugins-maintainers @grantila
/workspaces/matomo @backstage/community-plugins-maintainers @yashoswalyo @deshmukhmayur @riginoommen
/workspaces/mend @backstage/community-plugins-maintainers @dariuszsobkowicz
/workspaces/mta @backstage/community-plugins-maintainers @ibolton336

View File

@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@ -0,0 +1,14 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"privatePackages": {
"tag": false,
"version": false
}
}

View File

@ -0,0 +1,8 @@
.git
.yarn/cache
.yarn/install-state.gz
node_modules
packages/*/src
packages/*/node_modules
plugins
*.local.yaml

View File

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2

View File

@ -0,0 +1 @@
playwright.config.ts

View File

@ -0,0 +1 @@
module.exports = require('../../.eslintrc.cjs');

54
workspaces/manage/.gitignore vendored Normal file
View File

@ -0,0 +1,54 @@
# macOS
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Coverage directory generated when running tests with coverage
coverage
# Dependencies
node_modules/
# Yarn 3 files
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Node version directives
.nvmrc
# dotenv environment variables file
.env
.env.test
# Build output
dist
dist-types
# Temporary change files created by Vim
*.swp
# MkDocs build output
site
# Local configuration files
*.local.yaml
# Sensitive credentials
*-credentials.yaml
# vscode database functionality support files
*.session.sql
# E2E test reports
e2e-test-report/

View File

@ -0,0 +1,5 @@
dist
dist-types
coverage
.vscode
.eslintrc.js

View File

@ -0,0 +1,22 @@
# Manage page
This plugin offers a place for developers to manage things they and their team own.
Read the [documentation](./plugins/manage/README.md) for the frontend plugin.
![Components tab](./plugins/manage/docs/components.png)
## Packages
- [manage](./plugins/manage/README.md) - The frontend plugin for the Manage page
- [manage-module-tech-insights](./plugins/manage-module-tech-insights/README.md) - The tech insights module for the Manage plugin, to display tech insight checks in the entity tables, and/or display aggregated gauges.
- [manage-react](./plugins/manage-react/README.md) - A module for extending the Manage page with custom components, features, entity table columns, etc.
## Local Development
To start the Backstage App, run:
```sh
yarn install
yarn dev
```

View File

@ -0,0 +1,84 @@
app:
title: Tech insights Example App
baseUrl: http://localhost:3000
organization:
name: Tech insights Example
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
csp:
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
database:
client: better-sqlite3
connection: ':memory:'
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
techdocs:
builder: 'local'
generator:
runIn: 'local'
publisher:
type: 'local'
auth:
providers:
guest:
userEntityRef: user:default/guest
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
target: ../../examples/entities.yaml
# Local example organizational data
- type: file
target: ../../examples/org.yaml
rules:
- allow: [User, Group]
techInsights:
factRetrievers:
entityOwnershipFactRetriever:
cadence: '*/1 * * * *'
lifecycle: { timeToLive: { weeks: 2 } }
entityMetadataFactRetriever:
cadence: '*/1 * * * *'
lifecycle: { timeToLive: { weeks: 2 } }
techdocsFactRetriever:
cadence: '*/1 * * * *'
lifecycle: { timeToLive: { weeks: 2 } }
apiDefinitionFactRetriever:
cadence: '*/1 * * * *'
lifecycle: { timeToLive: { weeks: 2 } }
# See packages/backend/src/index.ts for programmatically registration
# factChecker:
# checks:
# groupOwnerCheck:
# type: json-rules-engine
# name: Group Owner Check
# description: Verifies that a group has been set as the spec.owner for this entity
# factIds:
# - entityOwnershipFactRetriever
# rule:
# conditions:
# all:
# - fact: hasGroupOwner
# operator: equal
# value: true

View File

@ -0,0 +1,3 @@
{
"version": "1.35.0"
}

View File

@ -0,0 +1,13 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: manage
description: An example of a Backstage application.
# Example for optional annotations
# annotations:
# github.com/project-slug: backstage/backstage
# backstage.io/techdocs-ref: dir:.
spec:
type: website
owner: john@example.com
lifecycle: experimental

View File

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

View File

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

View File

@ -0,0 +1,64 @@
{
"name": "@internal/manage",
"version": "1.0.0",
"private": true,
"engines": {
"node": "18 || 20"
},
"scripts": {
"dev": "yarn workspaces foreach -A --include backend --include app --parallel --jobs unlimited -v -i run start",
"start": "yarn workspace app start",
"start-backend": "yarn workspace backend start",
"build:backend": "yarn workspace backend build",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"build:all": "backstage-cli repo build --all",
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"fix": "backstage-cli repo fix",
"lint": "backstage-cli repo lint --since origin/main",
"lint:all": "backstage-cli repo lint",
"prettier:check": "prettier --check .",
"prettier:write": "prettier --write .",
"new": "backstage-cli new --scope @backstage-community",
"build:api-reports": "yarn build:api-reports:only --tsc",
"build:api-reports:only": "backstage-repo-tools api-reports -o ae-wrong-input-file-type,ae-undocumented --validate-release-tags",
"postinstall": "cd ../../ && yarn install"
},
"workspaces": {
"packages": [
"packages/*",
"plugins/*"
]
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/manage"
},
"devDependencies": {
"@backstage/cli": "^0.29.5",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/repo-tools": "^0.12.1",
"@changesets/cli": "^2.27.1",
"knip": "^5.27.4",
"node-gyp": "^11.0.0",
"prettier": "^2.3.2",
"typescript": "~5.3.0"
},
"resolutions": {
"@types/react": "^18",
"@types/react-dom": "^18"
},
"prettier": "@backstage/cli/config/prettier",
"lint-staged": {
"*.{js,jsx,ts,tsx,mjs,cjs}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}

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 @@
public

View File

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

View File

@ -0,0 +1,82 @@
# app
## 0.0.11
### Patch Changes
- Updated dependencies [7a14237]
- @backstage-community/plugin-tech-insights@0.3.39
## 0.0.10
### Patch Changes
- Updated dependencies [caa9401]
- @backstage-community/plugin-tech-insights@0.3.38
## 0.0.9
### Patch Changes
- Updated dependencies [e516773]
- Updated dependencies [e516773]
- @backstage-community/plugin-tech-insights@0.3.37
## 0.0.8
### Patch Changes
- Updated dependencies [42a2c31]
- @backstage-community/plugin-tech-insights@0.3.36
## 0.0.7
### Patch Changes
- Updated dependencies [1d33996]
- @backstage-community/plugin-tech-insights@0.3.35
## 0.0.6
### Patch Changes
- Updated dependencies [a84eb44]
- @backstage-community/plugin-tech-insights@0.3.34
## 0.0.5
### Patch Changes
- Updated dependencies [00d148d]
- @backstage-community/plugin-tech-insights@0.3.33
## 0.0.4
### Patch Changes
- Updated dependencies [83a5e80]
- @backstage-community/plugin-tech-insights@0.3.32
## 0.0.3
### Patch Changes
- Updated dependencies [0265767]
- @backstage-community/plugin-tech-insights@0.3.31
## 0.0.2
### Patch Changes
- Updated dependencies [7ac338c]
- Updated dependencies [794cc8b]
- Updated dependencies [a8d8d44]
- @backstage-community/plugin-tech-insights@0.3.30
## 0.0.1
### Patch Changes
- Updated dependencies [cbad35a]
- Updated dependencies [cbad35a]
- @backstage-community/plugin-tech-insights@0.3.29

View File

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from '@playwright/test';
test('App should render the welcome page', async ({ page }) => {
await page.goto('/');
const enterButton = page.getByRole('button', { name: 'Enter' });
await expect(enterButton).toBeVisible();
await enterButton.click();
await expect(page.getByText('My Company Catalog')).toBeVisible();
});

View File

@ -0,0 +1,19 @@
# Knip report
## Unused dependencies (4)
| Name | Location | Severity |
| :---------------------------------------- | :----------- | :------- |
| @backstage-community/plugin-tech-insights | package.json | error |
| react-router | package.json | error |
| react-use | package.json | error |
| history | package.json | error |
## Unused devDependencies (4)
| Name | Location | Severity |
| :-------------------------- | :----------- | :------- |
| @testing-library/user-event | package.json | error |
| @backstage/test-utils | package.json | error |
| @testing-library/dom | package.json | error |
| cross-env | package.json | error |

View File

@ -0,0 +1,85 @@
{
"name": "app",
"version": "0.0.11",
"private": true,
"bundled": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/manage/packages/app"
},
"backstage": {
"role": "frontend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"test": "backstage-cli package test",
"lint": "backstage-cli package lint"
},
"dependencies": {
"@backstage-community/plugin-github-actions": "^0.6.16",
"@backstage-community/plugin-manage": "workspace:^",
"@backstage-community/plugin-manage-module-tech-insights": "workspace:^",
"@backstage-community/plugin-manage-react": "workspace:^",
"@backstage-community/plugin-tech-insights": "^0.4.0",
"@backstage-community/plugin-tech-radar": "^0.7.4",
"@backstage/app-defaults": "^1.5.16",
"@backstage/catalog-model": "^1.7.3",
"@backstage/cli": "^0.29.5",
"@backstage/core-app-api": "^1.15.4",
"@backstage/core-components": "^0.16.3",
"@backstage/core-plugin-api": "^1.10.3",
"@backstage/integration-react": "^1.2.3",
"@backstage/plugin-api-docs": "^0.12.3",
"@backstage/plugin-catalog": "^1.26.1",
"@backstage/plugin-catalog-common": "^1.1.3",
"@backstage/plugin-catalog-graph": "^0.4.15",
"@backstage/plugin-catalog-import": "^0.12.9",
"@backstage/plugin-catalog-react": "^1.15.1",
"@backstage/plugin-org": "^0.6.35",
"@backstage/plugin-permission-react": "^0.4.30",
"@backstage/plugin-scaffolder": "^1.27.4",
"@backstage/plugin-search": "^1.4.22",
"@backstage/plugin-search-react": "^1.8.5",
"@backstage/plugin-techdocs": "^1.12.1",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.20",
"@backstage/plugin-techdocs-react": "^1.2.13",
"@backstage/plugin-user-settings": "^0.8.18",
"@backstage/theme": "^0.6.3",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/test-utils": "^1.7.4",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/react-dom": "*",
"cross-env": "^7.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"files": [
"dist"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

@ -0,0 +1,44 @@
/*
* 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 {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
} from '@backstage/core-plugin-api';
import {
createManageTechInsightsApiFactory,
manageTechInsightsApiRef,
} from '@backstage-community/plugin-manage-module-tech-insights';
import { createManageApiFactory } from '@backstage-community/plugin-manage-react';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
createManageTechInsightsApiFactory(),
createManageApiFactory({
kindOrder: ['component', 'api', 'template', 'system'],
extensions: [manageTechInsightsApiRef],
}),
];

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,46 @@
/*
* 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 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,130 @@
/*
* 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 React, { PropsWithChildren } from 'react';
import { makeStyles } from '@material-ui/core';
import CategoryIcon from '@material-ui/icons/Category';
import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
import MapIcon from '@material-ui/icons/MyLocation';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Settings as SidebarSettings,
UserSettingsSignInAvatar,
} from '@backstage/plugin-user-settings';
import { SidebarSearchModal } from '@backstage/plugin-search';
import {
Sidebar,
sidebarConfig,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebarOpenState,
Link,
} from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import EmojiObjectsIcon from '@material-ui/icons/EmojiObjects';
import ManageIcon from '@material-ui/icons/Ballot';
import { managePlugin } from '@backstage-community/plugin-manage';
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<{}>) => {
const managePage = useRouteRef(managePlugin.routes.root);
return (
<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={ManageIcon}
to={managePage()}
text="Manage"
data-tour="manage"
/>
<SidebarItem icon={CategoryIcon} to="catalog" text="Catalog" />
<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" />
<SidebarItem
icon={EmojiObjectsIcon}
to="tech-insights"
text="Tech insight"
/>
</SidebarScrollWrapper>
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
label="Settings"
icon={<UserSettingsSignInAvatar />}
to="/settings"
>
<SidebarSettings />
</SidebarGroup>
</Sidebar>
{children}
</SidebarPage>
);
};

View File

@ -0,0 +1,16 @@
/*
* 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.
*/
export { Root } from './Root';

View File

@ -0,0 +1,414 @@
/*
* 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 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,
hasRelationWarnings,
EntityRelationWarning,
} from '@backstage/plugin-catalog';
import {
isGithubActionsAvailable,
EntityGithubActionsContent,
} from '@backstage-community/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';
import { EntityTechInsightsScorecardCard } from '@backstage-community/plugin-tech-insights';
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={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
<EntityProcessingErrorsPanel />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
</>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={6} xs={12}>
<EntityTechInsightsScorecardCard
title="Customized title for the scorecard"
description="Small description about scorecards"
/>
</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} md={6}>
<EntityMembersListCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityLinksCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
const systemPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<Grid item md={8}>
<EntityHasComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
<EntityCatalogGraphCard
variant="gridItem"
direction={Direction.TOP_BOTTOM}
title="System Diagram"
height={700}
relations={[
RELATION_PART_OF,
RELATION_HAS_PART,
RELATION_API_CONSUMED_BY,
RELATION_API_PROVIDED_BY,
RELATION_CONSUMES_API,
RELATION_PROVIDES_API,
RELATION_DEPENDENCY_OF,
RELATION_DEPENDS_ON,
]}
unidirectional={false}
/>
</EntityLayout.Route>
</EntityLayout>
);
const domainPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);
export const entityPage = (
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
<EntitySwitch.Case if={isKind('user')} children={userPage} />
<EntitySwitch.Case if={isKind('system')} children={systemPage} />
<EntitySwitch.Case if={isKind('domain')} children={domainPage} />
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
);

View File

@ -0,0 +1,58 @@
/*
* 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 React from 'react';
import {
MANAGE_KIND_COMMON,
ManageTabs,
OrganizationGraph,
} from '@backstage-community/plugin-manage';
import {
manageTechInsightsColumns,
ManageTechInsightsCards,
ManageTechInsightsGrid,
} from '@backstage-community/plugin-manage-module-tech-insights';
export function Manage() {
return (
<ManageTabs
combined={{
header: <ManageTechInsightsCards inAccordion />,
columns: [manageTechInsightsColumns({ combined: true })],
}}
starred={{
header: <ManageTechInsightsGrid />,
columns: [manageTechInsightsColumns({ combined: true })],
}}
kinds={{
[MANAGE_KIND_COMMON]: {
header: <ManageTechInsightsCards inAccordion />,
columns: [manageTechInsightsColumns()],
},
component: {
columns: [manageTechInsightsColumns({ combined: true })],
},
}}
tabsAfter={[
{
path: 'organization',
title: 'Organization',
children: <OrganizationGraph />,
},
]}
/>
);
}

View File

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

View File

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

View File

@ -0,0 +1,16 @@
/*
* 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 '@testing-library/jest-dom';

View File

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

View File

@ -0,0 +1,107 @@
# backend
## 0.0.11
### Patch Changes
- Updated dependencies [7a14237]
- @backstage-community/plugin-tech-insights-backend@1.2.2
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.59
- @backstage-community/plugin-tech-insights-node@1.0.2
- app@0.0.11
## 0.0.10
### Patch Changes
- Updated dependencies [6a4787a]
- @backstage-community/plugin-tech-insights-backend@1.2.1
## 0.0.9
### Patch Changes
- Updated dependencies [0f5c58a]
- Updated dependencies [e516773]
- Updated dependencies [e516773]
- @backstage-community/plugin-tech-insights-backend@1.2.0
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.58
- @backstage-community/plugin-tech-insights-node@1.0.1
- app@0.0.9
## 0.0.8
### Patch Changes
- Updated dependencies [fad299b]
- @backstage-community/plugin-tech-insights-backend@1.1.0
- app@0.0.8
## 0.0.7
### Patch Changes
- Updated dependencies [9871d0b]
- Updated dependencies [9871d0b]
- @backstage-community/plugin-tech-insights-node@1.0.0
- @backstage-community/plugin-tech-insights-backend@1.0.0
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.57
## 0.0.6
### Patch Changes
- Updated dependencies [1d33996]
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.56
- @backstage-community/plugin-tech-insights-node@0.6.7
- app@0.0.7
- @backstage-community/plugin-tech-insights-backend@0.6.3
## 0.0.5
### Patch Changes
- @backstage-community/plugin-tech-insights-backend@0.6.2
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.55
- @backstage-community/plugin-tech-insights-node@0.6.6
- app@0.0.6
## 0.0.4
### Patch Changes
- Updated dependencies [ae2ee8a]
- Updated dependencies [00d148d]
- @backstage-community/plugin-tech-insights-backend@0.6.1
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.54
- @backstage-community/plugin-tech-insights-node@0.6.5
- app@0.0.5
## 0.0.3
### Patch Changes
- Updated dependencies [e49b4eb]
- @backstage-community/plugin-tech-insights-backend@0.6.0
- app@0.0.3
## 0.0.2
### Patch Changes
- Updated dependencies [7ac338c]
- Updated dependencies [a8d8d44]
- Updated dependencies [794cc8b]
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.53
- @backstage-community/plugin-tech-insights-backend@0.5.35
- @backstage-community/plugin-tech-insights-node@0.6.4
- app@0.0.2
## 0.0.1
### Patch Changes
- Updated dependencies [cbad35a]
- @backstage-community/plugin-tech-insights-backend-module-jsonfc@0.1.52
- @backstage-community/plugin-tech-insights-backend@0.5.34
- app@0.0.1

View File

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

View File

@ -0,0 +1,31 @@
# Knip report
## Unused dependencies (17)
| Name | Location | Severity |
| :-------------------------------------------------------------- | :----------- | :------- |
| @backstage-community/plugin-tech-insights-backend-module-jsonfc | package.json | error |
| @backstage/plugin-auth-backend-module-github-provider | package.json | error |
| @backstage-community/plugin-tech-insights-backend | package.json | error |
| @backstage-community/plugin-tech-insights-node | package.json | error |
| @backstage/plugin-search-backend-node | package.json | error |
| @backstage/plugin-permission-common | package.json | error |
| @backstage/plugin-permission-node | package.json | error |
| @backstage/plugin-auth-node | package.json | error |
| @backstage/backend-common | package.json | error |
| @backstage/config | package.json | error |
| better-sqlite3 | package.json | error |
| dockerode | package.json | error |
| node-gyp | package.json | error |
| winston | package.json | error |
| app | package.json | error |
| pg | package.json | error |
## Unused devDependencies (4)
| Name | Location | Severity |
| :------------------------------- | :----------- | :------- |
| @types/express-serve-static-core | package.json | error |
| @types/dockerode | package.json | error |
| @types/express | package.json | error |
| @types/luxon | package.json | error |

View File

@ -0,0 +1,66 @@
{
"name": "backend",
"version": "0.0.11",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/manage/packages/backend"
},
"backstage": {
"role": "backend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"build-image": "docker build ../.. -f Dockerfile --tag backstage"
},
"dependencies": {
"@backstage-community/plugin-tech-insights-backend": "^2.1.1",
"@backstage-community/plugin-tech-insights-backend-module-jsonfc": "^0.3.1",
"@backstage-community/plugin-tech-insights-common": "^0.4.0",
"@backstage-community/plugin-tech-insights-node": "^2.2.0",
"@backstage/backend-defaults": "^0.6.2",
"@backstage/backend-plugin-api": "^1.1.1",
"@backstage/catalog-client": "^1.9.1",
"@backstage/catalog-model": "^1.7.3",
"@backstage/config": "^1.3.2",
"@backstage/plugin-app-backend": "^0.4.4",
"@backstage/plugin-auth-backend": "^0.24.2",
"@backstage/plugin-auth-backend-module-github-provider": "^0.2.4",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.4",
"@backstage/plugin-auth-node": "^0.5.6",
"@backstage/plugin-catalog-backend": "^1.30.0",
"@backstage/plugin-permission-backend": "^0.5.53",
"@backstage/plugin-permission-backend-module-allow-all-policy": "^0.2.4",
"@backstage/plugin-permission-common": "^0.8.4",
"@backstage/plugin-permission-node": "^0.8.7",
"@backstage/plugin-proxy-backend": "^0.5.10",
"@backstage/plugin-search-backend": "^1.8.0",
"@backstage/plugin-search-backend-module-catalog": "^0.3.0",
"@backstage/plugin-search-backend-module-techdocs": "^0.3.5",
"@backstage/plugin-search-backend-node": "^1.3.7",
"@backstage/plugin-techdocs-backend": "^1.11.5",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"dockerode": "^3.3.1",
"node-gyp": "^11.0.0",
"pg": "^8.11.3",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.29.5",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,49 @@
/*
* 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 { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-proxy-backend'));
backend.add(import('@backstage/plugin-techdocs-backend'));
// auth plugin
backend.add(import('@backstage/plugin-auth-backend'));
// See https://backstage.io/docs/backend-system/building-backends/migrating#the-auth-plugin
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
// See https://backstage.io/docs/auth/guest/provider
// catalog plugin
backend.add(import('@backstage/plugin-catalog-backend'));
// permission plugin
backend.add(import('@backstage/plugin-permission-backend'));
backend.add(
import('@backstage/plugin-permission-backend-module-allow-all-policy'),
);
// search plugin
backend.add(import('@backstage/plugin-search-backend'));
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));
// tech insights
backend.add(import('@backstage-community/plugin-tech-insights-backend'));
backend.add(import('./plugins/tech-insights'));
backend.start();

View File

@ -0,0 +1,206 @@
/*
* 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 {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { CatalogClient } from '@backstage/catalog-client';
import { Entity, ApiEntityV1alpha1 } from '@backstage/catalog-model';
import {
JSON_RULE_ENGINE_CHECK_TYPE,
JsonRulesEngineFactCheckerFactory,
} from '@backstage-community/plugin-tech-insights-backend-module-jsonfc';
import {
FactRetriever,
FactRetrieverContext,
techInsightsFactCheckerFactoryExtensionPoint,
techInsightsFactRetrieversExtensionPoint,
} from '@backstage-community/plugin-tech-insights-node';
import {
entityMetadataFactRetriever,
techdocsFactRetriever,
} from '@backstage-community/plugin-tech-insights-backend';
export const checks = [
{
id: 'groupOwnerCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
name: 'Group Owner Check',
description:
'Verifies that a Group has been set as the owner for this entity',
factIds: ['entityOwnershipFactRetriever'],
rule: {
conditions: {
all: [
{
fact: 'hasGroupOwner',
operator: 'equal',
value: true,
},
],
},
},
},
{
id: 'titleCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
name: 'Title Check',
description: 'Verifies that an entity has a title',
factIds: ['entityMetadataFactRetriever'],
rule: {
conditions: {
all: [
{
fact: 'hasTitle',
operator: 'equal',
value: true,
},
],
},
},
},
{
id: 'descriptionCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
name: 'Description Check',
description: 'Verifies that an entity has a description',
factIds: ['entityMetadataFactRetriever'],
rule: {
conditions: {
all: [
{
fact: 'hasDescription',
operator: 'equal',
value: true,
},
],
},
},
},
{
id: 'apiDefinitionCheck',
type: JSON_RULE_ENGINE_CHECK_TYPE,
name: 'API definition Check',
description: 'Verifies that a API has a definition set',
factIds: ['apiDefinitionFactRetriever'],
rule: {
conditions: {
all: [
{
fact: 'hasDefinition',
operator: 'equal',
value: true,
},
],
},
},
},
];
export const apiDefinitionFactRetriever: FactRetriever = {
id: 'apiDefinitionFactRetriever',
version: '0.0.1',
title: 'API Definition',
description: 'Generates facts which indicate the completeness of API spec',
schema: {
hasDefinition: {
type: 'boolean',
description: 'The entity has a definition in spec',
},
},
handler: async ({ discovery, auth }: FactRetrieverContext) => {
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'catalog',
});
const catalogClient = new CatalogClient({
discoveryApi: discovery,
});
const entities = await catalogClient.getEntities(
{ filter: { kind: ['API'] } },
{ token },
);
return entities.items.map((entity: Entity) => {
return {
entity: {
namespace: entity.metadata.namespace!,
kind: entity.kind,
name: entity.metadata.name,
},
facts: {
hasDefinition:
(entity as ApiEntityV1alpha1).spec?.definition &&
(entity as ApiEntityV1alpha1).spec?.definition.length > 0,
},
};
});
},
};
export default createBackendModule({
pluginId: 'tech-insights',
moduleId: 'generic-fact-retrievers',
register(reg) {
reg.registerInit({
deps: {
logger: coreServices.logger,
techInsightsFactCheckerFactory:
techInsightsFactCheckerFactoryExtensionPoint,
techInsightsFactRetrievers: techInsightsFactRetrieversExtensionPoint,
},
async init({
logger,
techInsightsFactCheckerFactory,
techInsightsFactRetrievers,
}) {
techInsightsFactCheckerFactory.setFactCheckerFactory(
new JsonRulesEngineFactCheckerFactory({
logger,
checks,
}),
);
techInsightsFactRetrievers.addFactRetrievers({
apiDefinitionFactRetriever,
entityMetadataFactRetriever: {
...entityMetadataFactRetriever,
entityFilter: [
{
kind: [
'location',
'domain',
'system',
'component',
'api',
'resource',
'template',
],
},
],
},
techdocsFactRetriever: {
...techdocsFactRetriever,
entityFilter: [
{ kind: ['component', 'system', 'api', 'template'] },
],
},
});
},
});
},
});

View File

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

View File

@ -0,0 +1,57 @@
# Manage page extension: tech-insights
To add columns to the table of owned entities, showing tech insight checks, use the `manageTechInsightsColumns` function. To show gauges (e.g. above the tables), use `ManageTechInsightsCards` or `ManageTechInsightsGrid`.
First, ensure to install the tech-insight module by adding its API ref to `createManageApiFactory`:
```ts
import {
createManageTechInsightsApiFactory,
manageTechInsightsApiRef,
} from '@backstage-community/plugin-manage-module-tech-insights';
[
// ...,
createManageTechInsightsApiFactory(),
createManageApiFactory({
extensions: [manageTechInsightsApiRef],
}),
];
```
In `Manage.tsx` (as described in the README for `@backstage-community/plugin-manage`):
```tsx
import {
manageTechInsightsColumns,
ManageTechInsightsCards,
ManageTechInsightsGrid,
} from '@backstage-community/plugin-manage-module-tech-insights';
export function Manage() {
return (
<ManageTabs
combined={{
header: <ManageTechInsightsGrid inAccordion />,
columns: [manageTechInsightsColumns({ combined: true })],
}}
starred={{
header: <ManageTechInsightsGrid inAccordion />,
columns: [manageTechInsightsColumns({ combined: true })],
}}
kinds={{
[MANAGE_KIND_COMMON]: {
header: <ManageTechInsightsCards inAccordion />,
columns: [manageTechInsightsColumns()],
},
component: {
// There are maybe too many tech-insights checks for components to
// show one per column.
// The `combined` option squeezes them into one column.
columns: [manageTechInsightsColumns({ combined: true })],
},
}}
/>
);
}
```

View File

@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-manage-module-tech-insights
title: '@backstage/plugin-manage-module-tech-insights'
description: Manage page, Tech Insights module
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: maintainers

View File

@ -0,0 +1,68 @@
{
"name": "@backstage-community/plugin-manage-module-tech-insights",
"description": "Manage plugin - Tech Insights module",
"version": "0.1.0",
"backstage": {
"role": "frontend-plugin-module",
"pluginId": "manage",
"pluginPackages": [
"@backstage-community/plugin-manage-module-tech-insights"
],
"pluginPackage": "@backstage-community/plugin-manage"
},
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/manage-module-tech-insights"
},
"keywords": [
"backstage",
"manage",
"page",
"tech-insights",
"tech",
"insights"
],
"sideEffects": false,
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage-community/plugin-manage-react": "workspace:^",
"@backstage-community/plugin-tech-insights": "^0.4.0",
"@backstage-community/plugin-tech-insights-common": "^0.4.0",
"@backstage/catalog-model": "^1.7.3",
"@backstage/core-components": "^0.16.3",
"@backstage/core-plugin-api": "^1.10.3",
"@backstage/plugin-catalog-react": "^1.15.1",
"@mui/material": "^5.15.16",
"@mui/styles": "^5",
"react-use": "^17.5.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.29.5",
"@types/react": "^18.3.12"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,135 @@
## API Report File for "@backstage-community/plugin-manage-module-tech-insights"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiFactory } from '@backstage/core-plugin-api';
import { ApiRef } from '@backstage/core-plugin-api';
import type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import { GaugeCardProps } from '@backstage-community/plugin-manage-react';
import { GridOwnProps } from '@mui/material/Grid';
import type { ManageColumnModule } from '@backstage-community/plugin-manage-react';
import type { ManageModuleApi } from '@backstage-community/plugin-manage-react';
import type { ProgressColor } from '@backstage-community/plugin-manage-react';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import type { ReactNode } from 'react';
// @public
export function createManageTechInsightsApiFactory(
options?: DefaultManageApiOptions,
): ApiFactory<
ManageTechInsights,
DefaultManageTechInsightsApi,
{
[x: string]: unknown;
}
>;
// @public
export interface DefaultManageApiOptions {
checkFilter?: (check: Check) => boolean;
getPercentColor?: (percent: number) => ProgressColor;
mapTitle?: ManageTechInsightsMapTitle;
}
// @public
export class DefaultManageTechInsightsApi implements ManageTechInsights {
constructor(options?: DefaultManageApiOptions);
// (undocumented)
readonly checkFilter: (check: Check) => boolean;
// (undocumented)
readonly getPercentColor: (percent: number) => ProgressColor;
// (undocumented)
getProvider: () => typeof ManageProviderTechInsights;
// (undocumented)
readonly mapTitle: ManageTechInsightsMapTitle;
}
// @public (undocumented)
export function ManageProviderTechInsights(
props: PropsWithChildren<{}>,
): React_2.JSX.Element;
// @public
export interface ManageTechInsights extends ManageModuleApi {
checkFilter: (check: Check) => boolean;
getPercentColor: (percent: number) => ProgressColor;
mapTitle: ManageTechInsightsMapTitle;
}
// @public
export const manageTechInsightsApiRef: ApiRef<ManageTechInsights>;
// @public
export function ManageTechInsightsCards(
props: ManageTechInsightsCardsProps,
): React_2.JSX.Element;
// @public
export interface ManageTechInsightsCardsProps {
// (undocumented)
containerProps?: Pick<
GridOwnProps,
| 'classes'
| 'columns'
| 'columnSpacing'
| 'direction'
| 'rowSpacing'
| 'spacing'
| 'sx'
| 'wrap'
| 'zeroMinWidth'
>;
// (undocumented)
gaugeCardProps?: GaugeCardProps;
inAccordion?: boolean;
mapTitle?: ManageTechInsightsMapTitle;
}
// @public
export function manageTechInsightsColumns(
options?: ManageTechInsightsOptions,
): ManageColumnModule;
// @public
export function ManageTechInsightsGrid(
props: ManageTechInsightsGridProps,
): React_2.JSX.Element;
// @public
export interface ManageTechInsightsGridProps {
inAccordion?: boolean;
mapTitle?: ManageTechInsightsMapTitle;
}
// @public (undocumented)
export type ManageTechInsightsMapTitle = (
check: Check,
) => ManageTechInsightsTitle;
// @public
export interface ManageTechInsightsOptions {
checkFilter?: (check: Check) => boolean;
combined?: boolean;
showEmpty?: boolean;
}
// @public (undocumented)
export type ManageTechInsightsTitle =
| ManageTechInsightsTitleAsObject
| ManageTechInsightsTitleAsElement;
// @public (undocumented)
export type ManageTechInsightsTitleAsElement = {
content: ReactNode;
};
// @public (undocumented)
export type ManageTechInsightsTitleAsObject = {
title: string;
tooltip?: ReactNode;
};
// (No @packageDocumentation comment for this package)
```

View File

@ -0,0 +1,77 @@
/*
* 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 type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import type { ProgressColor } from '@backstage-community/plugin-manage-react';
import type { ManageTechInsights } from './ManageTechInsights';
import { ManageProviderTechInsights } from '../components/ManageProvider';
import { ManageTechInsightsMapTitle } from '../title';
function defaultGetPercentColor(percent: number): ProgressColor {
if (percent >= 100) return 'success';
else if (percent > 50) return 'warning';
return 'error';
}
/**
* Options for the {@link DefaultManageTechInsightsApi}.
*
* @public
*/
export interface DefaultManageApiOptions {
/**
* Custom filter to only show certain checks.
*/
checkFilter?: (check: Check) => boolean;
/**
* Override the default colors for gauges.
*
* @param percent - number between 0 and 100
*/
getPercentColor?: (percent: number) => ProgressColor;
/**
* The default mapping of checks to titles for the
* {@link ManageTechInsightsCards} and {@link ManageTechInsightsGrid}
* components.
*/
mapTitle?: ManageTechInsightsMapTitle;
}
const defaultMapTitle: ManageTechInsightsMapTitle = check => ({
title: check.name,
tooltip: check.description,
});
/**
* Default implementation of the {@link ManageTechInsights} API.
*
* @public
*/
export class DefaultManageTechInsightsApi implements ManageTechInsights {
readonly checkFilter: (check: Check) => boolean;
readonly getPercentColor: (percent: number) => ProgressColor;
readonly mapTitle: ManageTechInsightsMapTitle;
public constructor(options: DefaultManageApiOptions = {}) {
this.checkFilter = options.checkFilter ?? (() => true);
this.getPercentColor = options.getPercentColor ?? defaultGetPercentColor;
this.mapTitle = options.mapTitle ?? defaultMapTitle;
}
getProvider = () => ManageProviderTechInsights;
}

View File

@ -0,0 +1,45 @@
/*
* 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 type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import type {
ManageModuleApi,
ProgressColor,
} from '@backstage-community/plugin-manage-react';
import { ManageTechInsightsMapTitle } from '../title';
/**
* ManageTechInsights API, which is a `ManageModuleApi` with additional
* features used in the manage-module-tech-insights plugin.
*
* @public
*/
export interface ManageTechInsights extends ManageModuleApi {
/**
* Custom filter to only show certain checks.
*/
checkFilter: (check: Check) => boolean;
/**
* Function to deduce what color to use of percentage gauges.
*/
getPercentColor: (percent: number) => ProgressColor;
/**
* The mapping of checks to titles for the `ManageTechInsightsCards` and
* `ManageTechInsightsGrid`
*/
mapTitle: ManageTechInsightsMapTitle;
}

View File

@ -0,0 +1,27 @@
/*
* 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 { createApiRef } from '@backstage/core-plugin-api';
import type { ManageTechInsights } from './ManageTechInsights';
/**
* ApiRef for the manage tech insights plugin.
*
* @public
*/
export const manageTechInsightsApiRef = createApiRef<ManageTechInsights>({
id: 'manage-tech-insights',
});

View File

@ -0,0 +1,37 @@
/*
* 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 { createApiFactory } from '@backstage/core-plugin-api';
import type { DefaultManageApiOptions } from './DefaultManageTechInsightsApi';
import { DefaultManageTechInsightsApi } from './DefaultManageTechInsightsApi';
import { manageTechInsightsApiRef } from './api';
/**
* Factory for creating the manage tech insights API.
*
* @public
*/
export function createManageTechInsightsApiFactory(
options?: DefaultManageApiOptions,
) {
return createApiFactory({
api: manageTechInsightsApiRef,
deps: {},
factory() {
return new DefaultManageTechInsightsApi(options);
},
});
}

View File

@ -0,0 +1,22 @@
/*
* 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.
*/
export type { ManageTechInsights } from './ManageTechInsights';
export type { DefaultManageApiOptions } from './DefaultManageTechInsightsApi';
export { DefaultManageTechInsightsApi } from './DefaultManageTechInsightsApi';
export { manageTechInsightsApiRef } from './api';
export { createManageTechInsightsApiFactory } from './impl';

View File

@ -0,0 +1,35 @@
/*
* 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 React from 'react';
import { makeStyles } from '@mui/styles';
import Tooltip from '@mui/material/Tooltip';
const useStyles = makeStyles((theme: any) => ({
root: {
paddingLeft: theme.spacing(2),
},
}));
export function NoData() {
const { root } = useStyles();
return (
<Tooltip title="No data">
<div className={root}></div>
</Tooltip>
);
}

View File

@ -0,0 +1,79 @@
/*
* 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 React, { useMemo } from 'react';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { TechInsightsCheckIcon } from '@backstage-community/plugin-tech-insights';
import type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import {
ColumnIconError,
type ManageColumn,
type GetColumnsFunc,
} from '@backstage-community/plugin-manage-react';
import { eqCheck } from '../utils';
import { useEntityInsights } from './hooks';
import { NoData } from './NoData';
export function makeGetColumns(
checkFilter: ((check: Check) => boolean) | undefined,
showEmpty: boolean,
): GetColumnsFunc {
return function useColumns(entities): ManageColumn[] {
const { responses, checks } = useEntityInsights(
entities,
checkFilter,
showEmpty,
);
return useMemo(
() =>
checks.map(
(check): ManageColumn => ({
id: `tech-insights-${check.id}`,
title: check.name,
render: ({ entity }) => {
const entityRef = stringifyEntityRef(entity);
const response = responses.get(entityRef);
if (!response) {
return <></>;
}
const foundCheck = response.find(res =>
eqCheck(res.check, check),
);
if (!foundCheck) {
return <NoData />;
}
return (
<TechInsightsCheckIcon
result={foundCheck}
entity={entity}
missingRendererComponent={
<ColumnIconError title="No renderer found for this check" />
}
/>
);
},
}),
),
[checks, responses],
);
};
}

View File

@ -0,0 +1,182 @@
/*
* 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 React, { useMemo } from 'react';
import Tooltip from '@mui/material/Tooltip';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { useApi } from '@backstage/core-plugin-api';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { TechInsightsCheckIcon } from '@backstage-community/plugin-tech-insights';
import type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import {
ColumnIconError,
ColumnIconPercent,
type ManageColumn,
type GetColumnFunc,
ProgressColor,
} from '@backstage-community/plugin-manage-react';
import { eqCheck } from '../utils';
import { manageTechInsightsApiRef } from '../api';
import { useEntityInsights, UseEntityInsightsResult } from './hooks';
import { NoData } from './NoData';
interface CombinedColumnProps {
entity: Entity;
useEntityInsightsResult: UseEntityInsightsResult;
getPercentColor: (percent: number) => ProgressColor;
}
function CombinedColumn(props: CombinedColumnProps) {
const {
entity,
useEntityInsightsResult: { responses, checks, renderers },
getPercentColor,
} = props;
const entityRef = stringifyEntityRef(entity);
const results = responses.get(entityRef);
if (!results) {
return <NoData />;
}
// Order the results the same way all the time
const validResults = checks
.flatMap(check =>
results
.filter(res => eqCheck(res.check, check))
.map(result => ({
result,
renderer: renderers.get(result.check.type),
})),
)
.filter((v): v is NonNullable<typeof v> => !!v);
const tooltipList = validResults
.map(({ result }) => {
return (
<ListItem disablePadding key={Math.random()}>
<ListItemIcon>
<TechInsightsCheckIcon
result={result}
entity={entity}
disableLinksMenu
missingRendererComponent={
<ColumnIconError title="No renderer found for this check" />
}
/>
</ListItemIcon>
<ListItemText primary={result.check.name} />
</ListItem>
);
})
.filter((v): v is NonNullable<typeof v> => !!v);
const wrapTooltip = (child: JSX.Element) =>
tooltipList.length === 0 ? (
child
) : (
<Tooltip
title={
<List
disablePadding
component="nav"
aria-label="main mailbox folders"
>
{tooltipList}
</List>
}
children={child}
/>
);
if (!validResults.length) {
return <NoData />;
}
const succeeded = validResults.filter(
({ result, renderer }) => !renderer?.isFailed?.(result),
).length;
const rate = succeeded / validResults.length;
const percent = Math.round(rate * 100);
const color = getPercentColor(percent);
return (
<Grid container spacing={0}>
{wrapTooltip(
<Grid item>
<div style={{ cursor: 'default' }}>
<ColumnIconPercent percent={percent} color={color} />
</div>
</Grid>,
)}
<Grid px={1} item alignContent="center">
<Typography variant="caption">
{succeeded}/{validResults.length}
</Typography>
</Grid>
</Grid>
);
}
export function makeGetColumn(
checkFilter: ((check: Check) => boolean) | undefined,
showEmpty: boolean,
): GetColumnFunc {
return function useColumn(entities): ManageColumn {
const { getPercentColor } = useApi(manageTechInsightsApiRef);
const useEntityInsightsResult = useEntityInsights(
entities,
checkFilter,
showEmpty,
);
// We need unique id's for the columns if their render function has changed,
// or there's gonna be a UI warning from material-table
const id = useMemo(() => {
const newId = `${Math.random() * 1.001}`.slice(2);
return newId;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useEntityInsightsResult, getPercentColor]);
return useMemo(
() => ({
id: `tech-insights-merged-result-${id}`,
title: 'Tech Insights',
render: ({ entity }) => {
return (
<CombinedColumn
entity={entity}
useEntityInsightsResult={useEntityInsightsResult}
getPercentColor={getPercentColor}
/>
);
},
}),
[id, useEntityInsightsResult, getPercentColor],
);
};
}

View File

@ -0,0 +1,56 @@
/*
* 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 type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import type { ManageColumnModule } from '@backstage-community/plugin-manage-react';
import { makeGetColumn } from './columns-single';
import { makeGetColumns } from './columns-multiple';
/**
* Options for {@link manageTechInsightsColumns}.
*
* @public
*/
export interface ManageTechInsightsOptions {
/** Combine all checks into a single columns with percentage bar */
combined?: boolean;
/** Only use these checks (defaults to all). */
checkFilter?: (check: Check) => boolean;
/** Also show checks that are empty for all entities of the certain kind. */
showEmpty?: boolean;
}
/**
* Create a column module for displaying tech insights checks.
*
* @public
*/
export function manageTechInsightsColumns(
options?: ManageTechInsightsOptions,
): ManageColumnModule {
const { combined = false, checkFilter, showEmpty = false } = options ?? {};
if (combined) {
return {
getColumn: makeGetColumn(checkFilter, showEmpty),
};
}
return {
getColumns: makeGetColumns(checkFilter, showEmpty),
};
}

View File

@ -0,0 +1,52 @@
/*
* 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 { useApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { techInsightsApiRef } from '@backstage-community/plugin-tech-insights';
import type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import { useManageTechInsights } from '../components/ManageProvider';
import { filterEmptyChecks, stringifyCheck } from '../utils';
export function useEntityInsights(
entities: Entity[],
checkFilter: ((check: Check) => boolean) | undefined,
showEmpty: boolean,
) {
const { allChecks, bulkCheckResponse, renderers } =
useManageTechInsights(checkFilter);
const techInsightsApi = useApi(techInsightsApiRef);
const { usedChecks, responses } = filterEmptyChecks(
bulkCheckResponse,
entities,
showEmpty,
);
const filteredChecks = allChecks.filter(check =>
showEmpty ? true : usedChecks.has(stringifyCheck(check)),
);
return {
techInsightsApi,
responses,
checks: filteredChecks,
renderers,
};
}
export type UseEntityInsightsResult = ReturnType<typeof useEntityInsights>;

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export type { ManageTechInsightsOptions } from './columns';
export { manageTechInsightsColumns } from './columns';

View File

@ -0,0 +1,167 @@
/*
* 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 React, { ReactNode, useCallback } from 'react';
import { makeStyles } from '@mui/styles';
import Box from '@mui/material/Box';
import Grid, { GridOwnProps } from '@mui/material/Grid';
import Tooltip from '@mui/material/Tooltip';
import { useApi } from '@backstage/core-plugin-api';
import { GaugePropsGetColor } from '@backstage/core-components';
import {
useCurrentKinds,
useOwnedEntities,
GaugeCard,
GaugeCardProps,
ManageAccordion,
} from '@backstage-community/plugin-manage-react';
import {
ResponsesForCheck,
useManageTechInsightsForEntities,
} from '../ManageProvider/ManageProviderTechInsights';
import { manageTechInsightsApiRef } from '../../api';
import {
isTitleAsObject,
ManageTechInsightsMapTitle,
ManageTechInsightsTitle,
} from '../../title';
import { useAccordionTitle } from '../../utils';
const useStyles = makeStyles({
root: {
cursor: 'default',
},
});
/**
* Props for {@link ManageTechInsightsCards}.
*
* @public
*/
export interface ManageTechInsightsCardsProps {
containerProps?: Pick<
GridOwnProps,
| 'classes'
| 'columns'
| 'columnSpacing'
| 'direction'
| 'rowSpacing'
| 'spacing'
| 'sx'
| 'wrap'
| 'zeroMinWidth'
>;
gaugeCardProps?: GaugeCardProps;
/**
* Map the title of the check to either an object `{ title, tooltip? }` or to
* a JSX element.
*/
mapTitle?: ManageTechInsightsMapTitle;
/**
* Render the cards inside an accordion.
*
* Defaults to false.
*/
inAccordion?: boolean;
}
function Title({
titleInfo,
}: {
titleInfo: ManageTechInsightsTitle;
}): ReactNode {
const { root } = useStyles();
if (isTitleAsObject(titleInfo)) {
return titleInfo.tooltip ? (
<Tooltip title={titleInfo.tooltip}>
<div className={root}>{titleInfo.title}</div>
</Tooltip>
) : (
<div className={root}>{titleInfo.title}</div>
);
}
return <div className={root}>{titleInfo.content}</div>;
}
/**
* Display a set of cards for the tech insights checks given the current shown
* entities.
*
* @public
*/
export function ManageTechInsightsCards(props: ManageTechInsightsCardsProps) {
const { containerProps, gaugeCardProps, inAccordion } = props;
const kinds = useCurrentKinds();
const entities = useOwnedEntities(kinds);
const { checks, responsesForCheck } =
useManageTechInsightsForEntities(entities);
const { getPercentColor, mapTitle: defaultMapTitle } = useApi(
manageTechInsightsApiRef,
);
const mapTitle = props.mapTitle ?? defaultMapTitle;
const getRatio = (theseResponses: ResponsesForCheck) => {
const tot = theseResponses.length;
const succ = tot - theseResponses.filter(resp => resp.failed).length;
return tot === 0 ? 1 : succ / tot;
};
const getColor = useCallback<GaugePropsGetColor>(
args => {
const rawColor = getPercentColor(args.value);
const muiColor =
rawColor === 'inherit' ? 'inherit' : args.palette[rawColor].main;
return muiColor;
},
[getPercentColor],
);
const grid = (
<Grid columnSpacing={2} marginBottom={2} {...containerProps} container>
{checks.map(({ check, uniq }) => (
<Grid item key={uniq} padding={0}>
<GaugeCard
progress={getRatio(responsesForCheck.get(uniq) ?? [])}
gaugeCardProps={gaugeCardProps}
title={<Title titleInfo={mapTitle(check)} />}
getColor={getColor}
/>
</Grid>
))}
</Grid>
);
const accordionTitle = useAccordionTitle();
return inAccordion ? (
<ManageAccordion title={accordionTitle} name="tech-insights">
{grid}
</ManageAccordion>
) : (
<Box>{grid}</Box>
);
}

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export type { ManageTechInsightsCardsProps } from './Cards';
export { ManageTechInsightsCards } from './Cards';

View File

@ -0,0 +1,146 @@
/*
* 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 React, { ReactNode, useCallback, useMemo } from 'react';
import { makeStyles, useTheme } from '@mui/styles';
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
import { useApi } from '@backstage/core-plugin-api';
import {
useCurrentKinds,
useOwnedEntities,
GaugeGrid,
ManageAccordion,
} from '@backstage-community/plugin-manage-react';
import {
ResponsesForCheck,
useManageTechInsightsForEntities,
} from '../ManageProvider/ManageProviderTechInsights';
import { manageTechInsightsApiRef } from '../../api';
import {
isTitleAsObject,
ManageTechInsightsMapTitle,
ManageTechInsightsTitle,
} from '../../title';
import { useAccordionTitle } from '../../utils';
const useStyles = makeStyles({
root: {
cursor: 'default',
},
});
/**
* Props for {@link ManageTechInsightsGrid}.
*
* @public
*/
export interface ManageTechInsightsGridProps {
/**
* Map the title of the check to either an object `{ title, tooltip? }` or to
* a JSX element.
*/
mapTitle?: ManageTechInsightsMapTitle;
/**
* Render the grid inside an accordion.
*
* Defaults to true.
*/
inAccordion?: boolean;
}
function Title({
titleInfo,
}: {
titleInfo: ManageTechInsightsTitle;
}): ReactNode {
const { root } = useStyles();
if (isTitleAsObject(titleInfo)) {
return titleInfo.tooltip ? (
<Tooltip title={titleInfo.tooltip}>
<div className={root}>{titleInfo.title}</div>
</Tooltip>
) : (
<div className={root}>{titleInfo.title}</div>
);
}
return <div className={root}>{titleInfo.content}</div>;
}
/**
* Display a set of grid boxes for the tech insights checks given the current
* shown entities.
*
* @public
*/
export function ManageTechInsightsGrid(props: ManageTechInsightsGridProps) {
const { palette } = useTheme();
const kinds = useCurrentKinds();
const entities = useOwnedEntities(kinds);
const { inAccordion = true } = props;
const { checks, responsesForCheck } =
useManageTechInsightsForEntities(entities);
const { getPercentColor, mapTitle: defaultMapTitle } = useApi(
manageTechInsightsApiRef,
);
const mapTitle = props.mapTitle ?? defaultMapTitle;
const getRatio = (theseResponses: ResponsesForCheck) => {
const tot = theseResponses.length;
const succ = tot - theseResponses.filter(resp => resp.failed).length;
return tot === 0 ? 1 : succ / tot;
};
const getColor = useCallback(
(progress: number) => {
const rawColor = getPercentColor(progress * 100);
const muiColor =
rawColor === 'inherit'
? 'inherit'
: palette[rawColor]?.main ?? 'inherit';
return muiColor;
},
[getPercentColor, palette],
);
const items = useMemo(() => {
return checks.map(({ check, uniq }) => ({
title: <Title titleInfo={mapTitle(check)} />,
progress: getRatio(responsesForCheck.get(uniq) ?? []),
}));
}, [checks, mapTitle, responsesForCheck]);
const accordionTitle = useAccordionTitle();
return inAccordion ? (
<ManageAccordion title={accordionTitle} name="tech-insights">
<GaugeGrid items={items} getColor={getColor} />
</ManageAccordion>
) : (
<Box>
<GaugeGrid items={items} getColor={getColor} />
</Box>
);
}

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export type { ManageTechInsightsGridProps } from './Grid';
export { ManageTechInsightsGrid } from './Grid';

View File

@ -0,0 +1,202 @@
/*
* 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 React, {
PropsWithChildren,
createContext,
useContext,
useMemo,
} from 'react';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import {
Entity,
parseEntityRef,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
CheckResultRenderer,
techInsightsApiRef,
} from '@backstage-community/plugin-tech-insights';
import { Check } from '@backstage-community/plugin-tech-insights-common/client';
import {
BulkCheckResponse,
CheckResult,
} from '@backstage-community/plugin-tech-insights-common';
import { useManagedEntities } from '@backstage-community/plugin-manage-react';
import { stringifyCheck } from '../../utils';
import { manageTechInsightsApiRef } from '../../api/api';
/**
* @internal
*/
export interface ManageTechInsightsContext {
allChecks: Check[];
bulkCheckResponse: BulkCheckResponse | undefined;
renderers: Map<string, CheckResultRenderer>;
}
const ctx = createContext<ManageTechInsightsContext>(undefined as any);
const { Provider } = ctx;
/** @public */
export function ManageProviderTechInsights(props: PropsWithChildren<{}>) {
const techInsightsApi = useApi(techInsightsApiRef);
const ownedEntities = useManagedEntities();
const asyncState = useAsync(async () => {
const entityRefs = ownedEntities.map(entity =>
parseEntityRef(stringifyEntityRef(entity)),
);
const [allChecks, bulkCheckResponse] = await Promise.all([
techInsightsApi.getAllChecks(),
techInsightsApi.runBulkChecks(entityRefs),
]);
return { allChecks, bulkCheckResponse };
}, [ownedEntities, techInsightsApi]);
const state: ManageTechInsightsContext = useMemo(() => {
const allChecks = asyncState.value?.allChecks ?? [];
const bulkCheckResponse = asyncState.value?.bulkCheckResponse;
const allRenderers = techInsightsApi.getCheckResultRenderers(
allChecks.map(check => check.type),
);
const renderers = new Map(
allRenderers.map(renderer => [renderer.type, renderer]),
);
return {
allChecks,
bulkCheckResponse,
renderers,
};
}, [asyncState.value, techInsightsApi]);
return <Provider value={state} children={props.children} />;
}
/**
* @internal
*/
export function useManageTechInsights(checkFilter?: (check: Check) => boolean) {
const manageTechInsightsApi = useApi(manageTechInsightsApiRef);
const context = useContext(ctx);
const filter = checkFilter ?? manageTechInsightsApi.checkFilter;
return useMemo(() => {
return {
...context,
allChecks: context.allChecks.filter(filter),
bulkCheckResponse: (context.bulkCheckResponse ?? []).map(response => ({
...response,
results: response.results.filter(res => filter(res.check)),
})),
};
}, [context, filter]);
}
/**
* @internal
*/
export type ResponsesForCheck = Array<{
entity: string;
result: CheckResult;
renderer: CheckResultRenderer | undefined;
failed: boolean;
}>;
/**
* @internal
*/
export interface DecoratedCheck {
uniq: string;
renderer: CheckResultRenderer | undefined;
check: Check;
}
/**
* @internal
*/
export interface UseManageTechInsightsForEntitiesResult {
checks: DecoratedCheck[];
responses: BulkCheckResponse;
responsesForCheck: Map<string, ResponsesForCheck>;
}
/**
* @internal
*/
export function useManageTechInsightsForEntities(
entities: Entity[],
checkFilter?: (check: Check) => boolean,
): UseManageTechInsightsForEntitiesResult {
const { allChecks, bulkCheckResponse, renderers } =
useManageTechInsights(checkFilter);
return useMemo((): UseManageTechInsightsForEntitiesResult => {
const entitySet = new Set(
entities.map(entity =>
stringifyEntityRef(entity).toLocaleLowerCase('en-US'),
),
);
const responses = (bulkCheckResponse ?? []).filter(resp =>
entitySet.has(resp.entity.toLocaleLowerCase('en-US')),
);
const responsesForCheck = new Map<string, ResponsesForCheck>();
const uniqueChecks = new Set(
responses.flatMap(resp =>
resp.results.map(chkRes => {
const stringified = stringifyCheck(chkRes.check);
const renderer = renderers.get(chkRes.check.type);
const checkResponses = responsesForCheck.get(stringified) ?? [];
checkResponses.push({
entity: resp.entity,
result: chkRes,
renderer,
failed: !renderer?.isFailed ? false : renderer.isFailed(chkRes),
});
responsesForCheck.set(stringified, checkResponses);
return stringified;
}),
),
);
const checks = allChecks
.filter(check => uniqueChecks.has(stringifyCheck(check)))
.map(
(check): DecoratedCheck => ({
check,
uniq: stringifyCheck(check),
renderer: renderers.get(check.type),
}),
);
return { checks, responses, responsesForCheck };
}, [allChecks, bulkCheckResponse, renderers, entities]);
}

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
export type { ManageTechInsightsContext } from './ManageProviderTechInsights';
export {
ManageProviderTechInsights,
useManageTechInsights,
} from './ManageProviderTechInsights';

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
export type { ManageTechInsightsCardsProps } from './Cards';
export { ManageTechInsightsCards } from './Cards';
export type { ManageTechInsightsGridProps } from './Grid';
export { ManageTechInsightsGrid } from './Grid';

View File

@ -0,0 +1,32 @@
/*
* 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.
*/
export type * from './api';
export * from './api';
export type * from './components';
export * from './components';
export type { ManageProviderTechInsights } from './components/ManageProvider';
export type { ManageTechInsightsOptions } from './columns';
export { manageTechInsightsColumns } from './columns';
export type {
ManageTechInsightsTitleAsObject,
ManageTechInsightsTitleAsElement,
ManageTechInsightsTitle,
ManageTechInsightsMapTitle,
} from './title';

View File

@ -0,0 +1,16 @@
/*
* 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.
*/
export {};

View File

@ -0,0 +1,45 @@
/*
* 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 type { ReactNode } from 'react';
import type { Check } from '@backstage-community/plugin-tech-insights-common/client';
/** @public */
export type ManageTechInsightsTitleAsObject = {
title: string;
tooltip?: ReactNode;
};
/** @public */
export type ManageTechInsightsTitleAsElement = { content: ReactNode };
/** @public */
export type ManageTechInsightsTitle =
| ManageTechInsightsTitleAsObject
| ManageTechInsightsTitleAsElement;
/** @public */
export type ManageTechInsightsMapTitle = (
check: Check,
) => ManageTechInsightsTitle;
export function isTitleAsObject(
titleInfo: ManageTechInsightsTitle,
): titleInfo is ManageTechInsightsTitleAsObject {
return (
typeof (titleInfo as ManageTechInsightsTitleAsObject).title !== 'undefined'
);
}

View File

@ -0,0 +1,79 @@
/*
* 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 { stringifyEntityRef, type Entity } from '@backstage/catalog-model';
import type { Check } from '@backstage-community/plugin-tech-insights-common/client';
import type {
BulkCheckResponse,
CheckResponse,
CheckResult,
} from '@backstage-community/plugin-tech-insights-common';
import { useCurrentKindTitle } from '@backstage-community/plugin-manage-react';
export function stringifyCheck(check: Check | CheckResponse): string {
return `${check.id}-${check.name}-${check.type}`;
}
export function eqCheck(a: Check | CheckResponse, b: Check | CheckResponse) {
return stringifyCheck(a) === stringifyCheck(b);
}
/**
* Given a bulk response, it filters checks that are defined for the given
* entities.
*
* It also filters out checks that are undefined for all entities, if chosen to.
*
* Returns:
* {
* responses: Map<entity string ref, CheckResult[]>
* usedChecks: Set<string> // stringified checks
* }
*
* @internal
*/
export function filterEmptyChecks(
bulkCheckResponse: BulkCheckResponse | undefined,
entities: Entity[],
includeEmpty = false,
): { responses: Map<string, CheckResult[]>; usedChecks: Set<string> } {
const responses = new Map<string, CheckResult[]>();
const usedChecks = new Set<string>();
const entitiesSet = new Set(
entities.map(entity => stringifyEntityRef(entity)),
);
bulkCheckResponse
?.filter(resp => entitiesSet.has(resp.entity))
?.forEach(resp => {
responses.set(resp.entity, resp.results);
resp.results.forEach(res => {
if (includeEmpty || typeof res.result !== 'undefined')
usedChecks.add(stringifyCheck(res.check));
});
});
return { responses, usedChecks };
}
export function useAccordionTitle() {
const kindTitle = useCurrentKindTitle();
return `Tech insights of your ${kindTitle}`;
}

View File

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

View File

@ -0,0 +1,86 @@
# Manage, react package
This package is used by the Manage plugin, and contains components and hooks useful for building components to be used in the Manage page, in an entity tab or outside, or an entity table column.
It also acts as the base for all packages that extends the entity table columns and need a provider. Those packages can provide an api ref of a type that extends the exported type `ManageModuleApi`. When defining the `manageApiRef`, an implementor can add this api ref when doing `createManageApiFactory([...])`.
The hooks in this package can generally be used anywhere in the manage page unless stated otherwise.
## Hooks
### useOwnedKinds
Returns the kinds configured in the ManagePage component (or defaults to the original kinds in Backstage). If the parameter `onlyOwned` is true, it will only return the kinds of entities actually owned by the user.
### useOwnedEntities
Returns an array of all owned entities. By passing a kind (or array of kinds) as parameter, only entities of those kinds are returned.
### useManagedEntities
Returns all managed entities, i.e. owned entities and starred entities.
### useOwners
Returns an object on the form:
```ts
{
groups: Entity[];
ownedEntityRefs: string[];
}
```
`ownedEntityRefs` is a list of entity refs for all owners (incl. the current user). `groups` is a list of `Entity` objects for the owners that are groups.
These lists are ordered by:
1. Immediate group membership
2. Groups higher up the group hierarchy
3. User
Within each such category, the entities are ordered by their title/name alpha-numerically.
### useCurrentKind
Returns the _current_ kind, if used within an entity tab. If called from outside an entity tab, returns `undefined`.
### useCurrentKinds
Same as `useCurrentKind` but returns an array of kinds, either only the _current_ kind, but fallbacks to the result of `useOwnedKinds`,
### useCurrentKindTitle
Returns the name (title) of the current kind, e.g. "components" or "systems". Can also be "entities" if the combined view is used, or "starred entities" if that tab is active.
This can be used by modules that extend the page, and is currently used by `@backstage-community/plugin-manage-module-tech-insights` for the accordion title.
## Components
### Accordion
This is a MUI Accordion with the expanded state saved in user settings.
### GaugeCard
This is the `@backstage/core-components` `GuageCard` component with pre-defined props to make them appear the same, when showing multiple gauges from different plugin modules.
### GaugeGrid
Similar to `GaugeCard`, `GaugeGrid` can be used instead, which shows smaller cards for each Gauge.
### ColumnIconError
When implementing a column provider, this component can act as a fallback error icon.
### ColumnIconPercent
When implementing a column provider, this component can show a (circular) percentage gauge.
### ReorderableTabs
A component rendering tabs (although as a button group) with drag-and-drop support. This is used in the Settings page to give the user the ability to reorder the tabs and kinds.
### TabContentFullHeight
The helper component `TabContentFullHeight` can be used as a wrapper around the content for a tab. It sets its exact height to adapt to the screen size (and updates when the window changes size). The optional boolean prop `resizeChild` which can be set to also update the size of the (one and only) child component. The prop `bottomMargin` can be used to set a bottom margin other than the default.

View File

@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-manage-react
title: '@backstage/plugin-manage-react'
description: Manage page. React components.
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: maintainers

View File

@ -0,0 +1,73 @@
{
"name": "@backstage-community/plugin-manage-react",
"description": "Manage plugin - react package",
"version": "0.1.0",
"backstage": {
"role": "web-library",
"pluginId": "manage",
"pluginPackages": [
"@backstage-community/plugin-manage",
"@backstage-community/plugin-manage-react"
]
},
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/manage-react"
},
"keywords": [
"backstage",
"manage",
"page",
"manage-page",
"components"
],
"sideEffects": false,
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^1.7.3",
"@backstage/core-components": "^0.16.3",
"@backstage/core-plugin-api": "^1.10.3",
"@backstage/plugin-catalog-react": "^1.15.1",
"@backstage/types": "^1.2.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@mui/icons-material": "^5.16.7",
"@mui/material": "^5.15.16",
"@mui/styles": "^5",
"already": "^2.2.1",
"pluralize": "^8.0.0",
"react-use": "^17.5.0"
},
"peerDependencies": {
"@dnd-kit/utilities": "*",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.29.5",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.3.12"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,417 @@
## API Report File for "@backstage-community/plugin-manage-react"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { ApiFactory } from '@backstage/core-plugin-api';
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { CircularProgressProps } from '@mui/material/CircularProgress';
import { ComponentProps } from 'react';
import type { ComponentType } from 'react';
import { Entity } from '@backstage/catalog-model';
import { GaugeCard as GaugeCard_2 } from '@backstage/core-components';
import { GaugePropsGetColor } from '@backstage/core-components';
import { GridOwnProps } from '@mui/material/Grid';
import { JsonValue } from '@backstage/types';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
// @public
export interface ApiFactoryOptions {
extensions?: Iterable<ManageModuleApiRef>;
kindOrder?: string[];
}
// @public
export function arrayify<T>(t: T | T[] | Iterable<T> | undefined): T[];
// @public
export function ColumnIconError(
props: ColumnIconErrorProps,
): React_2.JSX.Element;
// @public
export interface ColumnIconErrorProps {
title?: string;
}
// @public
export function ColumnIconPercent(
props: ColumnIconPercentProps,
): React_2.JSX.Element;
// @public
export interface ColumnIconPercentProps {
// (undocumented)
color?: ProgressColor;
// (undocumented)
percent: number;
// (undocumented)
title?: string;
}
// @public
export function createManageApiFactory(options?: ApiFactoryOptions): ApiFactory<
ManageApi,
DefaultManageApi,
{
[x: string]: any;
}
>;
// @public
export function createUserSettingsContext<T extends JsonValue>(
feature: string,
settingsKey: string,
options?: CreateUserSettingsContextOptions<T>,
): UserSettingsContextResult<T>;
// @public (undocumented)
export interface CreateUserSettingsContextOptions<T extends JsonValue> {
coerce?: (value: JsonValue) => T;
// (undocumented)
defaultValue?: T | undefined;
}
// @public
export function CurrentKindProvider(
props: PropsWithChildren<
| {
kind: string;
starred?: never;
}
| {
kind?: never;
starred: true;
}
>,
): React_2.JSX.Element;
// @public
export class DefaultManageApi implements ManageApi {
constructor({ kindOrder, providers }: DefaultManageApiOptions);
// (undocumented)
getProviders: () => readonly ManageProvider[];
// (undocumented)
readonly kindOrder: string[];
}
// @public (undocumented)
export interface DefaultManageApiOptions {
kindOrder?: string[];
providers: Iterable<ManageModuleApi>;
}
// @public (undocumented)
export const GaugeCard: ManageGaugeCard;
// @public (undocumented)
export type GaugeCardProps = Pick<
ComponentProps<typeof GaugeCard_2>,
'size' | 'alignGauge' | 'variant' | 'description' | 'subheader'
>;
// @public (undocumented)
export const GaugeGrid: ManageGaugeGrid;
// @public (undocumented)
export type GetColumnFunc = (entities: Entity[]) => ManageColumn;
// @public (undocumented)
export type GetColumnsFunc = (entities: Entity[]) => ManageColumn[];
// @public
export function isManageColumnModuleMultiple(
column: ManageColumnModule,
): column is ManageColumnModuleMultiple;
// @public (undocumented)
export interface ItemWithKey<T> {
// (undocumented)
key: string;
// (undocumented)
value: T;
}
// @public
export const KindOrderProvider: (props: {
children?: ReactNode;
}) => JSX.Element;
// @public
export const KindStarred: unique symbol;
// @public (undocumented)
export type KindStarredType = typeof KindStarred;
// @public
export function ManageAccordion(
props: PropsWithChildren<ManageAccordionProps>,
): React_2.JSX.Element;
// @public
export interface ManageAccordionProps {
defaultExpanded?: boolean;
name: string;
perKind?: boolean;
title: string;
}
// @public (undocumented)
export interface ManageApi {
getProviders(): Iterable<ManageProvider>;
readonly kindOrder: string[];
}
// @public
export const manageApiRef: ApiRef<ManageApi>;
// @public (undocumented)
export interface ManageColumn {
// (undocumented)
id: string;
// (undocumented)
render: (opts: { entity: Entity }) => ReactNode;
// (undocumented)
title: string;
}
// @public (undocumented)
export type ManageColumnModule =
| ManageColumnModuleMultiple
| ManageColumnModuleSingle;
// @public (undocumented)
export interface ManageColumnModuleMultiple {
// (undocumented)
getColumn?: never;
// (undocumented)
getColumns: GetColumnsFunc;
}
// @public (undocumented)
export interface ManageColumnModuleSingle {
// (undocumented)
getColumn: GetColumnFunc;
// (undocumented)
getColumns?: never;
}
// @public
export function ManageGaugeCard(
props: ManageGaugeCardProps,
): React_2.JSX.Element;
// @public (undocumented)
export interface ManageGaugeCardProps {
gaugeCardProps?: GaugeCardProps;
getColor: GaugePropsGetColor;
progress: number;
title: ReactNode;
}
// @public (undocumented)
export function ManageGaugeGrid(
props: ManageGaugeGridProps,
): React_2.JSX.Element;
// @public (undocumented)
export interface ManageGaugeGridProps {
// (undocumented)
containerProps?: Pick<
GridOwnProps,
| 'classes'
| 'columns'
| 'columnSpacing'
| 'direction'
| 'rowSpacing'
| 'spacing'
| 'sx'
| 'wrap'
| 'zeroMinWidth'
>;
getColor: (percent: number) => string;
items: {
title: ReactNode;
description?: ReactNode;
progress: number;
}[];
noBottomMargin?: boolean;
}
// @public
export interface ManageModuleApi {
getProvider?: () => ManageProvider;
}
// @public
export type ManageModuleApiRef = ApiRef<ManageModuleApi>;
// @public
export function ManageOwnedProvider(
props: PropsWithChildren<OwnedEntitiesProviderProps>,
): React_2.JSX.Element;
// @public (undocumented)
export type ManageProvider = ComponentType<{
children?: ReactNode | undefined;
}>;
// @public (undocumented)
export const manageReactPlugin: BackstagePlugin<{}, {}, {}>;
// @public
export function ManageReorderableTabs(
props: ReorderableTabsProps,
): React_2.JSX.Element;
// @public (undocumented)
export function ManageTabContentFullHeight({
children,
bottomMargin,
resizeChild,
}: PropsWithChildren<TabContentFullHeightProps>): React_2.JSX.Element;
// @public (undocumented)
export interface OwnedEntitiesProviderProps {
// (undocumented)
kinds?: string[];
}
// @public (undocumented)
export const OwnedProvider: ManageOwnedProvider;
// @public
export interface Owners {
// (undocumented)
groups: Entity[];
// (undocumented)
ownedEntityRefs: string[];
}
// @public
export function pluralizeKind(kind: string): string;
// @public (undocumented)
export type ProgressColor = Extract<CircularProgressProps['color'], string>;
// @public (undocumented)
export const ReorderableTabs: ManageReorderableTabs;
// @public
export interface ReorderableTabsProps {
// (undocumented)
onChange?: (idOrder: string[]) => void;
// (undocumented)
tabs: {
id: string;
title: string;
}[];
}
// @public
export function simplifyColumns(
column: ManageColumnModule,
): ManageColumnModuleMultiple;
// @public (undocumented)
export const TabContentFullHeight: ManageTabContentFullHeight;
// @public
export interface TabContentFullHeightProps {
bottomMargin?: number;
resizeChild?: boolean;
}
// @public
export function useAccordionKey(key: string, uniquePerKind?: boolean): string;
// @public
export function useCurrentKind(): string | KindStarredType | undefined;
// @public
export function useCurrentKinds(
onlyOwned?: boolean,
): (string | KindStarredType)[];
// @public
export function useCurrentKindTitle(): string;
// @public
export function useKindOrder<T extends string | KindStarredType>(
kinds: T[],
): T[];
// @public
export function useManagedEntities(): Entity[];
// @public
export function useOrder<T, U>(
items: T[],
keys: U[],
options: UseOrderOptions<T, U>,
): T[];
// @public (undocumented)
export function useOrder<T extends string>(
items: T[],
keys: T[],
options?: Partial<UseOrderOptions<T, T>>,
): T[];
// @public
export interface UseOrderOptions<T, U> {
caseSensitive?: boolean;
itemsMemoMethod?: 'reference' | 'key';
joiner?: (keys: string[]) => string;
keyOf: (item: T) => U;
keysMemoMethod?: 'reference' | 'key';
nonFoundCompare?: (a: ItemWithKey<T>, b: ItemWithKey<T>) => number;
stringifyItem?: (item: T) => string;
stringifyKey?: (key: U) => string;
}
// @public
export function useOwnedEntities(
kind?: string | KindStarredType | (string | KindStarredType)[],
): Entity[];
// @public
export function useOwnedKinds(onlyOwned?: boolean): string[];
// @public
export function useOwners(): Owners;
// @public (undocumented)
export interface UserSettingsContextResult<T extends JsonValue> {
// (undocumented)
Provider: (props: PropsWithChildren<{}>) => JSX.Element;
// (undocumented)
useSetSetting: () => (value: T) => void;
// (undocumented)
useSetting: () => T | undefined;
}
// @public
export const useSetKindOrder: () => (value: string[]) => void;
// @public
export function useUserSettings<T extends JsonValue>(
feature: string,
key: string,
options?: UseUserSettingsOptions<T>,
): [value: T | undefined, setValue: (value: T) => void, isSettled: boolean];
// @public
export interface UseUserSettingsOptions<T extends JsonValue> {
coerce?: (value: JsonValue) => T;
// (undocumented)
defaultValue?: T | undefined;
}
// (No @packageDocumentation comment for this package)
```

View File

@ -0,0 +1,53 @@
/*
* 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 type { ManageApi, ManageProvider } from './ManageApi';
import { ManageModuleApi } from './types';
/** @public */
export interface DefaultManageApiOptions {
/**
* The kind order to use when rendering the owned entities.
*/
kindOrder?: string[];
/**
* Manage providers to include. These will be mounted top-level, so that any
* component in the Manage page can access them
*/
providers: Iterable<ManageModuleApi>;
}
/**
* Default implementation of the ManageApi.
*
* @public
*/
export class DefaultManageApi implements ManageApi {
public readonly kindOrder: string[];
readonly #providers: ManageProvider[] = [];
public constructor({ kindOrder, providers }: DefaultManageApiOptions) {
this.kindOrder = kindOrder ?? [];
this.#providers = Array.from(providers)
.map(provider => provider.getProvider?.())
.filter((v): v is NonNullable<typeof v> => !!v);
}
getProviders = (): readonly ManageProvider[] => {
return this.#providers;
};
}

View File

@ -0,0 +1,36 @@
/*
* 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 type { ComponentType, ReactNode } from 'react';
/** @public */
export type ManageProvider = ComponentType<{
children?: ReactNode | undefined;
}>;
/** @public */
export interface ManageApi {
/**
* The order of kinds to show for e.g. tabs.
*
* Kinds not part of this list will appear afterwards.
*/
readonly kindOrder: string[];
/**
* Get the list of registered Providers for the manage page
*/
getProviders(): Iterable<ManageProvider>;
}

View File

@ -0,0 +1,70 @@
/*
* 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 { createApiFactory, createApiRef } from '@backstage/core-plugin-api';
import type { ManageApi } from './ManageApi';
import { DefaultManageApi } from './DefaultManageApi';
import { ManageModuleApiRef } from './types';
/**
* ApiRef for the Manage API.
*
* @public
*/
export const manageApiRef = createApiRef<ManageApi>({ id: 'manage' });
/**
* Options for creating the Manage API.
*
* @public
*/
export interface ApiFactoryOptions {
/**
* The kind order to use when rendering the owned entities.
*/
kindOrder?: string[];
/**
* Optional Manage extensions to include in the API.
*/
extensions?: Iterable<ManageModuleApiRef>;
}
/**
* Default API factory for the Manage plugin.
*
* This simplifies the API creation by providing a default implementation.
*
* @public
*/
export function createManageApiFactory(options?: ApiFactoryOptions) {
const { kindOrder, extensions = [] } = options ?? {};
const apiDeps = Object.fromEntries(
Array.from(extensions).map(apiRef => [apiRef.id, apiRef]),
);
return createApiFactory({
api: manageApiRef,
deps: apiDeps,
factory(deps) {
return new DefaultManageApi({
kindOrder,
providers: Object.values(deps),
});
},
});
}

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
export type { ManageProvider, ManageApi } from './ManageApi';
export type { ManageModuleApi, ManageModuleApiRef } from './types';
export type { DefaultManageApiOptions } from './DefaultManageApi';
export { DefaultManageApi } from './DefaultManageApi';
export type { ApiFactoryOptions } from './api';
export { manageApiRef, createManageApiFactory } from './api';

View File

@ -0,0 +1,38 @@
/*
* 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 { ApiRef } from '@backstage/core-plugin-api';
import { ManageProvider } from './ManageApi';
/**
* This is a base interface for plugin modules to extends, to register
* themselves as extensions to the Manage plugin.
*
* @public
*/
export interface ManageModuleApi {
/**
* Get an optional ManageProvider component.
*/
getProvider?: () => ManageProvider;
}
/**
* Base type for extension APIs.
*
* @public
*/
export type ManageModuleApiRef = ApiRef<ManageModuleApi>;

View File

@ -0,0 +1,118 @@
/*
* 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 React, { ComponentProps, PropsWithChildren, useCallback } from 'react';
import { makeStyles } from '@mui/styles';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { useAccordionKey, useUserSettings } from '../../hooks';
const useStyles = makeStyles(theme => ({
root: {
marginBottom: theme.spacing(2),
},
summary: {
marginTop: theme.spacing(0.5),
marginBottom: theme.spacing(0),
'&.Mui-expanded': {
marginTop: theme.spacing(0.5),
marginBottom: theme.spacing(1),
},
},
details: {
paddingBottom: 0,
},
}));
/**
* Props for {@link ManageAccordion}
*
* @public
*/
export interface ManageAccordionProps {
/**
* Title of the accordion
*/
title: string;
/**
* Name of the accordion. This will be used to create accordion keys to save
* the open/close state. This is intended to be a feature/plugin name.
*/
name: string;
/**
* Make the accordion default-expanded. Defaults to false.
*/
defaultExpanded?: boolean;
/**
* Saves the expanded state per kind. Defaults to false, meaning the expanded
* state is shared between all kinds.
*/
perKind?: boolean;
}
type AccordionOnChange = NonNullable<
ComponentProps<typeof Accordion>['onChange']
>;
/**
* Renders a MUI Accordion with a title and content. The open/close state of the
* accordion is saved in the user settings.
*
* @public
*/
export function ManageAccordion(
props: PropsWithChildren<ManageAccordionProps>,
) {
const { title, name, defaultExpanded, perKind = false, children } = props;
const accordionKey = useAccordionKey('manage-accordion', perKind);
const [expanded, setExpanded] = useUserSettings(name, accordionKey, {
defaultValue: defaultExpanded ?? false,
});
const onChange = useCallback<AccordionOnChange>(
(_, value) => {
setExpanded(value);
},
[setExpanded],
);
const { root, summary, details } = useStyles();
return (
<Accordion classes={{ root }} expanded={expanded} onChange={onChange}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
classes={{ content: summary }}
>
<Typography variant="h6" component="span">
{title}
</Typography>
</AccordionSummary>
<AccordionDetails classes={{ root: details }}>
{children}
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export type { ManageAccordionProps } from './Accordion';
export { ManageAccordion } from './Accordion';

View File

@ -0,0 +1,116 @@
/*
* 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 React, {
createContext,
PropsWithChildren,
useContext,
useMemo,
} from 'react';
import { useOwnedKinds } from '../OwnedEntitiesProvider/OwnedEntitiesProvider';
import { pluralizeKind } from '../../utils';
import { KindStarred, KindStarredType } from './types';
interface CurrentKindContext {
kind: string | KindStarredType;
}
const ctx = createContext<CurrentKindContext>(undefined as any);
const { Provider } = ctx;
/**
* Provider used by `@backstage-community/plugin-manage`, and shouldn't be used
* elsewhere.
*
* @public
*/
export function CurrentKindProvider(
props: PropsWithChildren<
{ kind: string; starred?: never } | { kind?: never; starred: true }
>,
) {
const { kind, starred, children } = props;
const value = useMemo(() => {
if (starred) {
return { kind: KindStarred as KindStarredType };
}
return { kind };
}, [kind, starred]);
return <Provider value={value}>{children}</Provider>;
}
/**
* Returns the current kind, i.e. if the component is inside a tab with only
* components, or systems, e.g.
*
* If rendered outside such a tab, returns undefined.
*
* @public
*/
export function useCurrentKind(): string | KindStarredType | undefined {
const context = useContext(ctx);
if (!context) {
return undefined;
}
return context.kind;
}
/**
* Same as {@link useCurrentKind} except if not used inside a kind tab, it
* fallbacks to all owned entity kinds.
*
* @param onlyOwned - Only return kinds for entities actually owned, otherwise
* all configured kinds
*
* @public
*/
export function useCurrentKinds(
onlyOwned = false,
): (string | KindStarredType)[] {
const context = useContext(ctx);
const currentKind = useMemo(() => {
if (!context) {
return undefined;
}
return [context.kind as string | KindStarredType];
}, [context]);
const ownedKinds = useOwnedKinds(onlyOwned);
return currentKind ?? ownedKinds;
}
/**
*
* Returns the title for the current kind, e.g. "components" or
* "starred entities".
*
* @public
*/
export function useCurrentKindTitle() {
const kind = useCurrentKind();
if (!kind) {
return 'entities';
}
if (kind === KindStarred) {
return 'starred entities';
}
return `${pluralizeKind(kind)}`;
}

View File

@ -0,0 +1,25 @@
/*
* 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.
*/
export {
CurrentKindProvider,
useCurrentKind,
useCurrentKinds,
useCurrentKindTitle,
} from './CurrentKindProvider';
export type { KindStarredType } from './types';
export { KindStarred } from './types';

View File

@ -0,0 +1,25 @@
/*
* 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.
*/
/**
* Symbol used to represent starred entities. This is a special "kind".
*
* @public
*/
export const KindStarred = Symbol('Starred entities');
/** @public */
export type KindStarredType = typeof KindStarred;

View File

@ -0,0 +1,69 @@
/*
* 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 React, { ComponentProps, ReactNode } from 'react';
import { GaugeCard, GaugePropsGetColor } from '@backstage/core-components';
/** @public */
export type GaugeCardProps = Pick<
ComponentProps<typeof GaugeCard>,
'size' | 'alignGauge' | 'variant' | 'description' | 'subheader'
>;
/** @public */
export interface ManageGaugeCardProps {
/**
* Title of the card
*/
title: ReactNode;
/**
* A number between 0 and 1 defining the progress (0% - 100%)
*/
progress: number;
/**
* Function which turns a value into a color
*/
getColor: GaugePropsGetColor;
/**
* Optional gauge card props
*/
gaugeCardProps?: GaugeCardProps;
}
/**
* This component is `@backstage/core-component`'s GaugeCard with pre-defined
* defaults.
*
* @public
*/
export function ManageGaugeCard(props: ManageGaugeCardProps) {
const { title, progress, getColor, gaugeCardProps } = props;
return (
<GaugeCard
size="small"
alignGauge="bottom"
variant="fullHeight"
{...gaugeCardProps}
title={title as string}
progress={progress}
getColor={getColor}
/>
);
}

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export type { ManageGaugeCardProps, GaugeCardProps } from './GaugeCard';
export { ManageGaugeCard } from './GaugeCard';

View File

@ -0,0 +1,131 @@
/*
* 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 React, { ReactNode } from 'react';
import { makeStyles } from '@mui/styles';
import Box from '@mui/material/Box';
import Grid, { GridOwnProps } from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
const useStyles = makeStyles(theme => ({
gridRootWithoutBottomMargin: {
marginBottom: 0,
},
gridItem: {
overflow: 'visible',
},
box: {
lineHeight: 0,
borderRadius: theme.shape.borderRadius,
borderWidth: 1,
borderLeftWidth: 3,
borderStyle: 'solid',
borderColor: theme.palette.divider,
backgroundColor: theme.palette.background.paper,
// boxShadow: theme.shadows[2],
},
percentText: {
color: theme.palette.text.secondary,
},
}));
/** @public */
export interface ManageGaugeGridProps {
containerProps?: Pick<
GridOwnProps,
| 'classes'
| 'columns'
| 'columnSpacing'
| 'direction'
| 'rowSpacing'
| 'spacing'
| 'sx'
| 'wrap'
| 'zeroMinWidth'
>;
/**
* Items to display in the grid
*/
items: {
/**
* Title of the card
*/
title: ReactNode;
/**
* Description of the item
*/
description?: ReactNode;
/**
* A number between 0 and 1 defining the progress (0% - 100%)
*/
progress: number;
}[];
/**
* Function which turns a progress number (between 0 and 1) into a color
*/
getColor: (percent: number) => string;
/**
* Optionally disable the bottom margin of the grid
*/
noBottomMargin?: boolean;
}
/** @public */
export function ManageGaugeGrid(props: ManageGaugeGridProps) {
const { containerProps, items, getColor, noBottomMargin } = props;
const { gridRootWithoutBottomMargin, gridItem, box, percentText } =
useStyles();
const content = (
<Grid
columnSpacing={2}
marginBottom={2}
{...containerProps}
className={noBottomMargin ? gridRootWithoutBottomMargin : undefined}
container
>
{items.map(({ title, progress }, i) => {
const value = progress * 100;
const color = getColor(progress);
return (
<Grid item key={i} padding={0} className={gridItem}>
<div className={box} style={{ borderLeftColor: color }}>
<Grid container spacing={0} padding={1} columnSpacing={1}>
<Grid item>
<Typography variant="body2" className={percentText}>
{Math.round(value)}%
</Typography>
</Grid>
<Grid item alignContent="center">
{title}
</Grid>
</Grid>
</div>
</Grid>
);
})}
</Grid>
);
return <Box>{content}</Box>;
}

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export type { ManageGaugeGridProps } from './GaugeGrid';
export { ManageGaugeGrid } from './GaugeGrid';

View File

@ -0,0 +1,104 @@
/*
* 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 { useMemo } from 'react';
import { useApi } from '@backstage/core-plugin-api';
import type { KindStarredType } from '../CurrentKindProvider';
import { createUserSettingsContext } from '../UserSettingsProvider';
import { manageApiRef } from '../../api';
import { joinKinds, kindToOpaqueString } from '../../utils';
import { useOrder } from '../../hooks/use-order';
const userSettingsFeature = '$manage-page-kind';
const userSettingsKey = 'order';
const coerceStringArray = (arr: any): string[] => {
if (!Array.isArray(arr)) {
return [];
}
return arr.map(value => (typeof value !== 'string' ? `${value}` : value));
};
const userSettingsContext = createUserSettingsContext(
userSettingsFeature,
userSettingsKey,
{
defaultValue: [],
coerce: coerceStringArray,
},
);
/**
* This is an internal API and should not be used directly.
*
* @public
*/
export const KindOrderProvider = userSettingsContext.Provider;
/**
* This hook is internal and should not be used directly.
*
* @public
*/
export const useSetKindOrder = userSettingsContext.useSetSetting;
const useKindOrderUserSetting = userSettingsContext.useSetting;
/**
* Re-order kinds to adhere to the configured kind order (case-insensitive),
* i.e. configured in the API.
*/
function useKindOrderFromApi(
kinds: (string | KindStarredType)[],
): (string | KindStarredType)[] {
const manageApi = useApi(manageApiRef);
const { kindOrder } = manageApi;
const lcKindOrder = useMemo(
() => kindOrder.map(kind => kind.toLocaleLowerCase('en-US')),
[kindOrder],
);
return useOrder(kinds, lcKindOrder, {
keyOf: (kind: string | KindStarredType) =>
kindToOpaqueString(kind).toLocaleLowerCase('en-US'),
stringifyKey: key => kindToOpaqueString(key),
nonFoundCompare: (a, b) => a.key.localeCompare(b.key),
joiner: joinKinds,
});
}
/**
* Re-order kinds to adhere to the user settings kind order (case-insensitive)
* while falling back to the order as configured in the API.
*
* @public
*/
export function useKindOrder<T extends string | KindStarredType>(
kinds: T[],
): T[] {
const userSettingsOrder = useKindOrderUserSetting() ?? [];
const apiOrder = useKindOrderFromApi(kinds);
const orderBy = userSettingsOrder.length > 0 ? userSettingsOrder : apiOrder;
return useOrder(kinds, orderBy, {
keyOf: (kind: string | KindStarredType) =>
kindToOpaqueString(kind).toLocaleLowerCase('en-US'),
stringifyKey: key => kindToOpaqueString(key),
});
}

View File

@ -0,0 +1,16 @@
/*
* 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.
*/
export { KindOrderProvider, useSetKindOrder, useKindOrder } from './KindOrder';

View File

@ -0,0 +1,198 @@
/*
* 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 React, {
PropsWithChildren,
createContext,
useContext,
useMemo,
} from 'react';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import {
catalogApiRef,
useStarredEntities,
} from '@backstage/plugin-catalog-react';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { ErrorPanel, Progress } from '@backstage/core-components';
import { useOwners } from '../OwnedGroupsProvider';
import { useKindOrder } from '../KindOrder';
import { arrayify, joinKinds } from '../../utils';
import { getOwnedEntities } from './catalog';
import { defaultKinds } from './types';
import {
type KindStarredType,
KindStarred,
} from '../CurrentKindProvider/types';
interface OwnedEntitiesProviderContext {
kinds: string[];
asyncEntities: AsyncState<Entity[]>;
entities: Entity[];
starredEntities: Entity[];
}
const ctx = createContext<OwnedEntitiesProviderContext>(undefined as any);
const { Provider } = ctx;
export interface OwnedEntitiesProviderProps {
kinds?: string[];
}
export function OwnedEntitiesProvider(
props: PropsWithChildren<OwnedEntitiesProviderProps>,
) {
const { kinds = defaultKinds } = props;
const { starredEntities: starredEntityRefs } = useStarredEntities();
const catalogApi = useApi(catalogApiRef);
const owners = useOwners();
const entities = useAsync(async (): Promise<Entity[]> => {
if (owners.ownedEntityRefs.length === 0) {
return [];
}
return getOwnedEntities(catalogApi, kinds, owners.ownedEntityRefs);
}, [kinds, owners.ownedEntityRefs]);
const starredEntityRefList = Array.from(starredEntityRefs);
const starredEntities = useAsync(async (): Promise<Entity[]> => {
return (
await catalogApi.getEntitiesByRefs({
entityRefs: starredEntityRefList,
})
).items.filter((v): v is NonNullable<typeof v> => !!v);
}, [starredEntityRefList.join(' $ ')]);
const value = useMemo(
(): OwnedEntitiesProviderContext => ({
kinds,
asyncEntities: entities,
entities: entities.value ?? [],
starredEntities: starredEntities.value ?? [],
}),
[kinds, entities, starredEntities],
);
if (value.asyncEntities.loading || starredEntities.loading) {
return <Progress />;
} else if (value.asyncEntities.error) {
return <ErrorPanel error={value.asyncEntities.error} />;
}
return <Provider value={value}>{props.children}</Provider>;
}
/**
* Return all kinds given to <ManagePage>
*
* @param onlyOwned - Only return kinds for entities actually owned, otherwise
* all configured kinds
*
* @public
*/
export function useOwnedKinds(onlyOwned = false): string[] {
const { kinds, entities } = useContext(ctx);
const ownedEntities = useMemo(
() =>
new Set(
!onlyOwned
? []
: (entities ?? []).map(entity =>
entity.kind.toLocaleLowerCase('en-US'),
),
),
[onlyOwned, entities],
);
return useMemo(() => {
if (!onlyOwned) {
return kinds;
}
return kinds.filter(kind => {
const lcKind = kind.toLocaleLowerCase('en-US');
return ownedEntities.has(lcKind);
});
}, [onlyOwned, kinds, ownedEntities]);
}
/**
* Returns all owned entities, possibly filtered by kind.
*
* By default all owned entities are returned, but by passing a kind (or array
* of kinds), only those will be returned. There is a special kind `KindStarred`
* exported by this package, will reflects the starred entities.
*
* @public
*/
export function useOwnedEntities(
kind?: string | KindStarredType | (string | KindStarredType)[],
): Entity[] {
const { kinds: ownedKinds, entities, starredEntities } = useContext(ctx);
const kinds = arrayify(kind ?? ownedKinds);
const orderedKinds = useKindOrder(kinds);
return useMemo(
(): Entity[] => {
const lcKinds = orderedKinds.map(curKind =>
typeof curKind === 'symbol'
? curKind
: curKind?.toLocaleLowerCase('en-US'),
);
const filteredEntities = (entities ?? []).filter(entity =>
lcKinds.includes(entity.kind.toLocaleLowerCase('en-US')),
);
return lcKinds.flatMap(curKind =>
curKind === KindStarred
? starredEntities
: filteredEntities.filter(
entity => entity.kind.toLocaleLowerCase('en-US') === curKind,
),
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[joinKinds(orderedKinds), entities, starredEntities],
);
}
/**
* Returns all managed entites, i.e. owned entities and starred entities.
*
* @public
*/
export function useManagedEntities(): Entity[] {
const { entities, starredEntities } = useContext(ctx);
return useMemo((): Entity[] => {
const set = new Set<string>();
return ([] as Entity[]).concat(entities, starredEntities).filter(entity => {
const entityRef = stringifyEntityRef(entity);
if (set.has(entityRef)) {
return false;
}
set.add(entityRef);
return true;
});
}, [entities, starredEntities]);
}

Some files were not shown because too many files have changed in this diff Show More