Merge remote-tracking branch 'origin/master' into task-action-idempotency

This commit is contained in:
bnechyporenko
2024-02-11 20:50:42 +01:00
65 changed files with 1766 additions and 210 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Added a new `LegacyRootDatabaseService` interface that can be used to avoid direct dependencies on the `DatabaseManager`.
+6
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-tasks': patch
---
The `TaskScheduler.fromConfig` method now accepts the `LegacyRootDatabaseService` interface rather than the full `DatabaseManager` implementation.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': patch
---
Make schema processing gracefully handle an empty config.
+5
View File
@@ -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.
+5
View File
@@ -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.
+16
View File
@@ -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`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Fix entity content extension filtering.
+5
View File
@@ -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.
+1
View File
@@ -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` |
+39 -27
View File
@@ -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 }) => {
+10
View File
@@ -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
+1 -1
View File
@@ -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>
+6 -1
View File
@@ -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;
+1 -1
View File
@@ -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.
*
+3 -3
View File
@@ -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,
) {}
+1 -1
View File
@@ -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",
+1 -2
View File
@@ -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 = [
+3 -4
View File
@@ -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('//', '/');
+4 -3
View File
@@ -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:
-1
View File
@@ -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'
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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'),
};
+1 -1
View File
@@ -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 ',
},
],
},
+1 -1
View File
@@ -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',
},
],
+9 -7
View File
@@ -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({
+2 -2
View File
@@ -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({
+1 -1
View File
@@ -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({
+1 -1
View File
@@ -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({
+1 -1
View File
@@ -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',
}),
+1 -1
View File
@@ -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
+3
View File
@@ -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.
+22
View File
@@ -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)
```
+18 -3
View File
@@ -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:^",
+233
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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;
}
+18 -12
View File
@@ -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);
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+23 -12
View File
@@ -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"