Added DevTools plugin

Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com>
This commit is contained in:
Andre Wanlin
2023-04-14 13:46:42 -05:00
parent 1ebb0a3944
commit 347aeca204
67 changed files with 3405 additions and 15 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-devtools': minor
'@backstage/plugin-devtools-backend': minor
'@backstage/plugin-devtools-common': minor
---
Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+1
View File
@@ -29,6 +29,7 @@
"@backstage/plugin-cloudbuild": "workspace:^",
"@backstage/plugin-code-coverage": "workspace:^",
"@backstage/plugin-cost-insights": "workspace:^",
"@backstage/plugin-devtools": "workspace:^",
"@backstage/plugin-dynatrace": "workspace:^",
"@backstage/plugin-entity-feedback": "workspace:^",
"@backstage/plugin-explore": "workspace:^",
+5
View File
@@ -109,6 +109,8 @@ import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
import { StackstormPage } from '@backstage/plugin-stackstorm';
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
import { DevToolsPage } from '@backstage/plugin-devtools';
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
const app = createApp({
apis,
@@ -291,6 +293,9 @@ const routes = (
<Route path="/score-board" element={<ScoreBoardPage />} />
<Route path="/stackstorm" element={<StackstormPage />} />
<Route path="/puppetdb" element={<PuppetDbPage />} />
<Route path="/devtools" element={<DevToolsPage />}>
{customDevToolsPage}
</Route>
</FlatRoutes>
);
@@ -52,6 +52,7 @@ import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import { useApp } from '@backstage/core-plugin-api';
import BuildIcon from '@material-ui/icons/Build';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -176,6 +177,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
to="/settings"
>
<SidebarSettings />
<SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
</SidebarGroup>
</Sidebar>
{children}
@@ -0,0 +1,44 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ConfigContent,
ExternalDependenciesContent,
InfoContent,
} from '@backstage/plugin-devtools';
import { DevToolsLayout } from '@backstage/plugin-devtools';
import React from 'react';
const DevToolsPage = () => {
return (
<DevToolsLayout>
<DevToolsLayout.Route path="info" title="Info">
<InfoContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route path="config" title="Config">
<ConfigContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route
path="external-dependencies"
title="External Dependencies"
>
<ExternalDependenciesContent />
</DevToolsLayout.Route>
</DevToolsLayout>
);
};
export const customDevToolsPage = <DevToolsPage />;
+1
View File
@@ -42,6 +42,7 @@
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-code-coverage-backend": "workspace:^",
"@backstage/plugin-devtools-backend": "workspace:^",
"@backstage/plugin-entity-feedback-backend": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
+3 -1
View File
@@ -64,6 +64,7 @@ import playlist from './plugins/playlist';
import adr from './plugins/adr';
import lighthouse from './plugins/lighthouse';
import linguist from './plugins/linguist';
import devTools from './plugins/devtools';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
@@ -158,8 +159,8 @@ async function main() {
const eventsEnv = useHotMemoize(module, () => createEnv('events'));
const exploreEnv = useHotMemoize(module, () => createEnv('explore'));
const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse'));
const linguistEnv = useHotMemoize(module, () => createEnv('linguist'));
const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -185,6 +186,7 @@ async function main() {
apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv));
apiRouter.use('/adr', await adr(adrEnv));
apiRouter.use('/linguist', await linguist(linguistEnv));
apiRouter.use('/devtools', await devTools(devToolsEnv));
apiRouter.use(notFoundHandler());
await lighthouse(lighthouseEnv);
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-devtools-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return createRouter({
logger: env.logger,
config: env.config,
permissions: env.permissions,
});
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+56
View File
@@ -0,0 +1,56 @@
# DevTools Backend
Welcome to the DevTools backend plugin! This plugin provides data for the [DevTools frontend](../devtools/) features.
## Setup
Here's how to get the DevTools Backend up and running:
1. First we need to add the `@backstage/plugin-devtools-backend` package to your backend:
```sh
# From the Backstage root directory
cd packages/backend
yarn add @backstage/plugin-devtools-backend
```
2. Then we will create a new file named `packages/backend/src/plugins/devtools.ts`, and add the
following to it:
```ts
import { createRouter } from '@backstage/plugin-devtools-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return createRouter({
logger: env.logger,
config: env.config,
permissions: env.permissions,
});
}
```
3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
```ts
import devTools from './plugins/devtools';
// ...
async function main() {
// ...
// Add this line under the other lines that follow the useHotMemoize pattern
const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
// ...
// Insert this line under the other lines that add their routers to apiRouter in the same way
apiRouter.use('/devtools', await devTools(devToolsEnv));
```
4. Now run `yarn start-backend` from the repo root
5. Finally open `http://localhost:7007/api/devtools/health` in a browser and it should return `{"status":"ok"}`
## Links
- [Frontend part of the plugin](../devtools/README.md)
- [The Backstage homepage](https://backstage.io)
+41
View File
@@ -0,0 +1,41 @@
## API Report File for "@backstage/plugin-devtools-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import { ConfigInfo } from '@backstage/plugin-devtools-common';
import { DevToolsInfo } from '@backstage/plugin-devtools-common';
import express from 'express';
import { ExternalDependency } from '@backstage/plugin-devtools-common';
import { Logger } from 'winston';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export class DevToolsBackendApi {
constructor(logger: Logger, config: Config);
// (undocumented)
listConfig(): Promise<ConfigInfo>;
// (undocumented)
listExternalDependencyDetails(): Promise<ExternalDependency[]>;
// (undocumented)
listInfo(): Promise<DevToolsInfo>;
}
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
config: Config;
// (undocumented)
devToolsBackendApi?: DevToolsBackendApi;
// (undocumented)
logger: Logger;
// (undocumented)
permissions: PermissionEvaluator;
}
// (No @packageDocumentation comment for this package)
```
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/**
* DevTools configuration.
*/
devTools?: {
/**
* External dependency configuration.
*/
externalDependencies?: {
/**
* The list of endpoints to check.
*/
endpoints?: Array<{
/**
* The name of the endpoint.
*/
name: string;
/**
* Type of check to perform; currently fetch or ping
*/
type: string;
/**
* The target of the endpoint; currently either a URL for fetch or server name for ping.
*/
target: string;
}>;
};
};
}
+63
View File
@@ -0,0 +1,63 @@
{
"name": "@backstage/plugin-devtools-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin"
},
"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",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/cli-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-devtools-common": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@types/express": "*",
"@yarnpkg/lockfile": "^1.1.0",
"@yarnpkg/parsers": "^3.0.0-rc.4",
"express": "^4.18.1",
"express-promise-router": "^4.1.0",
"fs-extra": "^10.0.0",
"lodash": "^4.17.21",
"node-fetch": "^2.6.7",
"ping": "^0.4.1",
"semver": "^7.3.2",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/minimist": "^1.2.0",
"@types/ping": "^0.4.1",
"@types/supertest": "^2.0.8",
"@types/yarnpkg__lockfile": "^1.1.4",
"msw": "^0.47.0",
"supertest": "^6.2.4"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -0,0 +1,252 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config, ConfigReader } from '@backstage/config';
import { loadConfigSchema } from '@backstage/config-loader';
import {
PackageDependency,
DevToolsInfo,
ExternalDependency,
Endpoint,
ExternalDependencyStatus,
ConfigInfo,
} from '@backstage/plugin-devtools-common';
import { JsonObject } from '@backstage/types';
import { Logger } from 'winston';
import fetch from 'node-fetch';
import { findPaths } from '@backstage/cli-common';
import { getPackages } from '@manypkg/get-packages';
import ping from 'ping';
import os from 'os';
import fs from 'fs-extra';
import { Lockfile } from '../util/Lockfile';
import { memoize } from 'lodash';
import { assertError } from '@backstage/errors';
/** @public */
export class DevToolsBackendApi {
public constructor(
private readonly logger: Logger,
private readonly config: Config,
) {}
public async listExternalDependencyDetails(): Promise<ExternalDependency[]> {
const result: ExternalDependency[] = [];
const endpoints = this.config.getOptional<Endpoint[]>(
'devTools.externalDependencies.endpoints',
);
if (!endpoints) {
// No external dependency endpoints configured
return result;
}
for (const endpoint of endpoints) {
this.logger?.info(
`Checking external dependency "${endpoint.name}" at "${endpoint.target}"`,
);
switch (endpoint.type) {
case 'ping': {
const pingResult = await this.pingExternalDependency(endpoint);
result.push(pingResult);
break;
}
case 'fetch': {
const fetchResult = await this.fetchExternalDependency(endpoint);
result.push(fetchResult);
break;
}
default:
return result;
}
}
return result;
}
private async fetchExternalDependency(
endpoint: Endpoint,
): Promise<ExternalDependency> {
let status;
let error;
await fetch(endpoint.target)
.then(res => {
status =
res.status === 200
? ExternalDependencyStatus.healthy
: ExternalDependencyStatus.unhealthy;
this.logger.info(
`Fetch for ${endpoint.name} resulted in status code "${res.status}"`,
);
})
.catch((err: Error) => {
this.logger.error(`Fetch failed for ${endpoint.name} - ${err.message}`);
error = err.message;
});
const result: ExternalDependency = {
name: endpoint.name,
type: endpoint.type,
target: endpoint.target,
status: status ?? ExternalDependencyStatus.unhealthy,
error: error ?? undefined,
};
return result;
}
private async pingExternalDependency(
endpoint: Endpoint,
): Promise<ExternalDependency> {
const pingResult = await ping.promise.probe(endpoint.target);
let error;
if (
pingResult.packetLoss === '100.000' ||
pingResult.packetLoss === 'unknown'
) {
this.logger.error(
`Ping failed for ${endpoint.name} - ${pingResult.output}`,
);
error =
pingResult.output === ''
? `${endpoint.target} - Unknown`
: pingResult.output;
}
this.logger.info(`Ping results for ${endpoint.name}: ${pingResult.output}`);
const result: ExternalDependency = {
name: endpoint.name,
type: endpoint.type,
target: endpoint.target,
status: pingResult.alive
? ExternalDependencyStatus.healthy
: ExternalDependencyStatus.unhealthy,
error: error ?? undefined,
};
return result;
}
public async listConfig(): Promise<ConfigInfo> {
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
const { packages } = await getPackages(paths.targetDir);
const schemaFunc = async () => {
return await loadConfigSchema({
dependencies: packages.map(p => p.packageJson.name),
});
};
const schemaMemo = memoize(schemaFunc);
const schema = await schemaMemo();
const configInfo: ConfigInfo = {
config: undefined,
error: undefined,
};
try {
const config = {
data: this.config.get() as JsonObject,
context: 'inline',
};
const sanitizedConfigs = schema.process([config], {
ignoreSchemaErrors: false,
valueTransform: (value, context) =>
context.visibility === 'secret' ? '<secret>' : value,
});
const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
configInfo.config = data;
} catch (error) {
assertError(error);
// The config is not valid for some reason but we want to be able to see it still
const config = {
data: this.config.get() as JsonObject,
context: 'inline',
};
const sanitizedConfigs = schema.process([config], {
ignoreSchemaErrors: true,
valueTransform: (value, context) =>
context.visibility === 'secret' ? '<secret>' : value,
});
const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
configInfo.config = data;
configInfo.error = {
name: error.name,
message: error.message,
messages: error.messages as string[] | undefined,
stack: error.stack,
};
}
return configInfo;
}
public async listInfo(): Promise<DevToolsInfo> {
const operatingSystem = `${os.type} ${os.release} - ${os.platform}/${os.arch}`;
const nodeJsVersion = process.version;
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
const backstageFile = paths.resolveTargetRoot('backstage.json');
let backstageJson = undefined;
if (fs.existsSync(backstageFile)) {
const buffer = await fs.readFile(backstageFile);
backstageJson = JSON.parse(buffer.toString());
}
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/'));
const infoDependencies: PackageDependency[] = [];
for (const dep of deps) {
const versions = new Set(lockfile.get(dep)!.map(i => i.version));
const infoDependency: PackageDependency = {
name: dep,
versions: [...versions].join(', '),
};
infoDependencies.push(infoDependency);
}
const info: DevToolsInfo = {
operatingSystem: operatingSystem ?? 'N/A',
nodeJsVersion: nodeJsVersion ?? 'N/A',
backstageVersion:
backstageJson && backstageJson.version ? backstageJson.version : 'N/A',
dependencies: infoDependencies,
};
return info;
}
}
export function isValidUrl(url: string): boolean {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch {
return false;
}
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DevToolsBackendApi } from './DevToolsBackendApi';
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DevToolsBackendApi } from './api';
export * from './service/router';
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,69 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { createRouter } from './router';
const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
jest.fn();
const mockedPermissionQuery: jest.MockedFunction<
PermissionEvaluator['authorizeConditional']
> = jest.fn();
const permissionEvaluator: PermissionEvaluator = {
authorize: mockedAuthorize,
authorizeConditional: mockedPermissionQuery,
};
describe('createRouter', () => {
let app: express.Express;
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
config: new ConfigReader({
healthCheck: {
endpoint: [
{
name: '',
type: '',
target: '',
},
],
},
}),
permissions: permissionEvaluator,
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /health', () => {
it('returns ok', async () => {
const response = await request(app).get('/health');
expect(response.status).toEqual(200);
expect(response.body).toEqual({ status: 'ok' });
});
});
});
@@ -0,0 +1,130 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AuthorizeResult,
PermissionEvaluator,
} from '@backstage/plugin-permission-common';
import {
devToolsConfigReadPermission,
devToolsExternalDependenciesReadPermission,
devToolsInfoReadPermission,
} from '@backstage/plugin-devtools-common';
import { Config } from '@backstage/config';
import { DevToolsBackendApi } from '../api';
import { Logger } from 'winston';
import { NotAllowedError } from '@backstage/errors';
import Router from 'express-promise-router';
import { errorHandler } from '@backstage/backend-common';
import express from 'express';
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
/** @public */
export interface RouterOptions {
devToolsBackendApi?: DevToolsBackendApi;
logger: Logger;
config: Config;
permissions: PermissionEvaluator;
}
/** @public */
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, config, permissions } = options;
const devToolsBackendApi =
options.devToolsBackendApi || new DevToolsBackendApi(logger, config);
const router = Router();
router.use(express.json());
router.get('/health', (_req, res) => {
res.status(200).json({ status: 'ok' });
});
router.get('/info', async (req, response) => {
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const decision = (
await permissions.authorize(
[{ permission: devToolsInfoReadPermission }],
{
token,
},
)
)[0];
if (decision.result === AuthorizeResult.DENY) {
throw new NotAllowedError('Unauthorized');
}
const info = await devToolsBackendApi.listInfo();
response.status(200).json(info);
});
router.get('/config', async (req, response) => {
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const decision = (
await permissions.authorize(
[{ permission: devToolsConfigReadPermission }],
{
token,
},
)
)[0];
if (decision.result === AuthorizeResult.DENY) {
throw new NotAllowedError('Unauthorized');
}
const configList = await devToolsBackendApi.listConfig();
response.status(200).json(configList);
});
router.get('/external-dependencies', async (req, response) => {
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const decision = (
await permissions.authorize(
[{ permission: devToolsExternalDependenciesReadPermission }],
{
token,
},
)
)[0];
if (decision.result === AuthorizeResult.DENY) {
throw new NotAllowedError('Unauthorized');
}
const health = await devToolsBackendApi.listExternalDependencyDetails();
response.status(200).json(health);
});
router.use(errorHandler());
return router;
}
@@ -0,0 +1,68 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ServerTokenManager,
SingleHostDiscovery,
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { Logger } from 'winston';
import { Server } from 'http';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'devtools-backend-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
const tokenManager = ServerTokenManager.fromConfig(config, {
logger,
});
const permissions = ServerPermissionClient.fromConfig(config, {
discovery,
tokenManager,
});
logger.debug('Starting application server...');
const router = await createRouter({
logger,
config,
permissions,
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/devtools-backend', router);
if (options.enableCors) {
service = service.enableCors({ origin: 'http://localhost:3000' });
}
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
@@ -0,0 +1,317 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import semver from 'semver';
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
type LockfileData = {
[entry: string]: {
version: string;
resolved?: string;
integrity?: string;
dependencies?: { [name: string]: string };
};
};
type LockfileQueryEntry = {
range: string;
version: string;
};
/** Entries that have an invalid version range, for example an npm tag */
type AnalyzeResultInvalidRange = {
name: string;
range: string;
};
/** Entries that can be deduplicated by bumping to an existing higher version */
type AnalyzeResultNewVersion = {
name: string;
range: string;
oldVersion: string;
newVersion: string;
};
/** Entries that would need a dependency update in package.json to be deduplicated */
type AnalyzeResultNewRange = {
name: string;
oldRange: string;
newRange: string;
oldVersion: string;
newVersion: string;
};
type AnalyzeResult = {
invalidRanges: AnalyzeResultInvalidRange[];
newVersions: AnalyzeResultNewVersion[];
newRanges: AnalyzeResultNewRange[];
};
function parseLockfile(lockfileContents: string) {
try {
return {
object: parseSyml(lockfileContents),
type: 'success',
};
} catch (err) {
return {
object: null,
type: err,
};
}
}
// the new yarn header is handled out of band of the parsing
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
const NEW_HEADER = `${[
`# This file is generated by running "yarn install" inside your project.\n`,
`# Manual changes might be lost - proceed with caution!\n`,
].join(``)}\n`;
function stringifyLockfile(data: LockfileData, legacy: boolean) {
return legacy
? legacyStringifyLockfile(data)
: NEW_HEADER + stringifySyml(data);
}
// taken from yarn parser package
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
// these are special top level yarn keys.
// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9
const SPECIAL_OBJECT_KEYS = [
`__metadata`,
`version`,
`resolution`,
`dependencies`,
`peerDependencies`,
`dependenciesMeta`,
`peerDependenciesMeta`,
`binaries`,
];
export class Lockfile {
static async load(path: string) {
const lockfileContents = await fs.readFile(path, 'utf8');
const legacy = LEGACY_REGEX.test(lockfileContents);
const lockfile = parseLockfile(lockfileContents);
if (lockfile.type !== 'success') {
throw new Error(`Failed yarn.lock parse with ${lockfile.type}`);
}
const data = lockfile.object as LockfileData;
const packages = new Map<string, LockfileQueryEntry[]>();
for (const [key, value] of Object.entries(data)) {
if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
const [, name, range] = ENTRY_PATTERN.exec(key) ?? [];
if (!name) {
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
}
let queries = packages.get(name);
if (!queries) {
queries = [];
packages.set(name, queries);
}
queries.push({ range, version: value.version });
}
return new Lockfile(path, packages, data, legacy);
}
private constructor(
private readonly path: string,
private readonly packages: Map<string, LockfileQueryEntry[]>,
private readonly data: LockfileData,
private readonly legacy: boolean = false,
) {}
/** Get the entries for a single package in the lockfile */
get(name: string): LockfileQueryEntry[] | undefined {
return this.packages.get(name);
}
/** Returns the name of all packages available in the lockfile */
keys(): IterableIterator<string> {
return this.packages.keys();
}
/** Analyzes the lockfile to identify possible actions and warnings for the entries */
analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult {
const { filter } = options ?? {};
const result: AnalyzeResult = {
invalidRanges: [],
newVersions: [],
newRanges: [],
};
for (const [name, allEntries] of this.packages) {
if (filter && !filter(name)) {
continue;
}
// Get rid of and signal any invalid ranges upfront
const invalid = allEntries.filter(e => !semver.validRange(e.range));
result.invalidRanges.push(
...invalid.map(({ range }) => ({ name, range })),
);
// Grab all valid entries, if there aren't at least 2 different valid ones we're done
const entries = allEntries.filter(e => semver.validRange(e.range));
if (entries.length < 2) {
continue;
}
// Find all versions currently in use
const versions = Array.from(new Set(entries.map(e => e.version))).sort(
(v1, v2) => semver.rcompare(v1, v2),
);
// If we're not using at least 2 different versions we're done
if (versions.length < 2) {
continue;
}
const acceptedVersions = new Set<string>();
for (const { version, range } of entries) {
// Finds the highest matching version from the the known versions
// TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one
const acceptedVersion = versions.find(v => semver.satisfies(v, range));
if (!acceptedVersion) {
throw new Error(
`No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
);
}
if (acceptedVersion !== version) {
result.newVersions.push({
name,
range,
newVersion: acceptedVersion,
oldVersion: version,
});
}
acceptedVersions.add(acceptedVersion);
}
// If all ranges were able to accept the same version, we're done
if (acceptedVersions.size === 1) {
continue;
}
// Find the max version that we may want bump older packages to
const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0];
// Find all existing ranges that satisfy the new max version, and pick the one that
// results in the highest minimum allowed version, usually being the more specific one
const maxEntry = entries
.filter(e => semver.satisfies(maxVersion, e.range))
.map(e => ({ e, min: semver.minVersion(e.range) }))
.filter(p => p.min)
.sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e;
if (!maxEntry) {
throw new Error(
`No entry found that satisfies max version '${maxVersion}'`,
);
}
// Find all entries that don't satisfy the max version
for (const { version, range } of entries) {
if (semver.satisfies(maxVersion, range)) {
continue;
}
result.newRanges.push({
name,
oldRange: range,
newRange: maxEntry.range,
oldVersion: version,
newVersion: maxVersion,
});
}
}
return result;
}
remove(name: string, range: string): boolean {
const query = `${name}@${range}`;
const existed = Boolean(this.data[query]);
delete this.data[query];
const newEntries = this.packages.get(name)?.filter(e => e.range !== range);
if (newEntries) {
this.packages.set(name, newEntries);
}
return existed;
}
/** Modifies the lockfile by bumping packages to the suggested versions */
replaceVersions(results: AnalyzeResultNewVersion[]) {
for (const { name, range, oldVersion, newVersion } of results) {
const query = `${name}@${range}`;
// Update the backing data
const entryData = this.data[query];
if (!entryData) {
throw new Error(`No entry data for ${query}`);
}
if (entryData.version !== oldVersion) {
throw new Error(
`Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
);
}
// Modifying the data in the entry is not enough, we need to reference an existing version object
const matchingEntry = Object.entries(this.data).find(
([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion,
);
if (!matchingEntry) {
throw new Error(
`No matching entry found for ${name} at version ${newVersion}`,
);
}
this.data[query] = matchingEntry[1];
// Update our internal data structure
const entry = this.packages.get(name)?.find(e => e.range === range);
if (!entry) {
throw new Error(`No entry data for ${query}`);
}
if (entry.version !== oldVersion) {
throw new Error(
`Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
);
}
entry.version = newVersion;
}
}
async save() {
await fs.writeFile(this.path, this.toString(), 'utf8');
}
toString() {
return stringifyLockfile(this.data, this.legacy);
}
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+3
View File
@@ -0,0 +1,3 @@
# DevTools Common
Common types and permissions for the DevTools plugin.
+74
View File
@@ -0,0 +1,74 @@
## API Report File for "@backstage/plugin-devtools-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BasicPermission } from '@backstage/plugin-permission-common';
import { JsonValue } from '@backstage/types';
// @public (undocumented)
export type ConfigError = {
name: string;
message: string;
messages?: string[];
stack?: string;
};
// @public (undocumented)
export type ConfigInfo = {
config?: JsonValue;
error?: ConfigError;
};
// @public (undocumented)
export const devToolsAdministerPermission: BasicPermission;
// @public (undocumented)
export const devToolsConfigReadPermission: BasicPermission;
// @public (undocumented)
export const devToolsExternalDependenciesReadPermission: BasicPermission;
// @public (undocumented)
export type DevToolsInfo = {
operatingSystem: string;
nodeJsVersion: string;
backstageVersion: string;
dependencies: PackageDependency[];
};
// @public (undocumented)
export const devToolsInfoReadPermission: BasicPermission;
// @public (undocumented)
export type Endpoint = {
name: string;
type: string;
target: string;
};
// @public (undocumented)
export type ExternalDependency = {
name: string;
type: string;
target: string;
status: string;
error?: string;
};
// @public (undocumented)
export enum ExternalDependencyStatus {
// (undocumented)
healthy = 'Healthy',
// (undocumented)
unhealthy = 'Unhealthy',
}
// @public (undocumented)
export type PackageDependency = {
name: string;
versions: string;
};
// (No @packageDocumentation comment for this package)
```
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@backstage/plugin-devtools-common",
"description": "Common functionalities for the devtools plugin",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "common-library"
},
"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/plugin-permission-common": "workspace:^",
"@backstage/types": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
]
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types';
export * from './permissions';
@@ -0,0 +1,49 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPermission } from '@backstage/plugin-permission-common';
/**
* @public
*/
export const devToolsAdministerPermission = createPermission({
name: 'devtools.administer',
attributes: { action: 'read' },
});
/**
* @public
*/
export const devToolsInfoReadPermission = createPermission({
name: 'devtools.info',
attributes: { action: 'read' },
});
/**
* @public
*/
export const devToolsConfigReadPermission = createPermission({
name: 'devtools.config',
attributes: { action: 'read' },
});
/**
* @public
*/
export const devToolsExternalDependenciesReadPermission = createPermission({
name: 'devtools.external-dependencies',
attributes: { action: 'read' },
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
+67
View File
@@ -0,0 +1,67 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonValue } from '@backstage/types';
/** @public */
export type Endpoint = {
name: string;
type: string;
target: string;
};
/** @public */
export type ExternalDependency = {
name: string;
type: string;
target: string;
status: string;
error?: string;
};
/** @public */
export type DevToolsInfo = {
operatingSystem: string;
nodeJsVersion: string;
backstageVersion: string;
dependencies: PackageDependency[];
};
/** @public */
export type PackageDependency = {
name: string;
versions: string;
};
/** @public */
export enum ExternalDependencyStatus {
healthy = 'Healthy',
unhealthy = 'Unhealthy',
}
/** @public */
export type ConfigInfo = {
config?: JsonValue;
error?: ConfigError;
};
/** @public */
export type ConfigError = {
name: string;
message: string;
messages?: string[];
stack?: string;
};
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+375
View File
@@ -0,0 +1,375 @@
# DevTools
Welcome to the DevTools plugin!
## Features
The DevTools plugin comes with two tabs out of the box.
### Info
Lists helpful information about your current running Backstage instance such as: OS, NodeJS version, Backstage version, and package versions.
![Example of Info tab](./docs/devtools-info-tab.png)
### Config
Lists the configuration being used by your current running Backstage instance.
![Example of Config tab](./docs/devtools-config-tab.png)
## Optional Features
The DevTools plugin can be setup with other tabs with additional helpful features.
### External Dependencies
Lists the status of configured External Dependencies based on your current running Backstage instance's ability to reach them
![Example of external dependencies tab](./docs/devtools-external-dependencies.png)
## Setup
The following sections will help you get the DevTools plugin setup and running.
### Backend
You need to setup the [DevTools backend plugin](../devtools-backend/README.md) before you move forward with any of the following steps if you haven't already.
### Frontend
To setup the DevTools frontend you'll need to do the following steps:
1. First we need to add the `@backstage/plugin-devtools` package to your frontend app:
```sh
# From your Backstage root directory
yarn add --cwd packages/app @backstage/plugin-devtools
```
2. Now open the `packages/app/src/App.tsx` file
3. Then after all the import statements add the following line:
```ts
import { DevToolsPage } from '@backstage/plugin-devtools';
```
4. In this same file just before the closing `</ FlatRoutes>`, this will be near the bottom of the file, add this line:
```ts
<Route path="/devtools" element={<DevToolsPage />} />
```
5. Next open the `packages/app/src/components/Root/Root.tsx` file
6. We want to add this icon import after all the existing import statements:
```ts
import BuildIcon from '@material-ui/icons/Build';
```
7. Then add this line just after the `<SidebarSettings />` line:
```ts
<SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
```
8. Now run `yarn dev` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info)
## Customizing
The DevTools plugin has been designed so that you can customize the tabs to suite your needs. You may only want some or none of the out of the box tabs or you may want to add your own. The following sections explains how to do that (assuming you've already done the [setup steps](#setup)). As part of this example we'll also be showing how you can add the optional [External Dependencies](#external-dependencies) tab.
1. In the `packages/app/src/components` folder create a new sub-folder called `devtools`
2. Then in this new `devtools` folder add a file called `CustomDevToolsPage.tsx`
3. In the `CustomDevToolsPage.tsx` file add the following content:
```tsx
import {
ConfigContent,
ExternalDependenciesContent,
InfoContent,
} from '@backstage/plugin-devtools';
import { DevToolsLayout } from '@backstage/plugin-devtools';
import React from 'react';
export const DevToolsPage = () => {
return (
<DevToolsLayout>
<DevToolsLayout.Route path="info" title="Info">
<InfoContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route path="config" title="Config">
<ConfigContent />
</DevToolsLayout.Route>
<DevToolsLayout.Route
path="external-dependencies"
title="External Dependencies"
>
<ExternalDependenciesContent />
</DevToolsLayout.Route>
</DevToolsLayout>
);
};
export const customDevToolsPage = <DevToolsPage />;
```
4. Now open the `packages/app/src/App.tsx` file and add the following import after all the existing import statements:
```ts
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
```
5. Then we need to adjust our route as follows
```diff
- <Route path="/devtools" element={<DevToolsPage />} />
+ <Route path="/devtools" element={<DevToolsPage />} >
+ {customDevToolsPage}
+ </Route>
```
6. Now run `yarn dev` from the root of your project. When you go to the DevTools you'll now see you have a third tab for [External Dependencies](#external-dependencies)
With this setup you can add or remove the tabs as you'd like or add your own simply by editing your `CustomDevToolsPage.tsx` file
## Permissions
The DevTools plugin supports the [permissions framework](https://backstage.io/docs/permissions/overview), the following sections outline how you can use them with the assumption that you have the permissions framework setup and working.
**Note:** These sections are intended as guidance and are completely optional. The DevTools plugin will work with the permission framework off or on without any specific policy setup.
### Secure Sidebar Option
To use the permission framework to secure the DevTools sidebar option you'll want to do the following:
1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app:
```sh
# From your Backstage root directory
yarn add --cwd packages/app @backstage/plugin-devtools
```
2. Then open the `packages/app/src/components/Root/Root.tsx` file
3. The add these imports after all the existing import statements:
```ts
import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common';
import { RequirePermission } from '@backstage/plugin-permission-react';
```
4. Then make the following change:
```diff
- <SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
+ <RequirePermission
+ permission={devToolsAdministerPermission}
+ errorPage={<></>}>
+ <SidebarItem icon={BuildIcon} to="devtools" text="DevTools" />
+ </RequirePermission>
```
### Secure the DevTools Route
To use the permission framework to secure the DevTools route you'll want to do the following:
1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app (skip this step if you've already done this):
```sh
# From your Backstage root directory
yarn add --cwd packages/app @backstage/plugin-devtools-common
```
2. Then open the `packages/app/src/App.tsx` file
3. The add this import after all the existing import statements:
```ts
import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common';
```
4. Then make the following change:
```diff
- <Route path="/devtools" element={<DevToolsPage />} />
+ <Route path="/devtools"
+ element={
+ <RequirePermission permission={devToolsAdministerPermission}>
+ <DevToolsPage />
+ </RequirePermission>
+ }
+ />
```
Note: if you are using a `customDevToolsPage` as per the [Customizing](#customizing) documentation the changes for Step 4 will be:
```diff
- <Route path="/devtools" element={<DevToolsPage />} />
+ <Route path="/devtools"
+ element={
+ <RequirePermission permission={devToolsAdministerPermission}>
+ <DevToolsPage />
+ </RequirePermission>
+ }
+ >
+ {customDevToolsPage}
+ </Route>
```
### Permission Policy
Here is an example permission policy that you might use to secure the DevTools plugin:
```ts
// packages/backend/src/plugins/permission.ts
class TestPermissionPolicy implements PermissionPolicy {
async handle(request: PolicyQuery): Promise<PolicyDecision> {
if (isPermission(request.permission, devToolsAdministerPermission)) {
if (
user?.identity.ownershipEntityRefs.includes(
'group:default/backstage-admins',
)
) {
return { result: AuthorizeResult.ALLOW };
}
return { result: AuthorizeResult.DENY };
}
if (isPermission(request.permission, devToolsInfoReadPermission)) {
if (
user?.identity.ownershipEntityRefs.includes(
'group:default/backstage-admins',
)
) {
return { result: AuthorizeResult.ALLOW };
}
return { result: AuthorizeResult.DENY };
}
if (isPermission(request.permission, devToolsConfigReadPermission)) {
if (
user?.identity.ownershipEntityRefs.includes(
'group:default/backstage-admins',
)
) {
return { result: AuthorizeResult.ALLOW };
}
return { result: AuthorizeResult.DENY };
}
if (
isPermission(
request.permission,
devToolsExternalDependenciesReadPermission,
)
) {
if (
user?.identity.ownershipEntityRefs.includes(
'group:default/backstage-admins',
)
) {
return { result: AuthorizeResult.ALLOW };
}
return { result: AuthorizeResult.DENY };
}
return { result: AuthorizeResult.ALLOW };
}
}
```
To use this policy you'll need to make sure to add the `@backstage/plugin-devtools-common` package to your backend you can do that by running this command:
```sh
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-devtools-common
```
You'll also need to add these imports:
```ts
import {
devToolsAdministerPermission,
devToolsConfigReadPermission,
devToolsExternalDependenciesReadPermission,
devToolsInfoReadPermission,
} from '@backstage/plugin-devtools-common';
```
**Note:** The group "group:default/backstage-admins" is simply an example and does not exist. You can point this to any group you have in your catalog instead.
### Customizing with Permissions
If you followed the [Customizing](#customizing) documentation and want to use permission there this is what your `CustomDevToolsPage.tsx` would look like:
```tsx
import {
ConfigContent,
ExternalDependenciesContent,
InfoContent,
} from '@backstage/plugin-devtools';
import { DevToolsLayout } from '@backstage/plugin-devtools';
import {
devToolsConfigReadPermission,
devToolsExternalDependenciesReadPermission,
devToolsInfoReadPermission,
} from '@backstage/plugin-devtools-common';
import { RequirePermission } from '@backstage/plugin-permission-react';
import React from 'react';
const DevToolsPage = () => {
return (
<DevToolsLayout>
<DevToolsLayout.Route path="info" title="Info">
<RequirePermission permission={devToolsInfoReadPermission}>
<InfoContent />
</RequirePermission>
</DevToolsLayout.Route>
<DevToolsLayout.Route path="config" title="Config">
<RequirePermission permission={devToolsConfigReadPermission}>
<ConfigContent />
</RequirePermission>
</DevToolsLayout.Route>
<DevToolsLayout.Route
path="external-dependencies"
title="External Dependencies"
>
<RequirePermission
permission={devToolsExternalDependenciesReadPermission}
>
<ExternalDependenciesContent />
</RequirePermission>
</DevToolsLayout.Route>
</DevToolsLayout>
);
};
export const customDevToolsPage = <DevToolsPage />;
```
## Configuration
The following sections outline the configuration for the DevTools plugin
### External Dependencies Configuration
If you decide to use the External Dependencies tab then you'll need to setup the configuration for it in your `app-config.yaml`, if there is no config setup then the tab will be empty. Here's an example:
```yaml
devTools:
externalDependencies:
endpoints:
- name: 'Google'
type: 'fetch'
target: 'https://google.ca'
- name: 'Google Public DNS'
type: 'ping'
target: '8.8.8.8'
```
Configuration details:
- `endpoints` is an array
- `name` is the friendly name for your endpoint
- `type` can be either `ping` or `fetch` and will perform the respective action on the `target`
- `target` is either a URL or server that you want to trigger a `type` action on
+59
View File
@@ -0,0 +1,59 @@
## API Report File for "@backstage/plugin-devtools"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { default as default_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { TabProps } from '@material-ui/core';
// @public (undocumented)
export const ConfigContent: () => JSX.Element;
// @public
export const DevToolsLayout: {
({ children }: DevToolsLayoutProps): JSX.Element;
Route: (props: SubRoute) => null;
};
// @public (undocumented)
export type DevToolsLayoutProps = {
children?: default_2.ReactNode;
};
// @public (undocumented)
export const DevToolsPage: () => JSX.Element;
// @public (undocumented)
export const devToolsPlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
},
{},
{}
>;
// @public (undocumented)
export const ExternalDependenciesContent: () => JSX.Element;
// @public (undocumented)
export const InfoContent: () => JSX.Element;
// @public (undocumented)
export type SubRoute = {
path: string;
title: string;
children: JSX.Element;
tabProps?: TabProps<
default_2.ElementType,
{
component?: default_2.ElementType;
}
>;
};
// (No @packageDocumentation comment for this package)
```
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { devToolsPlugin, DevToolsPage } from '../src/plugin';
createDevApp()
.registerPlugin(devToolsPlugin)
.addPage({
element: <DevToolsPage />,
title: 'Root Page',
path: '/devtools',
})
.render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

