Merge remote-tracking branch 'origin/master' into task-action-idempotency
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Added a new `LegacyRootDatabaseService` interface that can be used to avoid direct dependencies on the `DatabaseManager`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/integration-aws-node': patch
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
All single-line secrets read from config will now have both leading and trailing whitespace trimmed. This is done to ensure that the secrets are always valid HTTP header values, since many fetch implementations will include the header value itself when an error is thrown due to invalid header values.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
The `TaskScheduler.fromConfig` method now accepts the `LegacyRootDatabaseService` interface rather than the full `DatabaseManager` implementation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config-loader': patch
|
||||
---
|
||||
|
||||
Make schema processing gracefully handle an empty config.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Updated the default app index template at `packages/app/public/index.html` to have a fallback value for the `app.title` config.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Make `http://localhost:3000` the default base URL for serving locally, and `/` the default public path for built apps. The app build no longer requires any configuration values to be present.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/cli': patch
|
||||
'@backstage/integration': patch
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-github-actions': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-techdocs-module-addons-contrib': patch
|
||||
'@backstage/plugin-techdocs-node': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Updated dependency `git-url-parse` to `^14.0.0`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fix entity content extension filtering.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': minor
|
||||
---
|
||||
|
||||
Migrate the `api-docs` to the new frontend system. It is experimental and available via alpha subpath.
|
||||
@@ -148,6 +148,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling.
|
||||
| Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` |
|
||||
| Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` |
|
||||
| David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` |
|
||||
| Deepankumar Loganathan | | [deepan10](https://github.com/deepan10) | `deepan10` |
|
||||
| Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` |
|
||||
| Irma Solakovic | Roadie.io | [Irma12](https://github.com/Irma12) | `Irma#7629` |
|
||||
| Jamie Klassen | VMware | [jamieklassen](https://github.com/jamieklassen) | `jamieklassen#3047` |
|
||||
|
||||
@@ -113,7 +113,9 @@ export interface AuthService {
|
||||
|
||||
// TODO: should the caller provide the target plugin ID?
|
||||
// TODO: how can we make it very difficult to forget to forward credentials
|
||||
issueToken(credentials: BackstageCredentials): Promise<{ token: string }>;
|
||||
issueToken(options: {
|
||||
forward?: BackstageCredentials;
|
||||
}): Promise<{ token: string }>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -138,15 +140,18 @@ The `UserInfoService` is exported by `@backstage/auth-node`, and the initial imp
|
||||
The `HttpRouterService` interface will be extended with the ability to opt-out of the default protection of endpoints, enabling either cookie auth or unauthenticated access.
|
||||
|
||||
```ts
|
||||
export interface HttpRouterServiceAuthPolicy {
|
||||
// The path matches in the same way as if it was passed to `express.Router.use(path, ...)`
|
||||
path: string;
|
||||
allow: 'unauthenticated' | 'user-cookie';
|
||||
}
|
||||
|
||||
export interface HttpRouterService {
|
||||
// All routes only allow authenticated users and services by default.
|
||||
use(handler: Handler): void;
|
||||
|
||||
// Exact option structure is TBD, just highlighting the general idea for now
|
||||
configure(options: {
|
||||
allowCookieAuthOnPaths?: string[];
|
||||
allowUnauthenticatedAccessPaths?: string[];
|
||||
}): void;
|
||||
// These are additive and the most relaxed access level takes precedence
|
||||
addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -187,8 +192,9 @@ export default createBackendPlugin({
|
||||
async init({ http }) {
|
||||
// The order of these two calls does not matter
|
||||
http.use(await createRouter(/* ... */));
|
||||
http.configure({
|
||||
allowCookieAuthOnPaths: ['/static'],
|
||||
http.addAuthPolicy({
|
||||
path: '/static',
|
||||
allow: 'user-cookie',
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -208,10 +214,16 @@ export default createBackendPlugin({
|
||||
},
|
||||
async init({ http }) {
|
||||
http.use(await createRouter(/* ... */));
|
||||
http.configure({
|
||||
allowCookieAuthOnPaths: ['/'],
|
||||
// Unauthenticated access takes precedence, the /public endpoint does not require cookie auth
|
||||
allowUnauthenticatedAccessPaths: ['/public'],
|
||||
|
||||
http.addAuthPolicy({
|
||||
path: '/',
|
||||
allow: 'user-cookie',
|
||||
});
|
||||
|
||||
// Unauthenticated access takes precedence, the /public endpoint does not require cookie auth
|
||||
http.addAuthPolicy({
|
||||
path: '/public',
|
||||
allow: 'unauthenticated',
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -232,22 +244,21 @@ export type BackstageUnauthorizedCredentials = {
|
||||
|
||||
type BackstageCredentialTypes = {
|
||||
user: BackstageUserCredentials;
|
||||
'user-cookie': BackstageUserCredentials;
|
||||
service: BackstageServiceCredentials;
|
||||
unauthorized: BackstageUnauthorizedCredentials;
|
||||
};
|
||||
|
||||
export interface HttpAuthService {
|
||||
createHttpPluginRouterMiddleware(options: OptionsTBD): Handler;
|
||||
|
||||
// Implementations should cache resolved credentials on the request object
|
||||
credentials<TAllowed extends keyof BackstageCredentialTypes>(
|
||||
req: Request,
|
||||
options?: HttpAuthServiceMiddlewareOptions<TAllowed>,
|
||||
): Promise<BackstageCredentialTypes[TAllowed]>;
|
||||
|
||||
// TODO: Keep an eye on this, might not be needed
|
||||
requestHeaders(
|
||||
credentials: BackstageCredentials,
|
||||
): Promise<Record<string, string>>;
|
||||
requestHeaders(options?: {
|
||||
forward?: BackstageCredentials;
|
||||
}): Promise<Record<string, string>>;
|
||||
|
||||
issueUserCookie(res: Response): Promise<void>;
|
||||
}
|
||||
@@ -336,11 +347,11 @@ router.get('/cookie', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Allowing cookie auth is a separate step where you call the configure method
|
||||
// Allowing cookie auth is a separate step where you call the addAuthPolicy method
|
||||
// of the httpRouter API in your plugin setup code.
|
||||
httpRouter.configure({
|
||||
// In practice we can make this configuration a lot more capable, this is just a minimal example
|
||||
allowCookieAuthOnPaths: ['/static'], // router.use('/static', cookieAuthMiddleware()) under the hood
|
||||
httpRouter.addAuthPolicy({
|
||||
path: '/static',
|
||||
allow: 'user-cookie',
|
||||
});
|
||||
|
||||
// Separate endpoint that serves static content, allowing user cookie auth as
|
||||
@@ -377,10 +388,11 @@ The release plan for the `HttpAuthService` is TBD, but is likely to be shipped a
|
||||
|
||||
- [ ] Implement `AuthService`
|
||||
- [ ] Implement `HttpAuthService` - leave cookie auth as unimplemented for now
|
||||
- [ ] Add `configure()` for `HttpRouterService`, using `HttpAuthService`
|
||||
- [ ] Add `addAuthPolicy()` for `HttpRouterService`, using `HttpAuthService`
|
||||
- [ ] Implement a compatibility wrapper in `backend-common` that accepts `AuthService`, `HttpAuthService`, `IdentityService`, and `TokenManagerService` (all optional), and returns implementations for `AuthService` and `HttpAuthService`, such hat existing plugins can use a single `createRouter` implementation for both the old and new backend systems.
|
||||
- [ ] Implement `UserInfoService` in `@backstage/auth-node` - for now it will just extract the ownership entity refs from the token stored in the credentials
|
||||
- [ ] Implement cookie auth in `HttpAuthService` - just put the user token in the cookie for now
|
||||
- [ ] Deprecate `IdentityService` and `TokenManagerService`, switch to using default factories that depend on the `AuthService` and `HttpAuthService`. Stop supplying implementations for these in `backend-defaults` and `backend-test-utils`
|
||||
- [ ] Migrate plugins:
|
||||
- [ ] Permission backend
|
||||
- [ ] TechDocs backend
|
||||
@@ -432,13 +444,13 @@ http.use(cookieRouter, { allow: ['user-cookie'] });
|
||||
|
||||
Similar to the previous approach, but also require that a path is provided. This removes much of the confusion around what middleware are applied.
|
||||
|
||||
The downside of this approach is that it still has the drawback of forcing a separation of the router, but at the same it provides very little benefit over a top-level path configuration approach like `http.configure()`. The `'/static'` path in the below example essentially has the exact same logic as `.configure({ cookieAuthPaths: ['/static'] })` since it'd be implemented in the same way. The `.configure()` approach has the benefit of allowing plugin authors to decide whether they want to keep the routes separate or not.
|
||||
The downside of this approach is that it still has the drawback of forcing a separation of the router, but at the same it provides very little benefit over a top-level path configuration approach like `http.addAuthPolicy()`. The `'/static'` path in the below example essentially has the exact same logic as `.addAuthPolicy({ path: '/static', allow: 'user-cookie' })` since it'd be implemented in the same way. The `.addAuthPolicy()` approach has the benefit of allowing plugin authors to decide whether they want to keep the routes separate or not.
|
||||
|
||||
This does have the benefit of letting the framework know which exact routes are protected, which can be useful for introspection, although that benefit also applies to the `.configure()` approach.
|
||||
This does have the benefit of letting the framework know which exact routes are protected, which can be useful for introspection, although that benefit also applies to the `.addAuthPolicy()` approach.
|
||||
|
||||
```ts
|
||||
// This isn't too bad, but it's extremely similar to the configure() method since
|
||||
// we're just matching on the path. The benefit of configure is that it allows you
|
||||
// This isn't too bad, but it's extremely similar to the addAuthPolicy() method since
|
||||
// we're just matching on the path. The benefit of addAuthPolicy is that it allows you
|
||||
// to keep everything in a singe router if desired.
|
||||
http.use('/static', cookieRouter, { allow: ['user-cookie'] });
|
||||
```
|
||||
|
||||
@@ -20,11 +20,11 @@ The [createScaffolderLayout](https://backstage.io/docs/reference/plugin-scaffold
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
import {
|
||||
createScaffolderLayout,
|
||||
LayoutTemplate,
|
||||
scaffolderPlugin,
|
||||
} from '@backstage/plugin-scaffolder';
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
app:
|
||||
experimental:
|
||||
packages: 'all' # ✨
|
||||
|
||||
routes:
|
||||
bindings:
|
||||
catalog.viewTechDoc: techdocs.docRoot
|
||||
@@ -11,6 +12,7 @@ app:
|
||||
# - apis.plugin.graphiql.browse.gitlab: true
|
||||
- graphiql-endpoint:graphiql/gitlab: true
|
||||
|
||||
# Entity page cards
|
||||
- entity-card:catalog/about
|
||||
- entity-card:catalog/labels
|
||||
- entity-card:catalog/links:
|
||||
@@ -21,7 +23,15 @@ app:
|
||||
config:
|
||||
height: 300
|
||||
- entity-card:azure-devops/readme
|
||||
- entity-card:api-docs/has-apis
|
||||
- entity-card:api-docs/consumed-apis
|
||||
- entity-card:api-docs/provided-apis
|
||||
- entity-card:api-docs/providing-components
|
||||
- entity-card:api-docs/consuming-components
|
||||
|
||||
# Entity page content
|
||||
- entity-content:api-docs/definition
|
||||
- entity-content:api-docs/apis
|
||||
- entity-content:techdocs
|
||||
- entity-content:azure-devops/pipelines
|
||||
- entity-content:azure-devops/pull-requests
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
href="<%= publicPath %>/safari-pinned-tab.svg"
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<title><%= config.getString('app.title') %></title>
|
||||
<title><%= config.getOptionalString('app.title') ?? 'Backstage' %></title>
|
||||
|
||||
<% if (config.has('app.datadogRum')) { %>
|
||||
<script>
|
||||
|
||||
@@ -249,7 +249,7 @@ export function createStatusCheckRouter(options: {
|
||||
}): Promise<express.Router>;
|
||||
|
||||
// @public
|
||||
export class DatabaseManager {
|
||||
export class DatabaseManager implements LegacyRootDatabaseService {
|
||||
forPlugin(
|
||||
pluginId: string,
|
||||
deps?: {
|
||||
@@ -555,6 +555,11 @@ export const legacyPlugin: (
|
||||
}>,
|
||||
) => BackendFeature;
|
||||
|
||||
// @public
|
||||
export type LegacyRootDatabaseService = {
|
||||
forPlugin(pluginId: string): PluginDatabaseManager;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function loadBackendConfig(options: {
|
||||
logger: LoggerService;
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"helmet": "^6.0.0",
|
||||
"isomorphic-git": "^1.23.0",
|
||||
"jose": "^4.6.0",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { PluginDatabaseManager } from './types';
|
||||
import path from 'path';
|
||||
import {
|
||||
DatabaseService,
|
||||
LifecycleService,
|
||||
LoggerService,
|
||||
PluginMetadataService,
|
||||
@@ -53,6 +54,14 @@ export type DatabaseManagerOptions = {
|
||||
logger?: LoggerService;
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface that represents the legacy global DatabaseManager implementation.
|
||||
* @public
|
||||
*/
|
||||
export type LegacyRootDatabaseService = {
|
||||
forPlugin(pluginId: string): DatabaseService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages database connections for Backstage backend plugins.
|
||||
*
|
||||
@@ -65,7 +74,7 @@ export type DatabaseManagerOptions = {
|
||||
* set `prefix` which is used to prefix generated database names if config is
|
||||
* not provided.
|
||||
*/
|
||||
export class DatabaseManager {
|
||||
export class DatabaseManager implements LegacyRootDatabaseService {
|
||||
/**
|
||||
* Creates a {@link DatabaseManager} from `backend.database` config.
|
||||
*
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
```ts
|
||||
import { Config } from '@backstage/config';
|
||||
import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { Duration } from 'luxon';
|
||||
import { HumanDuration as HumanDuration_2 } from '@backstage/types';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { LegacyRootDatabaseService } from '@backstage/backend-common';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface TaskScheduleDefinitionConfig {
|
||||
|
||||
// @public
|
||||
export class TaskScheduler {
|
||||
constructor(databaseManager: DatabaseManager, logger: Logger);
|
||||
constructor(databaseManager: LegacyRootDatabaseService, logger: Logger);
|
||||
forPlugin(pluginId: string): PluginTaskScheduler;
|
||||
// (undocumented)
|
||||
static forPlugin(opts: {
|
||||
@@ -95,7 +95,7 @@ export class TaskScheduler {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
databaseManager?: DatabaseManager;
|
||||
databaseManager?: LegacyRootDatabaseService;
|
||||
logger?: Logger;
|
||||
},
|
||||
): TaskScheduler;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import {
|
||||
DatabaseManager,
|
||||
getRootLogger,
|
||||
LegacyRootDatabaseService,
|
||||
PluginDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
@@ -37,7 +38,7 @@ export class TaskScheduler {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
databaseManager?: DatabaseManager;
|
||||
databaseManager?: LegacyRootDatabaseService;
|
||||
logger?: Logger;
|
||||
},
|
||||
): TaskScheduler {
|
||||
@@ -50,7 +51,7 @@ export class TaskScheduler {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly databaseManager: LegacyRootDatabaseService,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"express": "^4.17.1",
|
||||
"fork-ts-checker-webpack-plugin": "^7.0.0-alpha.8",
|
||||
"fs-extra": "10.1.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"glob": "^7.1.7",
|
||||
"global-agent": "^3.0.0",
|
||||
"handlebars": "^4.7.3",
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
printFileSizesAfterBuild,
|
||||
} from 'react-dev-utils/FileSizeReporter';
|
||||
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
|
||||
import { createConfig, resolveBaseUrl } from './config';
|
||||
import { createConfig } from './config';
|
||||
import { BuildOptions } from './types';
|
||||
import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
|
||||
import chalk from 'chalk';
|
||||
@@ -51,7 +51,6 @@ export async function buildBundle(options: BuildOptions) {
|
||||
...options,
|
||||
checksEnabled: false,
|
||||
isDev: false,
|
||||
baseUrl: resolveBaseUrl(options.frontendConfig),
|
||||
getFrontendAppConfigs: () => options.frontendAppConfigs,
|
||||
};
|
||||
const configs = [
|
||||
|
||||
@@ -44,9 +44,9 @@ import { hasReactDomClient } from './hasReactDomClient';
|
||||
const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE';
|
||||
|
||||
export function resolveBaseUrl(config: Config): URL {
|
||||
const baseUrl = config.getString('app.baseUrl');
|
||||
const baseUrl = config.getOptionalString('app.baseUrl');
|
||||
try {
|
||||
return new URL(baseUrl);
|
||||
return new URL(baseUrl ?? '/', 'http://localhost:3000');
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid app.baseUrl, ${error}`);
|
||||
}
|
||||
@@ -100,8 +100,7 @@ export async function createConfig(
|
||||
const { packages } = await getPackages(cliPaths.targetDir);
|
||||
const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir));
|
||||
|
||||
const baseUrl = frontendConfig.getString('app.baseUrl');
|
||||
const validBaseUrl = new URL(baseUrl);
|
||||
const validBaseUrl = resolveBaseUrl(frontendConfig);
|
||||
let publicPath = validBaseUrl.pathname.replace(/\/$/, '');
|
||||
if (publicSubPath) {
|
||||
publicPath = `${publicPath}${publicSubPath}`.replace('//', '/');
|
||||
|
||||
@@ -110,9 +110,10 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
|
||||
});
|
||||
latestFrontendAppConfigs = cliConfig.frontendAppConfigs;
|
||||
|
||||
const appBaseUrl = cliConfig.frontendConfig.getString('app.baseUrl');
|
||||
const backendBaseUrl = cliConfig.frontendConfig.getString('backend.baseUrl');
|
||||
if (appBaseUrl === backendBaseUrl) {
|
||||
const appBaseUrl = cliConfig.frontendConfig.getOptionalString('app.baseUrl');
|
||||
const backendBaseUrl =
|
||||
cliConfig.frontendConfig.getOptionalString('backend.baseUrl');
|
||||
if (appBaseUrl && appBaseUrl === backendBaseUrl) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Conflict between app baseUrl and backend baseUrl:
|
||||
|
||||
@@ -23,7 +23,6 @@ export type BundlingOptions = {
|
||||
isDev: boolean;
|
||||
frontendConfig: Config;
|
||||
getFrontendAppConfigs(): AppConfig[];
|
||||
baseUrl: URL;
|
||||
parallelism?: number;
|
||||
additionalEntryPoints?: string[];
|
||||
// Path to append to the detected public path, e.g. '/public'
|
||||
|
||||
@@ -190,7 +190,7 @@ export function compileConfigSchemas(
|
||||
});
|
||||
|
||||
return configs => {
|
||||
const config = ConfigReader.fromConfigs(configs).get();
|
||||
const config = ConfigReader.fromConfigs(configs).getOptional();
|
||||
|
||||
visibilityByDataPath.clear();
|
||||
deepVisibilityByDataPath.clear();
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
href="<%= publicPath %>/safari-pinned-tab.svg"
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<title><%= config.getString('app.title') %></title>
|
||||
<title><%= config.getOptionalString('app.title') ?? 'Backstage' %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -152,7 +152,7 @@ function readAwsIntegrationAccountConfig(
|
||||
const accountConfig = {
|
||||
accountId: config.getString('accountId'),
|
||||
accessKeyId: config.getOptionalString('accessKeyId'),
|
||||
secretAccessKey: config.getOptionalString('secretAccessKey'),
|
||||
secretAccessKey: config.getOptionalString('secretAccessKey')?.trim(),
|
||||
profile: config.getOptionalString('profile'),
|
||||
roleName: config.getOptionalString('roleName'),
|
||||
region: config.getOptionalString('region'),
|
||||
@@ -216,7 +216,7 @@ function readMainAwsIntegrationAccountConfig(
|
||||
): AwsIntegrationMainAccountConfig {
|
||||
const mainAccountConfig = {
|
||||
accessKeyId: config.getOptionalString('accessKeyId'),
|
||||
secretAccessKey: config.getOptionalString('secretAccessKey'),
|
||||
secretAccessKey: config.getOptionalString('secretAccessKey')?.trim(),
|
||||
profile: config.getOptionalString('profile'),
|
||||
region: config.getOptionalString('region'),
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@octokit/auth-app": "^4.0.0",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('AwsS3Integration', () => {
|
||||
{
|
||||
endpoint: 'https://a.com',
|
||||
accessKeyId: 'access key',
|
||||
secretAccessKey: 'secret key',
|
||||
secretAccessKey: ' secret key ',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ export function readAwsS3IntegrationConfig(
|
||||
}
|
||||
|
||||
const accessKeyId = config.getOptionalString('accessKeyId');
|
||||
const secretAccessKey = config.getOptionalString('secretAccessKey');
|
||||
const secretAccessKey = config.getOptionalString('secretAccessKey')?.trim();
|
||||
const roleArn = config.getOptionalString('roleArn');
|
||||
const externalId = config.getOptionalString('externalId');
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('readAzureIntegrationConfig', () => {
|
||||
credentials: [
|
||||
{
|
||||
organizations: ['org1'],
|
||||
personalAccessToken: 't',
|
||||
personalAccessToken: 't ',
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -119,7 +119,7 @@ describe('readAzureIntegrationConfig', () => {
|
||||
{
|
||||
organizations: ['org1', 'org2'],
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
clientSecret: 'secret\n\n\n',
|
||||
tenantId: 'tenant',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -205,18 +205,18 @@ export function readAzureIntegrationConfig(
|
||||
?.map(credential => {
|
||||
const result: Partial<AzureDevOpsCredentialLike> = {
|
||||
organizations: credential.getOptionalStringArray('organizations'),
|
||||
personalAccessToken: credential.getOptionalString(
|
||||
'personalAccessToken',
|
||||
),
|
||||
personalAccessToken: credential
|
||||
.getOptionalString('personalAccessToken')
|
||||
?.trim(),
|
||||
tenantId: credential.getOptionalString('tenantId'),
|
||||
clientId: credential.getOptionalString('clientId'),
|
||||
clientSecret: credential.getOptionalString('clientSecret'),
|
||||
clientSecret: credential.getOptionalString('clientSecret')?.trim(),
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const token = config.getOptionalString('token');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
|
||||
if (
|
||||
config.getOptional('credential') !== undefined &&
|
||||
@@ -247,10 +247,12 @@ export function readAzureIntegrationConfig(
|
||||
organizations: config.getOptionalStringArray(
|
||||
'credential.organizations',
|
||||
),
|
||||
token: config.getOptionalString('credential.token'),
|
||||
token: config.getOptionalString('credential.token')?.trim(),
|
||||
tenantId: config.getOptionalString('credential.tenantId'),
|
||||
clientId: config.getOptionalString('credential.clientId'),
|
||||
clientSecret: config.getOptionalString('credential.clientSecret'),
|
||||
clientSecret: config
|
||||
.getOptionalString('credential.clientSecret')
|
||||
?.trim(),
|
||||
},
|
||||
];
|
||||
credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped;
|
||||
|
||||
@@ -58,9 +58,9 @@ describe('readBitbucketIntegrationConfig', () => {
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
token: 't\n\n\n',
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
appPassword: '\n\n\np',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
|
||||
@@ -76,9 +76,9 @@ export function readBitbucketIntegrationConfig(
|
||||
): BitbucketIntegrationConfig {
|
||||
const host = config.getOptionalString('host') ?? BITBUCKET_HOST;
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
const username = config.getOptionalString('username');
|
||||
const appPassword = config.getOptionalString('appPassword');
|
||||
const appPassword = config.getOptionalString('appPassword')?.trim();
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('readBitbucketCloudIntegrationConfig', () => {
|
||||
const output = readBitbucketCloudIntegrationConfig(
|
||||
buildConfig({
|
||||
username: 'u',
|
||||
appPassword: 'p',
|
||||
appPassword: '\n\n\np',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
|
||||
@@ -62,7 +62,7 @@ export function readBitbucketCloudIntegrationConfig(
|
||||
// If config is provided, we assume authenticated access is desired
|
||||
// (as the anonymous one is provided by default).
|
||||
const username = config.getString('username');
|
||||
const appPassword = config.getString('appPassword');
|
||||
const appPassword = config.getString('appPassword')?.trim();
|
||||
|
||||
return {
|
||||
host,
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('readBitbucketServerIntegrationConfig', () => {
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
token: 't',
|
||||
token: '\tt\t',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
|
||||
@@ -77,7 +77,7 @@ export function readBitbucketServerIntegrationConfig(
|
||||
): BitbucketServerIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('readGerritIntegrationConfig', () => {
|
||||
cloneUrl: 'https:a.com/clone',
|
||||
gitilesBaseUrl: 'https://a.com/git',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
password: ' p ',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
|
||||
@@ -78,7 +78,7 @@ export function readGerritIntegrationConfig(
|
||||
let cloneUrl = config.getOptionalString('cloneUrl');
|
||||
let gitilesBaseUrl = config.getOptionalString('gitilesBaseUrl');
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
const password = config.getOptionalString('password')?.trim();
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('readGiteaConfig', () => {
|
||||
host: 'a.com',
|
||||
baseUrl: 'https://a.com/route/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
password: 'p ',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
|
||||
@@ -56,7 +56,7 @@ export function readGiteaConfig(config: Config): GiteaIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let baseUrl = config.getOptionalString('baseUrl');
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
const password = config.getOptionalString('password')?.trim();
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('readGithubIntegrationConfig', () => {
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
rawBaseUrl: 'https://a.com/raw',
|
||||
token: 't',
|
||||
token: '\nt\t',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
|
||||
@@ -123,7 +123,7 @@ export function readGithubIntegrationConfig(
|
||||
const host = config.getOptionalString('host') ?? GITHUB_HOST;
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
let rawBaseUrl = config.getOptionalString('rawBaseUrl');
|
||||
const token = config.getOptionalString('token');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
const apps = config.getOptionalConfigArray('apps')?.map(c => ({
|
||||
appId: c.getNumber('appId'),
|
||||
clientId: c.getString('clientId'),
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('readGitLabIntegrationConfig', () => {
|
||||
const output = readGitLabIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
token: 't',
|
||||
token: ' t\n',
|
||||
apiBaseUrl: 'https://a.com',
|
||||
baseUrl: 'https://baseurl.for.me/gitlab',
|
||||
}),
|
||||
|
||||
@@ -67,7 +67,7 @@ export function readGitLabIntegrationConfig(
|
||||
): GitLabIntegrationConfig {
|
||||
const host = config.getString('host');
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token');
|
||||
const token = config.getOptionalString('token')?.trim();
|
||||
let baseUrl = config.getOptionalString('baseUrl');
|
||||
if (apiBaseUrl) {
|
||||
apiBaseUrl = trimEnd(apiBaseUrl, '/');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,8 @@
|
||||
# API Documentation
|
||||
|
||||
> Disclaimer:
|
||||
> If you are looking for documentation on the experimental new frontend system support, please go [here](./README-alpha.md).
|
||||
|
||||
This is an extension for the catalog plugin that provides components to discover and display API entities.
|
||||
APIs define the interface between components, see the [system model](https://backstage.io/docs/features/software-catalog/system-model) for details.
|
||||
They are defined in machine readable formats and provide a human readable documentation.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
## API Report File for "@backstage/plugin-api-docs"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{
|
||||
registerApi: ExternalRouteRef<undefined, true>;
|
||||
}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -6,9 +6,22 @@
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.tsx",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.tsx"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
@@ -35,8 +48,10 @@
|
||||
"dependencies": {
|
||||
"@asyncapi/react-component": "1.2.13",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/core-compat-api": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
createApiExtension,
|
||||
createApiFactory,
|
||||
createNavItemExtension,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
import {
|
||||
compatWrapper,
|
||||
convertLegacyRouteRef,
|
||||
} from '@backstage/core-compat-api';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
|
||||
import {
|
||||
createEntityCardExtension,
|
||||
createEntityContentExtension,
|
||||
} from '@backstage/plugin-catalog-react/alpha';
|
||||
import {
|
||||
ApiEntity,
|
||||
parseEntityRef,
|
||||
RELATION_HAS_PART,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
import { defaultDefinitionWidgets } from './components/ApiDefinitionCard';
|
||||
import { rootRoute, registerComponentRouteRef } from './routes';
|
||||
import { apiDocsConfigRef } from './config';
|
||||
|
||||
function ApiIcon() {
|
||||
const app = useApp();
|
||||
const KindApiSystemIcon = app.getSystemIcon('kind:api')!;
|
||||
return <KindApiSystemIcon />;
|
||||
}
|
||||
|
||||
const apiDocsNavItem = createNavItemExtension({
|
||||
title: 'APIs',
|
||||
routeRef: convertLegacyRouteRef(rootRoute),
|
||||
icon: () => compatWrapper(<ApiIcon />),
|
||||
});
|
||||
|
||||
const apiDocsConfigApi = createApiExtension({
|
||||
factory: createApiFactory({
|
||||
api: apiDocsConfigRef,
|
||||
deps: {},
|
||||
factory: () => {
|
||||
const definitionWidgets = defaultDefinitionWidgets();
|
||||
return {
|
||||
getApiDefinitionWidget: (apiEntity: ApiEntity) => {
|
||||
return definitionWidgets.find(d => d.type === apiEntity.spec.type);
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const apiDocsExplorerPage = createPageExtension({
|
||||
defaultPath: '/api-docs',
|
||||
routeRef: convertLegacyRouteRef(rootRoute),
|
||||
// Mapping DefaultApiExplorerPageProps to config
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
path: z.string().default('/api-docs'),
|
||||
initiallySelectedFilter: z.enum(['owned', 'starred', 'all']).optional(),
|
||||
// Ommiting columns and actions for now as their types are too complex to map to zod
|
||||
}),
|
||||
),
|
||||
loader: ({ config }) =>
|
||||
import('./components/ApiExplorerPage').then(m =>
|
||||
compatWrapper(
|
||||
<m.ApiExplorerIndexPage
|
||||
initiallySelectedFilter={config.initiallySelectedFilter}
|
||||
/>,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsHasApisEntityCard = createEntityCardExtension({
|
||||
name: 'has-apis',
|
||||
// Ommiting configSchema for now
|
||||
// We are skipping variants and columns are too complex to map to zod
|
||||
// See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252
|
||||
filter: entity => {
|
||||
return (
|
||||
entity.kind === 'Component' &&
|
||||
entity.relations?.some(
|
||||
({ type, targetRef }) =>
|
||||
type.toLocaleLowerCase('en-US') === RELATION_HAS_PART &&
|
||||
parseEntityRef(targetRef).kind === 'API',
|
||||
)!!
|
||||
);
|
||||
},
|
||||
loader: () =>
|
||||
import('./components/ApisCards').then(m =>
|
||||
compatWrapper(<m.HasApisCard />),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsDefinitionEntityCard = createEntityCardExtension({
|
||||
name: 'definition',
|
||||
filter: 'kind:api',
|
||||
loader: () =>
|
||||
import('./components/ApiDefinitionCard').then(m =>
|
||||
compatWrapper(<m.ApiDefinitionCard />),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsConsumedApisEntityCard = createEntityCardExtension({
|
||||
name: 'consumed-apis',
|
||||
// Ommiting configSchema for now
|
||||
// We are skipping variants and columns are too complex to map to zod
|
||||
// See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252
|
||||
filter: 'kind:component',
|
||||
loader: () =>
|
||||
import('./components/ApisCards').then(m =>
|
||||
compatWrapper(<m.ConsumedApisCard />),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsProvidedApisEntityCard = createEntityCardExtension({
|
||||
name: 'provided-apis',
|
||||
// Ommiting configSchema for now
|
||||
// We are skipping variants and columns are too complex to map to zod
|
||||
// See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252
|
||||
filter: 'kind:component',
|
||||
loader: () =>
|
||||
import('./components/ApisCards').then(m =>
|
||||
compatWrapper(<m.ProvidedApisCard />),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsConsumingComponentsEntityCard = createEntityCardExtension({
|
||||
name: 'consuming-components',
|
||||
// Ommiting configSchema for now
|
||||
// We are skipping variants
|
||||
// See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252
|
||||
filter: 'kind:api',
|
||||
loader: () =>
|
||||
import('./components/ComponentsCards').then(m =>
|
||||
compatWrapper(<m.ConsumingComponentsCard />),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsProvidingComponentsEntityCard = createEntityCardExtension({
|
||||
name: 'providing-components',
|
||||
// Ommiting configSchema for now
|
||||
// We are skipping variants
|
||||
// See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252
|
||||
filter: 'kind:api',
|
||||
loader: () =>
|
||||
import('./components/ComponentsCards').then(m =>
|
||||
compatWrapper(<m.ProvidingComponentsCard />),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsDefinitionEntityContent = createEntityContentExtension({
|
||||
name: 'definition',
|
||||
defaultPath: '/defintion',
|
||||
defaultTitle: 'Definition',
|
||||
filter: 'kind:api',
|
||||
loader: async () =>
|
||||
import('./components/ApiDefinitionCard').then(m =>
|
||||
compatWrapper(
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<m.ApiDefinitionCard />
|
||||
</Grid>
|
||||
</Grid>,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocsApisEntityContent = createEntityContentExtension({
|
||||
name: 'apis',
|
||||
defaultPath: '/apis',
|
||||
defaultTitle: 'APIs',
|
||||
filter: 'kind:component',
|
||||
loader: async () =>
|
||||
import('./components/ApisCards').then(m =>
|
||||
compatWrapper(
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item xs={12}>
|
||||
<m.ProvidedApisCard />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<m.ConsumedApisCard />
|
||||
</Grid>
|
||||
</Grid>,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
export default createPlugin({
|
||||
id: 'api-docs',
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(rootRoute),
|
||||
},
|
||||
externalRoutes: {
|
||||
registerApi: convertLegacyRouteRef(registerComponentRouteRef),
|
||||
},
|
||||
extensions: [
|
||||
apiDocsNavItem,
|
||||
apiDocsConfigApi,
|
||||
apiDocsExplorerPage,
|
||||
apiDocsHasApisEntityCard,
|
||||
apiDocsDefinitionEntityCard,
|
||||
apiDocsProvidedApisEntityCard,
|
||||
apiDocsConsumedApisEntityCard,
|
||||
apiDocsConsumingComponentsEntityCard,
|
||||
apiDocsProvidingComponentsEntityCard,
|
||||
apiDocsDefinitionEntityContent,
|
||||
apiDocsApisEntityContent,
|
||||
],
|
||||
});
|
||||
@@ -58,7 +58,7 @@
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"@octokit/graphql": "^5.0.0",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "^5.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"express": "^4.17.1",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"knex": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"js-base64": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react-hook-form": "^7.12.2",
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import React, { useMemo } from 'react';
|
||||
import { parseFilterExpression } from './filter/parseFilterExpression';
|
||||
import React from 'react';
|
||||
import { FilterWrapper } from './filter/FilterWrapper';
|
||||
|
||||
interface EntityOverviewPageProps {
|
||||
cards: Array<{
|
||||
@@ -28,80 +28,12 @@ interface EntityOverviewPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Keeps track of what filter expression strings that we've seen duplicates of
|
||||
// with functions, or which emitted parsing errors for so far
|
||||
const seenParseErrorExpressionStrings = new Set<string>();
|
||||
const seenDuplicateExpressionStrings = new Set<string>();
|
||||
|
||||
// Given an optional filter function and an optional filter expression, make
|
||||
// sure that at most one of them was given, and return a filter function that
|
||||
// does the right thing.
|
||||
function buildFilterFn(
|
||||
filterFunction?: (entity: Entity) => boolean,
|
||||
filterExpression?: string,
|
||||
): (entity: Entity) => boolean {
|
||||
if (
|
||||
filterFunction &&
|
||||
filterExpression &&
|
||||
!seenDuplicateExpressionStrings.has(filterExpression)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Duplicate entity filter methods found, both '${filterExpression}' as well as a callback function, which is not permitted - using the callback`,
|
||||
);
|
||||
seenDuplicateExpressionStrings.add(filterExpression);
|
||||
}
|
||||
|
||||
const filter = filterFunction || filterExpression;
|
||||
if (!filter) {
|
||||
return () => true;
|
||||
} else if (typeof filter === 'function') {
|
||||
return subject => filter(subject);
|
||||
}
|
||||
|
||||
const result = parseFilterExpression(filter);
|
||||
if (
|
||||
result.expressionParseErrors.length &&
|
||||
!seenParseErrorExpressionStrings.has(filter)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Error(s) in entity filter expression '${filter}'`,
|
||||
result.expressionParseErrors,
|
||||
);
|
||||
seenParseErrorExpressionStrings.add(filter);
|
||||
}
|
||||
|
||||
return result.filterFn;
|
||||
}
|
||||
|
||||
// Handles the memoized parsing of filter expressions for each card
|
||||
function CardWrapper(props: {
|
||||
entity: Entity;
|
||||
element: React.JSX.Element;
|
||||
filterFunction?: (entity: Entity) => boolean;
|
||||
filterExpression?: string;
|
||||
}) {
|
||||
const { entity, element, filterFunction, filterExpression } = props;
|
||||
|
||||
const filterFn = useMemo(
|
||||
() => buildFilterFn(filterFunction, filterExpression),
|
||||
[filterFunction, filterExpression],
|
||||
);
|
||||
|
||||
return filterFn(entity) ? (
|
||||
<Grid item md={6} xs={12}>
|
||||
{element}
|
||||
</Grid>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export function EntityOverviewPage(props: EntityOverviewPageProps) {
|
||||
const { entity } = useEntity();
|
||||
return (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{props.cards.map((card, index) => (
|
||||
<CardWrapper key={index} entity={entity} {...card} />
|
||||
<FilterWrapper key={index} entity={entity} {...card} />
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import React, { useMemo } from 'react';
|
||||
import { parseFilterExpression } from './parseFilterExpression';
|
||||
|
||||
// Keeps track of what filter expression strings that we've seen duplicates of
|
||||
// with functions, or which emitted parsing errors for so far
|
||||
const seenParseErrorExpressionStrings = new Set<string>();
|
||||
const seenDuplicateExpressionStrings = new Set<string>();
|
||||
|
||||
// Given an optional filter function and an optional filter expression, make
|
||||
// sure that at most one of them was given, and return a filter function that
|
||||
// does the right thing.
|
||||
export function buildFilterFn(
|
||||
filterFunction?: (entity: Entity) => boolean,
|
||||
filterExpression?: string,
|
||||
): (entity: Entity) => boolean {
|
||||
if (
|
||||
filterFunction &&
|
||||
filterExpression &&
|
||||
!seenDuplicateExpressionStrings.has(filterExpression)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Duplicate entity filter methods found, both '${filterExpression}' as well as a callback function, which is not permitted - using the callback`,
|
||||
);
|
||||
seenDuplicateExpressionStrings.add(filterExpression);
|
||||
}
|
||||
|
||||
const filter = filterFunction || filterExpression;
|
||||
if (!filter) {
|
||||
return () => true;
|
||||
} else if (typeof filter === 'function') {
|
||||
return subject => filter(subject);
|
||||
}
|
||||
|
||||
const result = parseFilterExpression(filter);
|
||||
if (
|
||||
result.expressionParseErrors.length &&
|
||||
!seenParseErrorExpressionStrings.has(filter)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Error(s) in entity filter expression '${filter}'`,
|
||||
result.expressionParseErrors,
|
||||
);
|
||||
seenParseErrorExpressionStrings.add(filter);
|
||||
}
|
||||
|
||||
return result.filterFn;
|
||||
}
|
||||
|
||||
// Handles the memoized parsing of filter expressions
|
||||
export function FilterWrapper(props: {
|
||||
entity: Entity;
|
||||
element: React.JSX.Element;
|
||||
filterFunction?: (entity: Entity) => boolean;
|
||||
filterExpression?: string;
|
||||
}) {
|
||||
const { entity, element, filterFunction, filterExpression } = props;
|
||||
|
||||
const filterFn = useMemo(
|
||||
() => buildFilterFn(filterFunction, filterExpression),
|
||||
[filterFunction, filterExpression],
|
||||
);
|
||||
|
||||
return filterFn(entity) ? (
|
||||
<Grid item md={6} xs={12}>
|
||||
{element}
|
||||
</Grid>
|
||||
) : null;
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { catalogExtensionData } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { rootRouteRef } from '../routes';
|
||||
import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl';
|
||||
import { buildFilterFn } from './filter/FilterWrapper';
|
||||
|
||||
export const catalogPage = createPageExtension({
|
||||
defaultPath: '/catalog',
|
||||
@@ -57,24 +58,29 @@ export const catalogEntityPage = createPageExtension({
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
title: catalogExtensionData.entityContentTitle,
|
||||
filterFunction: catalogExtensionData.entityFilterFunction.optional(),
|
||||
filterExpression: catalogExtensionData.entityFilterExpression.optional(),
|
||||
}),
|
||||
},
|
||||
loader: async ({ inputs }) => {
|
||||
const { EntityLayout } = await import('../components/EntityLayout');
|
||||
const Component = () => {
|
||||
const { entity, ...rest } = useEntityFromUrl();
|
||||
return (
|
||||
<AsyncEntityProvider {...useEntityFromUrl()}>
|
||||
<EntityLayout>
|
||||
{inputs.contents.map(content => (
|
||||
<EntityLayout.Route
|
||||
key={content.output.path}
|
||||
path={content.output.path}
|
||||
title={content.output.title}
|
||||
>
|
||||
{content.output.element}
|
||||
</EntityLayout.Route>
|
||||
))}
|
||||
</EntityLayout>
|
||||
<AsyncEntityProvider entity={entity} {...rest}>
|
||||
{entity ? (
|
||||
<EntityLayout>
|
||||
{inputs.contents
|
||||
.filter(({ output: { filterFunction, filterExpression } }) =>
|
||||
buildFilterFn(filterFunction, filterExpression)(entity),
|
||||
)
|
||||
.map(({ output: { path, title, element } }) => (
|
||||
<EntityLayout.Route key={path} path={path} title={title}>
|
||||
{element}
|
||||
</EntityLayout.Route>
|
||||
))}
|
||||
</EntityLayout>
|
||||
) : null}
|
||||
</AsyncEntityProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,21 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha';
|
||||
import {
|
||||
TestEventBroker,
|
||||
TestEventPublisher,
|
||||
TestEventSubscriber,
|
||||
} from '@backstage/plugin-events-backend-test-utils';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha';
|
||||
import request from 'supertest';
|
||||
import { eventsPlugin } from './EventsPlugin';
|
||||
|
||||
@@ -38,11 +31,6 @@ describe('eventPlugin', () => {
|
||||
const publisher = new TestEventPublisher();
|
||||
const subscriber = new TestEventSubscriber('sub', ['fake']);
|
||||
|
||||
const httpRouter = Router();
|
||||
httpRouter.use(express.json());
|
||||
httpRouter.use(errorHandler());
|
||||
const app = express().use(httpRouter);
|
||||
|
||||
const testModule = createBackendModule({
|
||||
pluginId: 'events',
|
||||
moduleId: 'test',
|
||||
@@ -60,7 +48,7 @@ describe('eventPlugin', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
const { server } = await startTestBackend({
|
||||
extensionPoints: [],
|
||||
features: [
|
||||
eventsPlugin(),
|
||||
@@ -75,11 +63,6 @@ describe('eventPlugin', () => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
createServiceFactory({
|
||||
service: coreServices.httpRouter,
|
||||
deps: {},
|
||||
factory: async () => httpRouter,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -87,8 +70,8 @@ describe('eventPlugin', () => {
|
||||
expect(eventBroker.subscribed.length).toEqual(1);
|
||||
expect(eventBroker.subscribed[0]).toBe(subscriber);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/http/fake')
|
||||
const response = await request(server)
|
||||
.post('/api/events/http/fake')
|
||||
.timeout(1000)
|
||||
.send({ test: 'fake' });
|
||||
expect(response.status).toBe(202);
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"luxon": "^3.0.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^",
|
||||
"fs-extra": "10.1.0",
|
||||
"git-url-parse": "^13.1.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"yaml": "^2.0.0"
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"@uiw/react-codemirror": "^4.9.3",
|
||||
"classnames": "^2.2.6",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"humanize-duration": "^3.25.1",
|
||||
"immer": "^9.0.1",
|
||||
"json-schema": "^0.4.0",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@react-hookz/web": "^24.0.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"photoswipe": "^5.3.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "10.1.0",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"hpagent": "^1.2.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"json5": "^2.1.3",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"dompurify": "^3.0.0",
|
||||
"event-source-polyfill": "1.0.25",
|
||||
"git-url-parse": "^13.0.0",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"jss": "~10.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react-helmet": "6.1.0",
|
||||
|
||||
@@ -3316,7 +3316,7 @@ __metadata:
|
||||
express: ^4.17.1
|
||||
express-promise-router: ^4.1.0
|
||||
fs-extra: 10.1.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
helmet: ^6.0.0
|
||||
http-errors: ^2.0.0
|
||||
isomorphic-git: ^1.23.0
|
||||
@@ -3664,7 +3664,7 @@ __metadata:
|
||||
express: ^4.17.1
|
||||
fork-ts-checker-webpack-plugin: ^7.0.0-alpha.8
|
||||
fs-extra: 10.1.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
glob: ^7.1.7
|
||||
global-agent: ^3.0.0
|
||||
handlebars: ^4.7.3
|
||||
@@ -4293,7 +4293,7 @@ __metadata:
|
||||
"@octokit/rest": ^19.0.3
|
||||
"@types/luxon": ^3.0.0
|
||||
cross-fetch: ^4.0.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
lodash: ^4.17.21
|
||||
luxon: ^3.0.0
|
||||
msw: ^1.0.0
|
||||
@@ -4557,9 +4557,11 @@ __metadata:
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-compat-api": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/dev-utils": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/plugin-catalog": "workspace:^"
|
||||
"@backstage/plugin-catalog-common": "workspace:^"
|
||||
"@backstage/plugin-catalog-react": "workspace:^"
|
||||
@@ -5497,7 +5499,7 @@ __metadata:
|
||||
"@octokit/graphql": ^5.0.0
|
||||
"@octokit/rest": ^19.0.3
|
||||
"@types/lodash": ^4.14.151
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
lodash: ^4.17.21
|
||||
luxon: ^3.0.0
|
||||
minimatch: ^5.1.2
|
||||
@@ -5720,7 +5722,7 @@ __metadata:
|
||||
express: ^4.17.1
|
||||
fast-json-stable-stringify: ^2.1.0
|
||||
fs-extra: 10.1.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
glob: ^7.1.6
|
||||
knex: ^3.0.0
|
||||
lodash: ^4.17.21
|
||||
@@ -5828,7 +5830,7 @@ __metadata:
|
||||
"@testing-library/react": ^14.0.0
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
js-base64: ^3.6.0
|
||||
lodash: ^4.17.21
|
||||
msw: ^1.0.0
|
||||
@@ -6882,7 +6884,7 @@ __metadata:
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
luxon: ^3.0.0
|
||||
react-use: ^17.2.4
|
||||
peerDependencies:
|
||||
@@ -8437,7 +8439,7 @@ __metadata:
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^"
|
||||
fs-extra: 10.1.0
|
||||
git-url-parse: ^13.1.0
|
||||
git-url-parse: ^14.0.0
|
||||
msw: ^1.0.0
|
||||
node-fetch: ^2.6.7
|
||||
node-html-markdown: ^1.3.0
|
||||
@@ -8782,7 +8784,7 @@ __metadata:
|
||||
"@uiw/react-codemirror": ^4.9.3
|
||||
classnames: ^2.2.6
|
||||
event-source-polyfill: ^1.0.31
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
humanize-duration: ^3.25.1
|
||||
immer: ^9.0.1
|
||||
json-schema: ^0.4.0
|
||||
@@ -9598,7 +9600,7 @@ __metadata:
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
photoswipe: ^5.3.7
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
@@ -9639,7 +9641,7 @@ __metadata:
|
||||
aws-sdk-client-mock: ^3.0.0
|
||||
express: ^4.17.1
|
||||
fs-extra: 10.1.0
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
hpagent: ^1.2.0
|
||||
js-yaml: ^4.0.0
|
||||
json5: ^2.1.3
|
||||
@@ -9716,7 +9718,7 @@ __metadata:
|
||||
canvas: ^2.10.2
|
||||
dompurify: ^3.0.0
|
||||
event-source-polyfill: 1.0.25
|
||||
git-url-parse: ^13.0.0
|
||||
git-url-parse: ^14.0.0
|
||||
jss: ~10.10.0
|
||||
lodash: ^4.17.21
|
||||
react-helmet: 6.1.0
|
||||
@@ -29277,6 +29279,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"git-url-parse@npm:^14.0.0":
|
||||
version: 14.0.0
|
||||
resolution: "git-url-parse@npm:14.0.0"
|
||||
dependencies:
|
||||
git-up: ^7.0.0
|
||||
checksum: b011c5de652e60e5f19de9815d1b78b2f725deb07e73d1b9ff8ca6657406d0a6c691fbe4460017822676a80635f93099345cadbd06361b76f53c4556265d3e48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"github-from-package@npm:0.0.0":
|
||||
version: 0.0.0
|
||||
resolution: "github-from-package@npm:0.0.0"
|
||||
|
||||
Reference in New Issue
Block a user