+64
View File
@@ -0,0 +1,64 @@
{
"name": "@backstage/plugin-devtools",
"version": "0.0.0",
"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"
},
"backstage": {
"role": "frontend-plugin"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/devtools"
},
"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",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-devtools-common": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"react-json-view": "^1.21.3",
"react-use": "^17.2.4"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
"react": "^16.13.1 || ^17.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.47.0"
},
"files": [
"dist"
]
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core-plugin-api';
import {
ConfigInfo,
DevToolsInfo,
ExternalDependency,
} from '@backstage/plugin-devtools-common';
export const devToolsApiRef = createApiRef<DevToolsApi>({
id: 'plugin.devtools.service',
});
export interface DevToolsApi {
getConfig(): Promise<ConfigInfo | undefined>;
getExternalDependencies(): Promise<ExternalDependency[] | undefined>;
getInfo(): Promise<DevToolsInfo | undefined>;
}
@@ -0,0 +1,78 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import {
ConfigInfo,
DevToolsInfo,
ExternalDependency,
} from '@backstage/plugin-devtools-common';
import { ResponseError } from '@backstage/errors';
import { DevToolsApi } from './DevToolsApi';
export class DevToolsClient implements DevToolsApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
public constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
}
public async getConfig(): Promise<ConfigInfo | undefined> {
const urlSegment = 'config';
const configInfo = await this.get<ConfigInfo | undefined>(urlSegment);
return configInfo;
}
public async getExternalDependencies(): Promise<
ExternalDependency[] | undefined
> {
const urlSegment = 'external-dependencies';
const externalDependencies = await this.get<
ExternalDependency[] | undefined
>(urlSegment);
return externalDependencies;
}
public async getInfo(): Promise<DevToolsInfo | undefined> {
const urlSegment = 'info';
const info = await this.get<DevToolsInfo | undefined>(urlSegment);
return info;
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('devtools')}/`;
const url = new URL(path, baseUrl);
const { token } = await this.identityApi.getCredentials();
const response = await fetch(url.toString(), {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<T>;
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './DevToolsApi';
export * from './DevToolsClient';
@@ -0,0 +1,95 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Progress, WarningPanel } from '@backstage/core-components';
import {
Box,
createStyles,
makeStyles,
Paper,
Theme,
Typography,
useTheme,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import ReactJson from 'react-json-view';
import { useConfig } from '../../../hooks';
import { ConfigError } from '@backstage/plugin-devtools-common';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
warningStyle: {
paddingBottom: theme.spacing(2),
},
paperStyle: {
padding: theme.spacing(2),
},
}),
);
export const WarningContent = ({ error }: { error: ConfigError }) => {
if (!error.messages) {
return <Typography>{error.message}</Typography>;
}
const messages = error.messages as string[];
return (
<Box>
{messages.map(message => (
<Typography>{message}</Typography>
))}
</Box>
);
};
/** @public */
export const ConfigContent = () => {
const classes = useStyles();
const theme = useTheme();
const { configInfo, loading, error } = useConfig();
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (!configInfo) {
return <Alert severity="error">Unable to load config data</Alert>;
}
return (
<Box>
{configInfo && configInfo.error && (
<Box className={classes.warningStyle}>
<WarningPanel title="Config validation failed">
<WarningContent error={configInfo.error} />
</WarningPanel>
</Box>
)}
<Paper className={classes.paperStyle}>
<ReactJson
src={configInfo.config as object}
name="config"
enableClipboard={false}
theme={theme.palette.type === 'dark' ? 'monokai' : 'rjv-default'}
/>
</Paper>
</Box>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ConfigContent } from './ConfigContent';
@@ -0,0 +1,140 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Progress,
StatusError,
StatusOK,
StatusWarning,
Table,
TableColumn,
} from '@backstage/core-components';
import { ExternalDependency } from '@backstage/plugin-devtools-common';
import {
Box,
createStyles,
Grid,
makeStyles,
Paper,
Theme,
Typography,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { useExternalDependencies } from '../../../hooks';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
paperStyle: {
padding: theme.spacing(2),
},
}),
);
export const getExternalDependencyStatus = (
result: Partial<ExternalDependency> | undefined,
) => {
switch (result?.status) {
case 'Healthy':
return (
<Typography component="span">
<StatusOK /> {result.status}
</Typography>
);
case 'Unhealthy':
return (
<Typography component="span">
<StatusError /> {`${result.status}`}
</Typography>
);
case undefined:
default:
return (
<Typography component="span">
<StatusWarning /> Unknown
</Typography>
);
}
};
const columns: TableColumn[] = [
{
title: 'Name',
width: 'auto',
field: 'name',
},
{
title: 'Target',
width: 'auto',
field: 'target',
},
{
title: 'Type',
width: 'auto',
field: 'type',
},
{
title: 'Status',
width: 'auto',
render: (row: Partial<ExternalDependency>) => (
<Grid container direction="column">
<Grid item>
<Typography variant="button">
{getExternalDependencyStatus(row)}
</Typography>
</Grid>
<Grid item>{row.error && <Typography>{row.error}</Typography>}</Grid>
</Grid>
),
},
];
/** @public */
export const ExternalDependenciesContent = () => {
const classes = useStyles();
const { externalDependencies, loading, error } = useExternalDependencies();
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (!externalDependencies || externalDependencies.length === 0) {
return (
<Box>
<Paper className={classes.paperStyle}>
<Typography>No external dependencies found</Typography>
</Paper>
</Box>
);
}
return (
<Table
title="Status"
options={{
paging: true,
pageSize: 20,
pageSizeOptions: [20, 50, 100],
loadingType: 'linear',
showEmptyDataSourceMessage: !loading,
}}
columns={columns}
data={externalDependencies || []}
/>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ExternalDependenciesContent } from './ExternalDependenciesContent';
@@ -0,0 +1,25 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SvgIcon, SvgIconProps } from '@material-ui/core';
import React from 'react';
export const BackstageLogoIcon = (props: SvgIconProps) => (
<SvgIcon {...props} viewBox="0 0 337.46 428.5">
<path d="M303 166.05a80.69 80.69 0 0013.45-10.37c.79-.77 1.55-1.53 2.3-2.3a83.12 83.12 0 007.93-9.38 63.69 63.69 0 006.32-10.77 48.58 48.58 0 004.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 0057.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 00-6.8-8.87 89.17 89.17 0 0019.32 2.15h.15a85.08 85.08 0 0031-5.79 80.88 80.88 0 0012.85-6.45zm-100.55 59.81c-19.32 18.51-50.4 21.23-75.7 5.9l-75.14-45.61 67.45-64.64 76.41 46.38c27.53 16.69 26.02 39.72 6.98 57.97zm8.93-82.22l-70.65-42.89L205.14 39l69.37 42.1c25.94 15.72 29.31 37 10.55 55a60.69 60.69 0 01-73.68 7.54zm29.86 190c-19.57 18.75-46.17 29.09-74.88 29.09a123.73 123.73 0 01-64.1-18.2L0 282.52v24.67l108.6 65.91a111.6 111.6 0 0057.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 01-11.22 13.13zm0-39c-19.57 18.75-46.17 29.08-74.88 29.08a123.81 123.81 0 01-64.1-18.19L0 243.53v24.68l108.6 65.91a111.6 111.6 0 0057.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 01-11.22 13.13zm0-39c-19.57 18.76-46.17 29.09-74.88 29.09a123.81 123.81 0 01-64.1-18.19L0 204.55v24.68l108.6 65.91a111.59 111.59 0 0057.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 01-11.22 13.16zm83.7 25.74a94.15 94.15 0 01-60.2 25.86V334a81.6 81.6 0 0051.74-22.37c14-13.38 21.14-28.11 21-42.64v-2.19a94.92 94.92 0 01-12.54 14.65zm-83.7 91.21c-19.57 18.76-46.17 29.09-74.88 29.09a123.73 123.73 0 01-64.1-18.2L0 321.5v24.68l108.6 65.9a111.6 111.6 0 0057.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 01-11.22 13.13zM327 162.45c-.68.69-1.35 1.38-2.05 2.06a94.37 94.37 0 01-10.64 8.65 91.35 91.35 0 01-11.6 7 94.53 94.53 0 01-26.24 8.71 97.69 97.69 0 01-14.16 1.57c.5 1.61.9 3.25 1.25 4.9a53.27 53.27 0 011.14 12V217h.05a84.41 84.41 0 0025.35-5.55 81 81 0 0026.39-16.82c.8-.77 1.5-1.56 2.26-2.34a82.08 82.08 0 007.93-9.38 63.76 63.76 0 006.32-10.74 48.55 48.55 0 004.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08 1.54-2.25 3.09a96.73 96.73 0 01-8.26 9.36zm0 77.92c-.69.7-1.31 1.41-2 2.1a94.2 94.2 0 01-60.2 25.86V295a81.6 81.6 0 0051.74-22.37 73.51 73.51 0 0016.46-22.5 48.56 48.56 0 004.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 01-8.24 9.37zm0-39c-.69.7-1.31 1.41-2 2.1a93.18 93.18 0 01-10.63 8.65 91.63 91.63 0 01-11.63 7 95.47 95.47 0 01-37.94 10.18V256a81.65 81.65 0 0051.74-22.37c.8-.77 1.5-1.56 2.26-2.34a82.08 82.08 0 007.93-9.38 63.76 63.76 0 006.27-10.76 48.56 48.56 0 004.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 01-8.24 9.37z" />
</SvgIcon>
);
@@ -0,0 +1,138 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Progress } from '@backstage/core-components';
import {
Avatar,
Box,
createStyles,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
makeStyles,
Paper,
Theme,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { useInfo } from '../../../hooks';
import { InfoDependenciesTable } from './InfoDependenciesTable';
import DescriptionIcon from '@material-ui/icons/Description';
import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard';
import { BackstageLogoIcon } from './BackstageLogoIcon';
import FileCopyIcon from '@material-ui/icons/FileCopy';
import { DevToolsInfo } from '@backstage/plugin-devtools-common';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
paperStyle: {
marginBottom: theme.spacing(2),
},
flexContainer: {
display: 'flex',
flexDirection: 'row',
padding: 0,
},
copyButton: {
float: 'left',
margin: theme.spacing(2),
},
}),
);
const copyToClipboard = ({ about }: { about: DevToolsInfo | undefined }) => {
if (about) {
let formatted = `OS: ${about.operatingSystem}\nnode: ${about.nodeJsVersion}\nbackstage: ${about.backstageVersion}\nDependencies:\n`;
const deps = about.dependencies;
for (const key in deps) {
if (Object.prototype.hasOwnProperty.call(deps, key)) {
formatted = `${formatted} ${deps[key].name}: ${deps[key].versions}\n`;
}
}
window.navigator.clipboard.writeText(formatted);
}
};
/** @public */
export const InfoContent = () => {
const classes = useStyles();
const { about, loading, error } = useInfo();
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<Box>
<Paper className={classes.paperStyle}>
<List className={classes.flexContainer}>
<ListItem>
<ListItemAvatar>
<Avatar>
<DeveloperBoardIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Operating System"
secondary={about?.operatingSystem}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<DescriptionIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="NodeJS Version"
secondary={about?.nodeJsVersion}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<BackstageLogoIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Backstage Version"
secondary={about?.backstageVersion}
/>
</ListItem>
<Divider orientation="vertical" variant="middle" flexItem />
<ListItem
button
onClick={() => {
copyToClipboard({ about });
}}
className={classes.copyButton}
>
<ListItemAvatar>
<Avatar>
<FileCopyIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Copy Info to Clipboard" />
</ListItem>
</List>
</Paper>
<InfoDependenciesTable infoDependencies={about?.dependencies} />
</Box>
);
};
@@ -0,0 +1,55 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Table, TableColumn } from '@backstage/core-components';
import { PackageDependency } from '@backstage/plugin-devtools-common';
import React from 'react';
const columns: TableColumn[] = [
{
title: 'Name',
width: 'auto',
field: 'name',
defaultSort: 'asc',
},
{
title: 'Versions',
width: 'auto',
field: 'versions',
},
];
export const InfoDependenciesTable = ({
infoDependencies,
}: {
infoDependencies: PackageDependency[] | undefined;
}) => {
return (
<Table
title="Package Dependencies"
options={{
paging: true,
pageSize: 15,
pageSizeOptions: [15, 30, 100],
loadingType: 'linear',
padding: 'dense',
}}
columns={columns}
data={infoDependencies || []}
/>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { InfoContent } from './InfoContent';
@@ -0,0 +1,19 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './ConfigContent';
export * from './InfoContent';
export * from './ExternalDependenciesContent';
@@ -0,0 +1,42 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
devToolsConfigReadPermission,
devToolsInfoReadPermission,
} from '@backstage/plugin-devtools-common';
import { ConfigContent } from '../Content/ConfigContent';
import { DevToolsLayout } from '../DevToolsLayout';
import { InfoContent } from '../Content/InfoContent';
import React from 'react';
import { RequirePermission } from '@backstage/plugin-permission-react';
/** @public */
export const DefaultDevToolsPage = () => (
<DevToolsLayout>
<DevToolsLayout.Route path="info" title="Info">
<RequirePermission permission={devToolsInfoReadPermission}>
<InfoContent />
</RequirePermission>
</DevToolsLayout.Route>
<DevToolsLayout.Route path="config" title="Config">
<RequirePermission permission={devToolsConfigReadPermission}>
<ConfigContent />
</RequirePermission>
</DevToolsLayout.Route>
</DevToolsLayout>
);
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DefaultDevToolsPage } from './DefaultDevToolsPage';
@@ -0,0 +1,79 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Header, Page, RoutedTabs } from '@backstage/core-components';
import {
attachComponentData,
useElementFilter,
} from '@backstage/core-plugin-api';
import { TabProps } from '@material-ui/core';
import { default as React } from 'react';
/** @public */
export type SubRoute = {
path: string;
title: string;
children: JSX.Element;
tabProps?: TabProps<React.ElementType, { component?: React.ElementType }>;
};
const dataKey = 'plugin.devtools.devtoolsLayoutRoute';
const Route: (props: SubRoute) => null = () => null;
attachComponentData(Route, dataKey, true);
// This causes all mount points that are discovered within this route to use the path of the route itself
attachComponentData(Route, 'core.gatherMountPoints', true);
/** @public */
export type DevToolsLayoutProps = {
children?: React.ReactNode;
};
/**
* DevTools is a compound component, which allows you to define a custom layout
*
* @example
* ```jsx
* <DevToolsLayout>
* <DevToolsLayout.Route path="/example" title="Example tab">
* <div>This is rendered under /example/anything-here route</div>
* </DevToolsLayout.Route>
* </DevToolsLayout>
* ```
* @public
*/
export const DevToolsLayout = ({ children }: DevToolsLayoutProps) => {
const routes = useElementFilter(children, elements =>
elements
.selectByComponentData({
key: dataKey,
withStrictError:
'Child of DevToolsLayout must be an DevToolsLayout.Route',
})
.getElements<SubRoute>()
.map(child => child.props),
);
return (
<Page themeId="home">
<Header title="Backstage DevTools" />
<RoutedTabs routes={routes} />
</Page>
);
};
DevToolsLayout.Route = Route;
@@ -0,0 +1,18 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { DevToolsLayoutProps, SubRoute } from './DevToolsLayout';
export { DevToolsLayout } from './DevToolsLayout';
@@ -0,0 +1,25 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useOutlet } from 'react-router-dom';
import { DefaultDevToolsPage } from '../DefaultDevToolsPage';
export const DevToolsPage = () => {
const outlet = useOutlet();
return <>{outlet || <DefaultDevToolsPage />}</>;
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DevToolsPage } from './DevToolsPage';
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Content';
export * from './DevToolsLayout';
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useConfig } from './useConfig';
export { useExternalDependencies } from './useExternalDependencies';
export { useInfo } from './useInfo';
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devToolsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { ConfigInfo } from '@backstage/plugin-devtools-common';
export function useConfig(): {
configInfo?: ConfigInfo;
loading: boolean;
error?: Error;
} {
const api = useApi(devToolsApiRef);
const { value, loading, error } = useAsync(() => {
return api.getConfig();
}, [api]);
return {
configInfo: value,
loading,
error,
};
}
@@ -0,0 +1,37 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devToolsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { ExternalDependency } from '@backstage/plugin-devtools-common';
export function useExternalDependencies(): {
externalDependencies?: ExternalDependency[];
loading: boolean;
error?: Error;
} {
const api = useApi(devToolsApiRef);
const { value, loading, error } = useAsync(() => {
return api.getExternalDependencies();
}, [api]);
return {
externalDependencies: value,
loading,
error,
};
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devToolsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { DevToolsInfo } from '@backstage/plugin-devtools-common';
export function useInfo(): {
about?: DevToolsInfo;
loading: boolean;
error?: Error;
} {
const api = useApi(devToolsApiRef);
const { value, loading, error } = useAsync(() => {
return api.getInfo();
}, [api]);
return {
about: value,
loading,
error,
};
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { devToolsPlugin, DevToolsPage } from './plugin';
export * from './components';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devToolsPlugin } from './plugin';
describe('devtools', () => {
it('should export plugin', () => {
expect(devToolsPlugin).toBeDefined();
});
});
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { devToolsApiRef, DevToolsClient } from './api';
import { rootRouteRef } from './routes';
/** @public */
export const devToolsPlugin = createPlugin({
id: 'devtools',
apis: [
createApiFactory({
api: devToolsApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new DevToolsClient({ discoveryApi, identityApi }),
}),
],
routes: {
root: rootRouteRef,
},
});
/** @public */
export const DevToolsPage = devToolsPlugin.provide(
createRoutableExtension({
name: 'DevToolsPage',
component: () =>
import('./components/DevToolsPage').then(m => m.DevToolsPage),
mountPoint: rootRouteRef,
}),
);
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'devtools',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
+269 -14
View File
@@ -3384,7 +3384,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.21.0
resolution: "@babel/runtime@npm:7.21.0"
dependencies:
@@ -6142,6 +6142,86 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-devtools-backend@workspace:^, @backstage/plugin-devtools-backend@workspace:plugins/devtools-backend":
version: 0.0.0-use.local
resolution: "@backstage/plugin-devtools-backend@workspace:plugins/devtools-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/cli-common": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-devtools-common": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
"@manypkg/get-packages": ^1.1.3
"@types/express": "*"
"@types/minimist": ^1.2.0
"@types/ping": ^0.4.1
"@types/supertest": ^2.0.8
"@types/yarnpkg__lockfile": ^1.1.4
"@yarnpkg/lockfile": ^1.1.0
"@yarnpkg/parsers": ^3.0.0-rc.4
express: ^4.18.1
express-promise-router: ^4.1.0
fs-extra: ^10.0.0
lodash: ^4.17.21
msw: ^0.47.0
node-fetch: ^2.6.7
ping: ^0.4.1
semver: ^7.3.2
supertest: ^6.2.4
winston: ^3.2.1
yn: ^4.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-devtools-common@workspace:^, @backstage/plugin-devtools-common@workspace:plugins/devtools-common":
version: 0.0.0-use.local
resolution: "@backstage/plugin-devtools-common@workspace:plugins/devtools-common"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/types": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-devtools@workspace:^, @backstage/plugin-devtools@workspace:plugins/devtools":
version: 0.0.0-use.local
resolution: "@backstage/plugin-devtools@workspace:plugins/devtools"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-devtools-common": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"
"@material-ui/core": ^4.9.13
"@material-ui/icons": ^4.9.1
"@material-ui/lab": ^4.0.0-alpha.57
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
"@types/node": "*"
cross-fetch: ^3.1.5
msw: ^0.47.0
react-json-view: ^1.21.3
react-use: ^17.2.4
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/plugin-dynatrace@workspace:^, @backstage/plugin-dynatrace@workspace:plugins/dynatrace":
version: 0.0.0-use.local
resolution: "@backstage/plugin-dynatrace@workspace:plugins/dynatrace"
@@ -16383,6 +16463,13 @@ __metadata:
languageName: node
linkType: hard
"@types/ping@npm:^0.4.1":
version: 0.4.1
resolution: "@types/ping@npm:0.4.1"
checksum: 9b94837fe66df70558c5a42b0e0c8371b4950ab56b96c42c8df809ff2cf52477dd0a7e01d2e6b38af8bb6683b3dcb54587960b96b4b1f3d40fdb529aea348ad0
languageName: node
linkType: hard
"@types/pluralize@npm:^0.0.29":
version: 0.0.29
resolution: "@types/pluralize@npm:0.0.29"
@@ -18784,6 +18871,13 @@ __metadata:
languageName: node
linkType: hard
"base16@npm:^1.0.0":
version: 1.0.0
resolution: "base16@npm:1.0.0"
checksum: 0cd449a2db0f0f957e4b6b57e33bc43c9e20d4f1dd744065db94b5da35e8e71fa4dc4bc7a901e59a84d5f8b6936e3c520e2471787f667fc155fb0f50d8540f5d
languageName: node
linkType: hard
"base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
@@ -20882,7 +20976,7 @@ __metadata:
languageName: node
linkType: hard
"cross-fetch@npm:3.1.5, cross-fetch@npm:^3.0.4, cross-fetch@npm:^3.1.3, cross-fetch@npm:^3.1.5":
"cross-fetch@npm:3.1.5, cross-fetch@npm:^3.1.3, cross-fetch@npm:^3.1.5":
version: 3.1.5
resolution: "cross-fetch@npm:3.1.5"
dependencies:
@@ -23618,6 +23712,7 @@ __metadata:
"@backstage/plugin-cloudbuild": "workspace:^"
"@backstage/plugin-code-coverage": "workspace:^"
"@backstage/plugin-cost-insights": "workspace:^"
"@backstage/plugin-devtools": "workspace:^"
"@backstage/plugin-dynatrace": "workspace:^"
"@backstage/plugin-entity-feedback": "workspace:^"
"@backstage/plugin-explore": "workspace:^"
@@ -23741,6 +23836,7 @@ __metadata:
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/plugin-code-coverage-backend": "workspace:^"
"@backstage/plugin-devtools-backend": "workspace:^"
"@backstage/plugin-entity-feedback-backend": "workspace:^"
"@backstage/plugin-events-backend": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
@@ -24222,6 +24318,15 @@ __metadata:
languageName: node
linkType: hard
"fbemitter@npm:^3.0.0":
version: 3.0.0
resolution: "fbemitter@npm:3.0.0"
dependencies:
fbjs: ^3.0.0
checksum: 069690b8cdff3521ade3c9beb92ba0a38d818a86ef36dff8690e66749aef58809db4ac0d6938eb1cacea2dbef5f2a508952d455669590264cdc146bbe839f605
languageName: node
linkType: hard
"fbjs-css-vars@npm:^1.0.0":
version: 1.0.2
resolution: "fbjs-css-vars@npm:1.0.2"
@@ -24244,18 +24349,18 @@ __metadata:
languageName: node
linkType: hard
"fbjs@npm:^3.0.0":
version: 3.0.0
resolution: "fbjs@npm:3.0.0"
"fbjs@npm:^3.0.0, fbjs@npm:^3.0.1":
version: 3.0.4
resolution: "fbjs@npm:3.0.4"
dependencies:
cross-fetch: ^3.0.4
cross-fetch: ^3.1.5
fbjs-css-vars: ^1.0.0
loose-envify: ^1.0.0
object-assign: ^4.1.0
promise: ^7.1.1
setimmediate: ^1.0.5
ua-parser-js: ^0.7.18
checksum: 85ec57d8dbeddd7c82bf8f111a3c7de1abc1f4d7c603d6ccbcc1ec8dce35ff5b7a113dd34acbf7930093e5533c37a2298a92d342077f967bef34dc7cf2f3f07e
ua-parser-js: ^0.7.30
checksum: 8b23a3550fcda8a9109fca9475a3416590c18bb6825ea884192864ed686f67fcd618e308a140c9e5444fbd0168732e1ff3c092ba3d0c0ae1768969f32ba280c7
languageName: node
linkType: hard
@@ -24499,6 +24604,18 @@ __metadata:
languageName: node
linkType: hard
"flux@npm:^4.0.1":
version: 4.0.4
resolution: "flux@npm:4.0.4"
dependencies:
fbemitter: ^3.0.0
fbjs: ^3.0.1
peerDependencies:
react: ^15.0.2 || ^16.0.0 || ^17.0.0
checksum: 8fa5c2f9322258de3e331f67c6f1078a7f91c4dec9dbe8a54c4b8a80eed19a4f91889028b768668af4a796e8f2ee75e461e1571b8615432a3920ae95cc4ff794
languageName: node
linkType: hard
"fn.name@npm:1.x.x":
version: 1.1.0
resolution: "fn.name@npm:1.1.0"
@@ -29439,6 +29556,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.curry@npm:^4.0.1":
version: 4.1.1
resolution: "lodash.curry@npm:4.1.1"
checksum: 9192b70fe7df4d1ff780c0260bee271afa9168c93fe4fa24bc861900240531b59781b5fdaadf4644fea8f4fbcd96f0700539ab294b579ffc1022c6c15dcc462a
languageName: node
linkType: hard
"lodash.debounce@npm:^4, lodash.debounce@npm:^4.0.8":
version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8"
@@ -29474,6 +29598,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.flow@npm:^3.3.0":
version: 3.5.0
resolution: "lodash.flow@npm:3.5.0"
checksum: a9a62ad344e3c5a1f42bc121da20f64dd855aaafecee24b1db640f29b88bd165d81c37ff7e380a7191de6f70b26f5918abcebbee8396624f78f3618a0b18634c
languageName: node
linkType: hard
"lodash.get@npm:^4.4.2":
version: 4.4.2
resolution: "lodash.get@npm:4.4.2"
@@ -31083,6 +31214,41 @@ __metadata:
languageName: node
linkType: hard
"msw@npm:^0.47.0":
version: 0.47.4
resolution: "msw@npm:0.47.4"
dependencies:
"@mswjs/cookies": ^0.2.2
"@mswjs/interceptors": ^0.17.5
"@open-draft/until": ^1.0.3
"@types/cookie": ^0.4.1
"@types/js-levenshtein": ^1.1.1
chalk: 4.1.1
chokidar: ^3.4.2
cookie: ^0.4.2
graphql: ^15.0.0 || ^16.0.0
headers-polyfill: ^3.1.0
inquirer: ^8.2.0
is-node-process: ^1.0.1
js-levenshtein: ^1.1.6
node-fetch: ^2.6.7
outvariant: ^1.3.0
path-to-regexp: ^6.2.0
statuses: ^2.0.0
strict-event-emitter: ^0.2.6
type-fest: ^2.19.0
yargs: ^17.3.1
peerDependencies:
typescript: ">= 4.2.x <= 4.8.x"
peerDependenciesMeta:
typescript:
optional: true
bin:
msw: cli/index.js
checksum: 10ff632641d40384d6622abf4df6399e4ae649db0f676b5d1ee2d0a515ec96f33abe9d4fecba08cdba4b2e43255af419da9eefc020d40a7e10669d0906457197
languageName: node
linkType: hard
"msw@npm:^0.49.0":
version: 0.49.3
resolution: "msw@npm:0.49.3"
@@ -33154,6 +33320,13 @@ __metadata:
languageName: node
linkType: hard
"ping@npm:^0.4.1":
version: 0.4.4
resolution: "ping@npm:0.4.4"
checksum: cab10af309312e3a6822eccb7f323f0c9a1e17ef5895954114e31b6a1cced5e2ced3563990b79c66d074372e75aa4c846d9001120ee296b365de6a2adef5b8f9
languageName: node
linkType: hard
"pinkie-promise@npm:^2.0.0":
version: 2.0.1
resolution: "pinkie-promise@npm:2.0.1"
@@ -34242,6 +34415,13 @@ __metadata:
languageName: node
linkType: hard
"pure-color@npm:^1.2.0":
version: 1.3.0
resolution: "pure-color@npm:1.3.0"
checksum: 646d8bed6e6eab89affdd5e2c11f607a85b631a7fb03c061dfa658eb4dc4806881a15feed2ac5fd8c0bad8c00c632c640d5b1cb8b9a972e6e947393a1329371b
languageName: node
linkType: hard
"pvtsutils@npm:^1.3.2":
version: 1.3.2
resolution: "pvtsutils@npm:1.3.2"
@@ -34474,6 +34654,18 @@ __metadata:
languageName: node
linkType: hard
"react-base16-styling@npm:^0.6.0":
version: 0.6.0
resolution: "react-base16-styling@npm:0.6.0"
dependencies:
base16: ^1.0.0
lodash.curry: ^4.0.1
lodash.flow: ^3.3.0
pure-color: ^1.2.0
checksum: 00a12dddafc8a9025cca933b0dcb65fca41c81fa176d1fc3a6a9d0242127042e2c0a604f4c724a3254dd2c6aeb5ef55095522ff22f5462e419641c1341a658e4
languageName: node
linkType: hard
"react-beautiful-dnd@npm:^13.0.0":
version: 13.0.0
resolution: "react-beautiful-dnd@npm:13.0.0"
@@ -34743,6 +34935,21 @@ __metadata:
languageName: node
linkType: hard
"react-json-view@npm:^1.21.3":
version: 1.21.3
resolution: "react-json-view@npm:1.21.3"
dependencies:
flux: ^4.0.1
react-base16-styling: ^0.6.0
react-lifecycles-compat: ^3.0.4
react-textarea-autosize: ^8.3.2
peerDependencies:
react: ^17.0.0 || ^16.3.0 || ^15.5.4
react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
checksum: 5718bcd9210ad5b06eb9469cf8b9b44be9498845a7702e621343618e8251f26357e6e1c865532cf170db6165df1cb30202787e057309d8848c220bc600ec0d1a
languageName: node
linkType: hard
"react-lifecycles-compat@npm:^3.0.2, react-lifecycles-compat@npm:^3.0.4":
version: 3.0.4
resolution: "react-lifecycles-compat@npm:3.0.4"
@@ -35010,6 +35217,19 @@ __metadata:
languageName: node
linkType: hard
"react-textarea-autosize@npm:^8.3.2":
version: 8.4.1
resolution: "react-textarea-autosize@npm:8.4.1"
dependencies:
"@babel/runtime": ^7.20.13
use-composed-ref: ^1.3.0
use-latest: ^1.2.1
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: b200437cd68938c23b13944fe6fdfeb32a6d949ac88588307f14d6fcdaba3044b8c7d8e239851b081f2101d433b93d4cf5aa027543b170b84f2a0cbe6fc9093f
languageName: node
linkType: hard
"react-transition-group@npm:2.9.0, react-transition-group@npm:^2.2.1":
version: 2.9.0
resolution: "react-transition-group@npm:2.9.0"
@@ -37374,7 +37594,7 @@ __metadata:
languageName: node
linkType: hard
"statuses@npm:2.0.1":
"statuses@npm:2.0.1, statuses@npm:^2.0.0":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb
@@ -37476,12 +37696,12 @@ __metadata:
languageName: node
linkType: hard
"strict-event-emitter@npm:^0.2.4":
version: 0.2.7
resolution: "strict-event-emitter@npm:0.2.7"
"strict-event-emitter@npm:^0.2.4, strict-event-emitter@npm:^0.2.6":
version: 0.2.8
resolution: "strict-event-emitter@npm:0.2.8"
dependencies:
events: ^3.3.0
checksum: 111691e7d3fce0810586ccd8e8234af883ad3b121ef69091c7e260c32299d1ba085a95238ad09b43478bc5e9e80370f2fcb8114716e343be6f44bfc08fab4142
checksum: 6ac06fe72a6ee6ae64d20f1dd42838ea67342f1b5f32b03b3050d73ee6ecee44b4d5c4ed2965a7154b47991e215f373d4e789e2b2be2769cd80e356126c2ca53
languageName: node
linkType: hard
@@ -39046,7 +39266,7 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:^0.7.18, ua-parser-js@npm:^0.7.30":
"ua-parser-js@npm:^0.7.30":
version: 0.7.33
resolution: "ua-parser-js@npm:0.7.33"
checksum: 1510e9ec26fcaf0d8c6ae8f1078a8230e8816f083e1b5f453ea19d06b8ef2b8a596601c92148fd41899e8b3e5f83fa69c42332bd5729b931a721040339831696
@@ -39446,6 +39666,15 @@ __metadata:
languageName: node
linkType: hard
"use-composed-ref@npm:^1.3.0":
version: 1.3.0
resolution: "use-composed-ref@npm:1.3.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: f771cbadfdc91e03b7ab9eb32d0fc0cc647755711801bf507e891ad38c4bbc5f02b2509acadf9c965ec9c5f2f642fd33bdfdfb17b0873c4ad0a9b1f5e5e724bf
languageName: node
linkType: hard
"use-deep-compare-effect@npm:^1.8.1":
version: 1.8.1
resolution: "use-deep-compare-effect@npm:1.8.1"
@@ -39468,6 +39697,32 @@ __metadata:
languageName: node
linkType: hard
"use-isomorphic-layout-effect@npm:^1.1.1":
version: 1.1.2
resolution: "use-isomorphic-layout-effect@npm:1.1.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: a6532f7fc9ae222c3725ff0308aaf1f1ddbd3c00d685ef9eee6714fd0684de5cb9741b432fbf51e61a784e2955424864f7ea9f99734a02f237b17ad3e18ea5cb
languageName: node
linkType: hard
"use-latest@npm:^1.2.1":
version: 1.2.1
resolution: "use-latest@npm:1.2.1"
dependencies:
use-isomorphic-layout-effect: ^1.1.1
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: ed3f2ddddf6f21825e2ede4c2e0f0db8dcce5129802b69d1f0575fc1b42380436e8c76a6cd885d4e9aa8e292e60fb8b959c955f33c6a9123b83814a1a1875367
languageName: node
linkType: hard
"use-memo-one@npm:^1.1.1":
version: 1.1.1
resolution: "use-memo-one@npm:1.1.1"