diff --git a/.changeset/dependabot-2b68456.md b/.changeset/dependabot-2b68456.md new file mode 100644 index 0000000000..f54dd17d55 --- /dev/null +++ b/.changeset/dependabot-2b68456.md @@ -0,0 +1,7 @@ +--- +'@backstage/catalog-model': patch +'@backstage/config-loader': patch +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +--- + +build(deps): bump `ajv` from 7.0.3 to 8.10.0 diff --git a/.changeset/happy-foxes-arrive.md b/.changeset/happy-foxes-arrive.md new file mode 100644 index 0000000000..53e59c8d77 --- /dev/null +++ b/.changeset/happy-foxes-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added an experimental `package fix` command which applies automated fixes to the target package. The initial fix that is available is to add missing monorepo dependencies to the target package. diff --git a/.changeset/mean-pumas-search.md b/.changeset/mean-pumas-search.md new file mode 100644 index 0000000000..68a3aebc85 --- /dev/null +++ b/.changeset/mean-pumas-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix diff --git a/.changeset/modern-pumas-join.md b/.changeset/modern-pumas-join.md new file mode 100644 index 0000000000..d2995af986 --- /dev/null +++ b/.changeset/modern-pumas-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Enable internal batching of very large deletions, to not run into SQL binding limits diff --git a/.changeset/moody-pigs-rush.md b/.changeset/moody-pigs-rush.md new file mode 100644 index 0000000000..37ae171382 --- /dev/null +++ b/.changeset/moody-pigs-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Applied the fix from version 0.12.3 of this package, which is part of the v1.0.1 release of Backstage. diff --git a/.changeset/olive-geese-chew.md b/.changeset/olive-geese-chew.md new file mode 100644 index 0000000000..a08b73b4a7 --- /dev/null +++ b/.changeset/olive-geese-chew.md @@ -0,0 +1,10 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +--- + +Support external ID when assuming roles in S3 integration + +In order to assume a role created by a 3rd party as external +ID is needed. This change adds an optional field to the s3 +integration configuration and consumes that in the AwsS3UrlReader. diff --git a/.changeset/rich-maps-hear.md b/.changeset/rich-maps-hear.md new file mode 100644 index 0000000000..390da8b6bb --- /dev/null +++ b/.changeset/rich-maps-hear.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Added a new `NextScaffolderRouter` which will eventually replace the exiting router diff --git a/.changeset/silent-bobcats-matter.md b/.changeset/silent-bobcats-matter.md new file mode 100644 index 0000000000..f67f2d3ede --- /dev/null +++ b/.changeset/silent-bobcats-matter.md @@ -0,0 +1,9 @@ +--- +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/core-app-api': patch +'@backstage/plugin-catalog-backend-module-bitbucket': patch +'@backstage/plugin-tech-insights-backend': patch +--- + +Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. diff --git a/.changeset/smart-phones-exist.md b/.changeset/smart-phones-exist.md new file mode 100644 index 0000000000..eb1c3d041a --- /dev/null +++ b/.changeset/smart-phones-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a new experimental `repo list-deprecations` command, which scans the entire project for usage of deprecated APIs. diff --git a/.changeset/techdocs-coats-obey.md b/.changeset/techdocs-coats-obey.md new file mode 100644 index 0000000000..0c2c2b5ac2 --- /dev/null +++ b/.changeset/techdocs-coats-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fix permalink scrolling for anchors where the id starts with a number. diff --git a/.changeset/techdocs-head-shoulders-knees-toes.md b/.changeset/techdocs-head-shoulders-knees-toes.md new file mode 100644 index 0000000000..f96b624897 --- /dev/null +++ b/.changeset/techdocs-head-shoulders-knees-toes.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Fixed a bug affecting those with cache enabled that would result in empty content being cached if the first attempt to load a static asset from storage were made via a `HEAD` request, rather than a `GET` request. diff --git a/.changeset/thick-comics-fold.md b/.changeset/thick-comics-fold.md new file mode 100644 index 0000000000..5faf2fd399 --- /dev/null +++ b/.changeset/thick-comics-fold.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-insights-backend': patch +--- + +Improved the Tech-Insights documentation: + +- `lifecycle` examples used `ttl` when it should be `timeToLive` +- Added list of included FactRetrievers +- Added full backend example using all included FactRetrievers +- Added boolean scorecard example image showing results of backend example diff --git a/.changeset/yellow-hats-remember.md b/.changeset/yellow-hats-remember.md new file mode 100644 index 0000000000..1a9064f94e --- /dev/null +++ b/.changeset/yellow-hats-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins-backend': patch +--- + +Make `resourceRef` required in `JenkinsApi` to match usage. diff --git a/.github/workflows/sync_issue-labels.yml b/.github/workflows/sync_issue-labels.yml new file mode 100644 index 0000000000..6a1c33348a --- /dev/null +++ b/.github/workflows/sync_issue-labels.yml @@ -0,0 +1,37 @@ +name: Sync Issue Labels +on: + issues: + types: [opened] +jobs: + label-issue: + runs-on: ubuntu-latest + steps: + - name: View context attributes + uses: actions/github-script@v6 + with: + script: | + const keywords = { + 'techdocs|tech-docs|tech docs': 'docs-like-code', + 'search': 'search', + 'catalog': 'catalog', + 'scaffolder': 'scaffolder', + }; + + const labels = Object.entries(keywords) + .map(([regexp, label]) => { + if (new RegExp(regexp, 'gi').test(context.payload.issue.title)) { + return label; + } + }) + .filter(Boolean); + + if(!labels.length) { + return; + } + + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels + }); diff --git a/ADOPTERS.md b/ADOPTERS.md index 10661f9792..9a4e3de546 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -107,3 +107,4 @@ _If you're using Backstage in your organization, please try to add your company | [VIA](https://www.via.com.br) | [@vagnerguedes](https://github.com/vagnerguedes) | Centralized Developer Experience portal - Software catalog and documentation platform, software templates, techdocs, scaffolding, self-service infrastructure | | [Surevine](https://www.surevine.com/) | [@DJDANNY123](https://github.com/djdanny123) | Developer portal for software catalog, discovery and a view of the technologies we are using across the organisation, we are looking to explore how we can enrich our entities in Backstage by integrating a software bill of materials. | | [Bonial International GmbH](https://www.bonial.com/) | [@pjungermann](https://github.com/pjungermann) | Centralized developer portal with software catalog, tech docs, templates, and more. | +| [Beez Innovation Labs Pvt. Ltd](https://www.beezlabs.com/) | [Karthikeyan Venkatesan](https://github.com/karthikeyan23) | Developer portal with software catalog, scaffolding, tech docs, templates, and infra. | diff --git a/STYLE.md b/STYLE.md index d74616a88c..391b31ce50 100644 --- a/STYLE.md +++ b/STYLE.md @@ -100,6 +100,15 @@ This section describes guidelines for designing public APIs. It can also be appl } ``` +1. Prefer common prefixes over suffixes when naming constants. + + ```ts + // May be tempting to use `GITHUB_WIDGET_LABEL` instead. + const WIDGET_LABEL_GITHUB = 'github'; + const WIDGET_LABEL_GITLAB = 'gitlab'; + const WIDGET_LABEL_BITBUCKET = 'bitbucket'; + ``` + 1. When a type relates directly to other symbols, use the name of those as prefix for the type. ```ts diff --git a/contrib/catalog/ImmediateEntityProvider.ts b/contrib/catalog/ImmediateEntityProvider.ts new file mode 100644 index 0000000000..b66c62e330 --- /dev/null +++ b/contrib/catalog/ImmediateEntityProvider.ts @@ -0,0 +1,169 @@ +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + Entity, + entitySchemaValidator, +} from '@backstage/catalog-model'; +import { InputError } from '@backstage/errors'; +import { + DeferredEntity, + EntityProvider, + EntityProviderConnection, + parseEntityYaml, +} from '@backstage/plugin-catalog-backend'; +import bodyParser from 'body-parser'; +import express from 'express'; +import Router from 'express-promise-router'; +import lodash from 'lodash'; +import { Logger } from 'winston'; + +/** + * An entity provider attached to a router, that lets users perform direct + * manipulation of a set of entities using REST requests. + * + * @remarks + * + * Installation: + * + * Add it to the catalog builder in your + * `packages/backend/src/plugins/catalog.ts`. Note that it BOTH adds a provider + * and amends the catalog router: + * + * ``` + * const immediate = new ImmediateEntityProvider({ + * logger: env.logger, + * handleEntity: (deferred) => { + * // Optionally modify the incoming entity + * }, + * }); + * builder.addEntityProvider(immediate); + * + * // ... + * + * return router.use('/immediate', immediate.getRouter()); + * ``` + * + * API (assume a catalog prefix, e.g. `/api/catalog`): + * + * - `POST /immediate/entities`: Accepts a YAML document of entities, and + * inserts or updates the entities that match that document. Returns 201 OK on + * success. + * + * - `PUT /immediate/entities`: Accepts a YAML document of entities, and + * replaces the entire set of entities managed by the provider with those + * entities. Returns 201 OK on success. + */ +export class ImmediateEntityProvider implements EntityProvider { + private connection?: EntityProviderConnection; + private readonly entityValidator: (data: unknown) => Entity; + + constructor(private readonly options: ImmediateEntityProviderOptions) { + this.entityValidator = entitySchemaValidator(); + } + + /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */ + getProviderName() { + return `ImmediateEntityProvider`; + } + + /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */ + async connect(connection: EntityProviderConnection) { + this.connection = connection; + } + + getRouter(): express.Router { + const router = Router(); + + router.use(bodyParser.raw({ type: '*/*' })); + + router.post('/entities', async (req, res) => { + if (!this.connection) { + throw new Error(`Service is not yet initialized`); + } + const deferred = await this.getRequestBodyEntities(req); + await this.connection.applyMutation({ + type: 'delta', + added: deferred, + removed: [], + }); + res.status(201).end(); + }); + + router.put('/entities', async (req, res) => { + if (!this.connection) { + throw new Error(`Service is not yet initialized`); + } + const deferred = await this.getRequestBodyEntities(req); + await this.connection.applyMutation({ + type: 'full', + entities: deferred, + }); + res.status(201).end(); + }); + + return router; + } + + private async getRequestBodyEntities( + req: express.Request, + ): Promise { + if (!Buffer.isBuffer(req.body) || !req.body.length) { + throw new InputError(`Missing request body`); + } + + const result: DeferredEntity[] = []; + + for await (const item of parseEntityYaml(req.body, { + type: 'immediate', + target: 'immediate', + })) { + if (item.type === 'entity') { + const deferred: DeferredEntity = { + entity: lodash.merge( + { + metadata: { + annotations: { + [ANNOTATION_ORIGIN_LOCATION]: 'immediate:immediate', + [ANNOTATION_LOCATION]: 'immediate:immediate', + }, + }, + }, + item.entity, + ), + locationKey: `immediate:`, + }; + + await this.options.handleEntity?.(req, deferred); + deferred.entity = this.entityValidator(deferred.entity); + + result.push(deferred); + } else if (item.type === 'error') { + throw new InputError(`Malformed entity YAML, ${item.error}`); + } else { + throw new InputError(`Internal error, failed to parse entity`); + } + } + + return result; + } +} + +/** + * Options for {@link ImmediateEntityProvider}. + */ +export interface ImmediateEntityProviderOptions { + /** + * The logger to use. + */ + logger: Logger; + + /** + * An optional function to perform adjustments to, or validate, an incoming + * entity before being stored. It is permitted to modify the deferred entity, + * but the request is static and has had its body consumed. + */ + handleEntity?: ( + request: express.Request, + deferred: DeferredEntity, + ) => void | Promise; +} diff --git a/contrib/catalog/LoadTestingEntityProvider.ts b/contrib/catalog/LoadTestingEntityProvider.ts new file mode 100644 index 0000000000..598099fce9 --- /dev/null +++ b/contrib/catalog/LoadTestingEntityProvider.ts @@ -0,0 +1,151 @@ +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + Entity, +} from '@backstage/catalog-model'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; + +/** + * An entity provider that can be used for load testing. Not for production use. + * + * @remarks + * + * Add it to the catalog builder in your + * `packages/backend/src/plugins/catalog.ts` doing some type of work, for + * example: + * + * ``` + * builder.addEntityProvider( + * new LoadTestingEntityProvider({ + * logger: env.logger, + * onStartup: async ({ connection, generateRandomEntities }) => { + * await connection.applyMutation({ + * type: 'full', + * entities: generateRandomEntities(100000).map(e => ({ + * entity: e, + * locationKey: 'l', + * })), + * }); + * }, + * }), + * ); + * ``` + * + * The provider will run the test, outputting some timing info onto the console. + * It will also clean up everything you added through the given connection. + */ +export class LoadTestingEntityProvider implements EntityProvider { + constructor(private readonly options: LoadTestingEntityProviderOptions) {} + + /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */ + getProviderName() { + return `LoadTestingEntityProvider`; + } + + /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */ + async connect(connection: EntityProviderConnection) { + const delayStartup = this.options.delayStartup ?? 10_000; + const logger = this.options.logger.child({ + class: LoadTestingEntityProvider.prototype.constructor.name, + }); + + if (delayStartup) { + logger.info( + `[LOAD-TEST] Starting in ${(delayStartup / 1000).toFixed(1)}s`, + ); + } + + setTimeout(async () => { + const timer = () => { + const startedOn = Date.now(); + return () => `${((Date.now() - startedOn) / 1000).toFixed(1)}s`; + }; + + const overallTimer = timer(); + logger.info(`[LOAD-TEST] Started`); + + const runTimer = timer(); + try { + await this.options.onStartup({ + connection, + logger, + generateRandomEntities, + }); + logger.info(`[LOAD-TEST] Finished in ${runTimer()}`); + } catch (error) { + logger.error(`[LOAD-TEST] Failed after ${runTimer()}`, error); + } + + const cleanupTimer = timer(); + logger.info(`[LOAD-TEST] Running cleanup`); + await connection.applyMutation({ + type: 'full', + entities: [], + }); + + logger.info(`[LOAD-TEST] ***************************************`); + logger.info(`[LOAD-TEST] Test run time: ${runTimer()}`); + logger.info(`[LOAD-TEST] Cleanup run time: ${cleanupTimer()}`); + logger.info(`[LOAD-TEST] Total time: ${overallTimer()}`); + logger.info(`[LOAD-TEST] ***************************************`); + }, delayStartup); + } +} + +function generateRandomEntities(count: number): Entity[] { + const result: Entity[] = []; + + for (let i = 1; i <= count; ++i) { + result.push({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [ANNOTATION_ORIGIN_LOCATION]: 'url:http://example.com/load-testing', + [ANNOTATION_LOCATION]: 'url:http://example.com/load-testing', + }, + namespace: 'load-test', + name: `load-test-${i}`, + }, + spec: { + type: 'load-test-data', + owner: 'me', + lifecycle: 'experimental', + }, + }); + } + + return result; +} + +/** + * Options for LoadTestingEntityProvider. + */ +export interface LoadTestingEntityProviderOptions { + /** + * The logger to use. + */ + logger: Logger; + + /** + * The number of milliseconds of delay to wait before starting the test. This + * gives the backend a chance to settle into a stable state before the test + * starts. + * + * @defaultValue 5000 + */ + delayStartup?: number; + + /** + * What work to do on startup. + */ + onStartup: (context: { + connection: EntityProviderConnection; + logger: Logger; + generateRandomEntities(count: number): Entity[]; + }) => Promise; +} diff --git a/contrib/catalog/README.md b/contrib/catalog/README.md new file mode 100644 index 0000000000..e02760cdb0 --- /dev/null +++ b/contrib/catalog/README.md @@ -0,0 +1,27 @@ +# Catalog Contrib + +This directory contains various community contributions related to [the Backstage catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview). + +There is no guarantee of correctness or fitness of purpose of these +contributions, but we hope that they are helpful to someone! + +Installation instructions are generally in the doc comment on top of each class. + +## ImmediateEntityProvider + +Sometimes we get requests for the ability to POST/PUT entities directly to the +catalog, instead of its regular mode of operation where it pulls data from +authoritative sources itself. + +The core product does not intend to support this use case, since it comes with a +number of caveats. However, this entity provider demonstrates how to build a +very basic version of such functionality yourself. It does not offer any +protection from misuse, but can serve as a good starting point to build out such +a provider yourself, fit for your particular needs. + +## LoadTestingEntityProvider + +This is a trivial little test bed entity provider that lets you make huge batch +operations and get some timings back. It also tries to clean up after itself +when it's done. This can be useful if you are working on optimizing the catalog +itself, or on processors or similar that you add to it. diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index a95b276365..52f7888fb8 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -109,6 +109,10 @@ import { createRouter, createAwsAlbProvider, } from '@backstage/plugin-auth-backend'; +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -154,16 +158,20 @@ export default async function createPlugin({ const [id] = email?.split('@') ?? ''; // Fetch from an external system that returns entity claims like: // ['user:default/breanna.davison', ...] - const ent = [`user:default/${id}`]; + const userEntityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); // Resolve group membership from the Backstage catalog const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [id].concat(ent), + entityRefs: [id].concat([userEntityRef]), logger: ctx.logger, }); const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: fullEnt }, + claims: { sub: userEntityRef, ent: fullEnt }, }); return { id, token }; }, diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index c336da38dd..fea82894c8 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -51,11 +51,12 @@ export default async function createPlugin( // Let's use the username in the email ID as the user's default // unique identifier inside Backstage. const [id] = email.split('@'); - ent.push(stringifyEntityRef({ + const userEntityRef = stringifyEntityRef({ kind: 'User', namespace: DEFAULT_NAMESPACE, name: id, - })); + }); + ent.push(userEntityRef); // Let's call the internal LDAP provider to get a list of groups // that the user belongs to, and add those to the list as well @@ -68,7 +69,7 @@ export default async function createPlugin( // Issue the token containing the entity claims const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent }, + claims: { sub: userEntityRef, ent }, }); return { id, token }; }, @@ -130,6 +131,8 @@ can do this using the `CatalogIdentityClient` provided as context to Sign-In resolvers: ```ts +import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; + export default async function createPlugin( env: PluginEnvironment, ): Promise { @@ -140,6 +143,11 @@ export default async function createPlugin( signIn: { resolver: async ({ profile: { email } }, ctx) => { const [id] = email?.split('@') ?? ''; + const userEntityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); // Fetch from an external system that returns entity claims like: // ['user:default/breanna.davison', ...] const ent = await externalSystemClient.getUsernames(email); @@ -150,7 +158,7 @@ export default async function createPlugin( logger: ctx.logger, }); const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, ent: fullEnt }, + claims: { sub: userEntityRef, ent: fullEnt }, }); return { id, token }; }, diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md index 1f771b4eaf..a2277e096c 100644 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md @@ -180,6 +180,35 @@ These should be moved to `links` under the `output` object instead. ``` +## Watch out for `dash-case` + +The nunjucks compiler can run into issues if the `id` fields in your template steps use dash characters, since these IDs translate directly to JavaScript object properties when accessed as output. One possible migration path is to use `camelCase` for your action IDs. + +```diff + steps: +- id: my-custom-action +- ... +- +- id: publish-pull-request +- input: +- repoUrl: {{ steps.my-custom-action.output.repoUrl }} # Will not recognize 'my-custom-action' as a JS property since it contains dashes! + + steps: ++ id: myCustomAction ++ ... ++ ++ id: publishPullRequest ++ input: ++ repoUrl: ${{ steps.myCustomAction.output.repoUrl }} +``` + +Alternatively, it's possible to keep the `dash-case` syntax and use brackets for property access as you would in JavaScript: + +```yaml +input: + repoUrl: ${{ steps['my-custom-action'].output.repoUrl }} +``` + ### Summary Of course, we're always available on [discord](https://discord.gg/MUpMjP2) if diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index d5aad1c541..4417e62c67 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -74,32 +74,26 @@ providers are used. storage solutions, source control systems). - [Instructions for upgrading from Alpha to Beta](how-to-guides.md#how-to-migrate-from-techdocs-alpha-to-beta) +**v1** ✅ + +TechDocs packages: + +- '@backstage/plugin-techdocs' +- '@backstage/plugin-techdocs-backend' +- '@backstage/plugin-techdocs-node' +- '@techdocs/cli' + +was promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + ### **Future work 🔮** -**General Availability (GA) release** - -[Milestone](https://github.com/backstage/backstage/milestone/30) - -- Bugs are rare, TechDocs APIs are stable and scales easily in large - organizations. - Better integration with [Scaffolder V2](https://github.com/backstage/backstage/issues/2771) (e.g. easy to choose and plug documentation template with Software Templates). +- Static site generator agnostic - Possible to configure several aspects about TechDocs (e.g. URL, homepage, theme). - -**Implement Feedback loop** - -[Milestone](https://github.com/backstage/backstage/milestone/31) - -- A feedback loop between documentation reader and writer using TechDocs -- The `+` in `docs-like-code+` experience - -**TechDocs widget framework** - -Platformize TechDocs with a widget framework so that it is easy for TechDocs -contributors to add pieces of functionality and for users to choose which -functionalities they want to adopt. As a pre-requisite, the re-architecture of -TechDocs frontend [RFC](https://github.com/backstage/backstage/issues/3998) -needs to be addressed. +- [TechDocs Addon Framework](https://github.com/backstage/backstage/issues/9636) ## Tech stack diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index 23af83d09b..d781f71917 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -131,15 +131,3 @@ layer for users to determine whether they have the permission to view a particular docs site. There are a handful of features which are extremely hard to develop without a tightly integrated backend in place. Hence, support for `techdocs` without `techdocs-backend` is limited and challenging to develop. - -# Future work - -_Ideas here are far fetched and not in the project's milestone for near future -(~6 months)._ - -We currently depend on MkDocs to parse doc sites written in Markdown. And we -store the generated static assets and re-use it later to render in Backstage. A -better (futuristic) approach will be to directly parse whatever type of source -files you have in your docs repository and directly render in Backstage in -real-time. You can read more in this -[RFC - Simplifying TechDocs Frontend Architecture](https://github.com/backstage/backstage/issues/3998). diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index d16e9142dc..a3f4723173 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -160,10 +160,4 @@ techdocs: # object was not found (e.g. when the cache sercice is unavailable). The # default value is 1000 readTimeout: 500 - - # (Optional and Legacy) Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware - # to serve files from either a local directory or an External storage provider. - # You don't have to specify this anymore. - - storageUrl: http://localhost:7007/api/techdocs/static/docs ``` diff --git a/docs/integrations/aws-s3/locations.md b/docs/integrations/aws-s3/locations.md index b0c6829e2c..9bac28f906 100644 --- a/docs/integrations/aws-s3/locations.md +++ b/docs/integrations/aws-s3/locations.md @@ -37,6 +37,7 @@ integrations: - accessKeyId: ${AWS_ACCESS_KEY_ID} secretAccessKey: ${AWS_SECRET_ACCESS_KEY} roleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/example-role' + externalId: 'some-id' # optional ``` Configuration allows specifying custom S3 endpoint, along with diff --git a/package.json b/package.json index 47770d2b83..f394088825 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "backstage-create": "backstage-cli create --scope backstage --no-private", "create-plugin": "yarn backstage-create --select plugin", "remove-plugin": "backstage-cli remove-plugin", - "release": "node scripts/prepare-release.js && changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install", + "release": "node scripts/prepare-release.js && changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install", "prettier:check": "prettier --check .", "lerna": "lerna", "storybook": "yarn --cwd storybook start", diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.ts b/packages/backend-common/src/reading/AwsS3UrlReader.ts index d5de2a845c..4dd68bf32c 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.ts @@ -177,6 +177,7 @@ export class AwsS3UrlReader implements UrlReader { params: { RoleSessionName: 'backstage-aws-s3-url-reader', RoleArn: roleArn, + ExternalId: integration.config.externalId, }, }); } diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index e1ba4d8199..c7840b71b2 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -37,7 +37,7 @@ "@backstage/config": "^1.0.0", "@backstage/errors": "^1.0.0", "@backstage/types": "^1.0.0", - "ajv": "^7.0.3", + "ajv": "^8.10.0", "json-schema": "^0.4.0", "lodash": "^4.17.21", "uuid": "^8.0.0" diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 7f33e71f1c..1b5b7fe79b 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -54,7 +54,7 @@ export class SchemaValidEntityPolicy implements EntityPolicy { } throw new Error( - `Malformed envelope, ${error.dataPath || ''} ${error.message}`, + `Malformed envelope, ${error.instancePath || ''} ${error.message}`, ); } } diff --git a/packages/catalog-model/src/validation/ajv.ts b/packages/catalog-model/src/validation/ajv.ts index c65c3df18a..15d0b674bf 100644 --- a/packages/catalog-model/src/validation/ajv.ts +++ b/packages/catalog-model/src/validation/ajv.ts @@ -41,7 +41,7 @@ export function throwAjvError( const error = errors[0]; throw new TypeError( - `${error.dataPath || ''} ${error.message}${ + `${error.instancePath || ''} ${error.message}${ error.params ? ` - ${Object.entries(error.params) .map(([key, val]) => `${key}: ${val}`) diff --git a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts index 66259cc63d..dc5aa586aa 100644 --- a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts @@ -74,7 +74,7 @@ export function entityKindSchemaValidator( // Only in the case where kind and/or apiVersion have enum mismatches AND // have NO other errors, we call it a soft error. const softCandidates = validate.errors?.filter(e => - ['/kind', '/apiVersion'].includes(e.dataPath), + ['/kind', '/apiVersion'].includes(e.instancePath), ); if ( softCandidates?.length && diff --git a/packages/cli/.snyk b/packages/cli/.snyk index 0a32a6ac12..d012272068 100644 --- a/packages/cli/.snyk +++ b/packages/cli/.snyk @@ -57,6 +57,11 @@ ignore: reason: This is a development dependency only and is therefore acceptable expires: 2031-09-06T17:18:55.027Z created: 2021-09-06T17:18:55.027Z + 'snyk:lic:npm:eslint-plugin-deprecation:LGPL-3.0': + - '*': + reason: This is a development dependency only and is therefore acceptable + expires: 2032-03-22T13:37:00.000Z + created: 2022-03-22T13:37:00.000Z SNYK-JS-UNSETVALUE-2400660: - '*': reason: >- diff --git a/packages/cli/package.json b/packages/cli/package.json index f9147b924e..66b0456e32 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,15 +68,16 @@ "esbuild": "^0.14.10", "esbuild-loader": "^2.18.0", "eslint": "^8.6.0", - "eslint-webpack-plugin": "^2.6.0", "eslint-config-prettier": "^8.3.0", "eslint-formatter-friendly": "^7.0.0", + "eslint-plugin-deprecation": "^1.3.2", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^25.3.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-monorepo": "^0.3.2", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", + "eslint-webpack-plugin": "^2.6.0", "express": "^4.17.1", "fork-ts-checker-webpack-plugin": "^7.0.0-alpha.8", "fs-extra": "10.0.1", @@ -86,13 +87,13 @@ "inquirer": "^8.2.0", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", - "json-schema": "^0.4.0", "jest-transform-yaml": "^1.0.0", + "json-schema": "^0.4.0", "lodash": "^4.17.21", - "minimatch": "5.0.1", "mini-css-extract-plugin": "^2.4.2", - "npm-packlist": "^3.0.0", + "minimatch": "5.0.1", "node-libs-browser": "^2.2.1", + "npm-packlist": "^3.0.0", "ora": "^5.3.0", "postcss": "^8.1.0", "process": "^0.11.10", @@ -123,9 +124,9 @@ "devDependencies": { "@backstage/backend-common": "^0.13.1", "@backstage/config": "^1.0.0", + "@backstage/core-app-api": "^1.0.0", "@backstage/core-components": "^0.9.2", "@backstage/core-plugin-api": "^1.0.0", - "@backstage/core-app-api": "^1.0.0", "@backstage/dev-utils": "^1.0.0", "@backstage/test-utils": "^1.0.0", "@backstage/theme": "^0.2.15", @@ -147,9 +148,9 @@ "@types/yarnpkg__lockfile": "^1.1.4", "del": "^6.0.0", "mock-fs": "^5.1.0", + "msw": "^0.35.0", "nodemon": "^2.0.2", - "ts-node": "^10.0.0", - "msw": "^0.35.0" + "ts-node": "^10.0.0" }, "peerDependencies": { "@microsoft/api-extractor": "^7.19.2" diff --git a/packages/cli/src/commands/fix.ts b/packages/cli/src/commands/fix.ts new file mode 100644 index 0000000000..e4cdc4dc84 --- /dev/null +++ b/packages/cli/src/commands/fix.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { paths } from '../lib/paths'; +import { ESLint } from 'eslint'; +import { join as joinPath, basename } from 'path'; +import fs from 'fs-extra'; +import { isChildPath } from '@backstage/cli-common'; +import { PackageGraph } from '../lib/monorepo'; + +function isTestPath(filePath: string) { + if (!isChildPath(joinPath(paths.targetDir, 'src'), filePath)) { + return true; + } + const name = basename(filePath); + return ( + name.startsWith('setupTests.') || + name.includes('.test.') || + name.includes('.stories.') + ); +} + +export async function command() { + const pkgJsonPath = paths.resolveTarget('package.json'); + const pkg = await fs.readJson(pkgJsonPath); + if (pkg.workspaces) { + throw new Error( + 'Adding dependencies to the workspace root is not supported', + ); + } + + const packages = await PackageGraph.listTargetPackages(); + const localPackageVersions = new Map( + packages.map(p => [p.packageJson.name, p.packageJson.version]), + ); + + const eslint = new ESLint({ + cwd: paths.targetDir, + overrideConfig: { + plugins: ['monorepo'], + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + `!${joinPath(paths.targetDir, 'src/**')}`, + joinPath(paths.targetDir, 'src/**/*.test.*'), + joinPath(paths.targetDir, 'src/**/*.stories.*'), + joinPath(paths.targetDir, 'src/setupTests.*'), + ], + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + }, + }, + extensions: ['jsx', 'ts', 'tsx', 'mjs', 'cjs'], + }); + + const results = await eslint.lintFiles(['.']); + + const addedDeps = new Set(); + const addedDevDeps = new Set(); + const removedDevDeps = new Set(); + + for (const result of results) { + for (const message of result.messages) { + // Just in case + if (message.ruleId !== 'import/no-extraneous-dependencies') { + continue; + } + + const match = message.message.match(/^'([^']*)' should be listed/); + if (!match) { + continue; + } + const packageName = match[1]; + if (!localPackageVersions.has(packageName)) { + continue; + } + + if (message.message.endsWith('not devDependencies.')) { + addedDeps.add(packageName); + removedDevDeps.add(packageName); + } else if (isTestPath(result.filePath)) { + addedDevDeps.add(packageName); + } else { + addedDeps.add(packageName); + } + } + } + + if (addedDeps.size || addedDevDeps.size || removedDevDeps.size) { + for (const name of addedDeps) { + if (!pkg.dependencies) { + pkg.dependencies = {}; + } + pkg.dependencies[name] = `^${localPackageVersions.get(name)}`; + } + for (const name of addedDevDeps) { + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + pkg.devDependencies[name] = `^${localPackageVersions.get(name)}`; + } + for (const name of removedDevDeps) { + delete pkg.devDependencies[name]; + } + if (Object.keys(pkg.devDependencies).length === 0) { + delete pkg.devDependencies; + } + + if (pkg.dependencies) { + pkg.dependencies = Object.fromEntries( + Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)), + ); + } + if (pkg.devDependencies) { + pkg.devDependencies = Object.fromEntries( + Object.entries(pkg.devDependencies).sort(([a], [b]) => + a.localeCompare(b), + ), + ); + } + + await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 }); + } +} diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index bc427bb19b..2f345e8663 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -59,6 +59,14 @@ export function registerRepoCommand(program: CommanderStatic) { ) .option('--fix', 'Attempt to automatically fix violations') .action(lazy(() => import('./repo/lint').then(m => m.command))); + + command + .command('list-deprecations', { hidden: true }) + .description('List deprecations. [EXPERIMENTAL]') + .option('--json', 'Output as JSON') + .action( + lazy(() => import('./repo/list-deprecations').then(m => m.command)), + ); } export function registerScriptCommand(program: CommanderStatic) { @@ -125,6 +133,12 @@ export function registerScriptCommand(program: CommanderStatic) { .description('Run tests, forwarding args to Jest, defaulting to watch mode') .action(lazy(() => import('./testCommand').then(m => m.default))); + command + .command('fix', { hidden: true }) + .description('Applies automated fixes to the package. [EXPERIMENTAL]') + .option('--deps', 'Only fix monorepo dependencies in package.json') + .action(lazy(() => import('./fix').then(m => m.command))); + command .command('clean') .description('Delete cache directories') diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 153bd16597..9a2a3b2e3e 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -63,7 +63,7 @@ export default async (cmd: Command) => { const data = await readPluginData(); const templateFiles = await diffTemplateFiles('default-plugin', data); await handleAllFiles(fileHandlers, templateFiles, promptFunc); - await finalize(); + finalize(); }; // Reads templating data from the existing plugin diff --git a/packages/cli/src/commands/repo/list-deprecations.ts b/packages/cli/src/commands/repo/list-deprecations.ts new file mode 100644 index 0000000000..b53118cc40 --- /dev/null +++ b/packages/cli/src/commands/repo/list-deprecations.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import { ESLint } from 'eslint'; +import { Command } from 'commander'; +import { join as joinPath, relative as relativePath } from 'path'; +import { paths } from '../../lib/paths'; +import { PackageGraph } from '../../lib/monorepo'; + +export async function command(cmd: Command) { + const packages = await PackageGraph.listTargetPackages(); + + const eslint = new ESLint({ + cwd: paths.targetDir, + overrideConfig: { + plugins: ['deprecation'], + rules: { + 'deprecation/deprecation': 'error', + }, + parserOptions: { + project: [paths.resolveTargetRoot('tsconfig.json')], + }, + }, + extensions: ['jsx', 'ts', 'tsx', 'mjs', 'cjs'], + }); + + const { stderr } = process; + if (stderr.isTTY) { + stderr.write('Initializing TypeScript...'); + } + + const deprecations = []; + for (const [index, pkg] of packages.entries()) { + const results = await eslint.lintFiles(joinPath(pkg.dir, 'src')); + for (const result of results) { + for (const message of result.messages) { + if (message.ruleId !== 'deprecation/deprecation') { + continue; + } + + const path = relativePath(paths.targetRoot, result.filePath); + deprecations.push({ + path, + message: message.message, + line: message.line, + column: message.column, + }); + } + } + + if (stderr.isTTY) { + stderr.clearLine(0); + stderr.cursorTo(0); + stderr.write(`Scanning packages ${index + 1}/${packages.length}`); + } + } + + if (stderr.isTTY) { + stderr.clearLine(0); + stderr.cursorTo(0); + } + + if (cmd.json) { + console.log(JSON.stringify(deprecations, null, 2)); + } else { + for (const d of deprecations) { + const location = `${d.path}:${d.line}:${d.column}`; + const wrappedMessage = d.message.replace(/\r?\n\s*/g, ' '); + console.log(`${location} - ${chalk.yellow(wrappedMessage)}`); + } + } + + if (deprecations.length > 0) { + process.exit(1); + } +} diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 260a2ca882..24ed7c140c 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -39,7 +39,7 @@ "@backstage/errors": "^1.0.0", "@backstage/types": "^1.0.0", "@types/json-schema": "^7.0.6", - "ajv": "^7.0.3", + "ajv": "^8.10.0", "chokidar": "^3.5.2", "fs-extra": "10.0.1", "json-schema": "^0.4.0", diff --git a/packages/config-loader/src/lib/schema/compile.test.ts b/packages/config-loader/src/lib/schema/compile.test.ts index 7b7db87c8f..999dabd307 100644 --- a/packages/config-loader/src/lib/schema/compile.test.ts +++ b/packages/config-loader/src/lib/schema/compile.test.ts @@ -32,9 +32,9 @@ describe('compileConfigSchemas', () => { errors: [ { keyword: 'type', - dataPath: '/a', + instancePath: '/a', schemaPath: '#/properties/a/type', - message: 'should be string', + message: 'must be string', params: { type: 'string' }, }, ], @@ -46,9 +46,9 @@ describe('compileConfigSchemas', () => { errors: [ { keyword: 'type', - dataPath: '/b', + instancePath: '/b', schemaPath: '#/properties/b/type', - message: 'should be number', + message: 'must be number', params: { type: 'number' }, }, ], diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index b7f705751e..c1059a28ae 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -57,11 +57,11 @@ export function compileConfigSchemas( }, compile(visibility: ConfigVisibility) { return (_data, context) => { - if (context?.dataPath === undefined) { + if (context?.instancePath === undefined) { return false; } if (visibility && visibility !== 'backend') { - const normalizedPath = context.dataPath.replace( + const normalizedPath = context.instancePath.replace( /\['?(.*?)'?\]/g, (_, segment) => `/${segment}`, ); @@ -77,10 +77,10 @@ export function compileConfigSchemas( metaSchema: { type: 'string' }, compile(deprecationDescription: string) { return (_data, context) => { - if (context?.dataPath === undefined) { + if (context?.instancePath === undefined) { return false; } - const normalizedPath = context.dataPath.replace( + const normalizedPath = context.instancePath.replace( /\['?(.*?)'?\]/g, (_, segment) => `/${segment}`, ); diff --git a/packages/config-loader/src/lib/schema/filtering.test.ts b/packages/config-loader/src/lib/schema/filtering.test.ts index df06975c9f..3a5de7e2c0 100644 --- a/packages/config-loader/src/lib/schema/filtering.test.ts +++ b/packages/config-loader/src/lib/schema/filtering.test.ts @@ -244,21 +244,21 @@ describe('filterErrorsByVisibility', () => { const errors = [ { keyword: 'something', - dataPath: '/a', + instancePath: '/a', schemaPath: '#/properties/a/something', params: {}, message: 'a', }, { keyword: 'something', - dataPath: '/b', + instancePath: '/b', schemaPath: '#/properties/b/something', params: {}, message: 'b', }, { keyword: 'something', - dataPath: '/c', + instancePath: '/c', schemaPath: '#/properties/c/something', params: {}, message: 'c', @@ -314,35 +314,35 @@ describe('filterErrorsByVisibility', () => { const errors = [ { keyword: 'type', - dataPath: '/a', + instancePath: '/a', schemaPath: '#/properties/a/type', params: { type: 'number' }, message: 'a', }, { keyword: 'type', - dataPath: '/b', + instancePath: '/b', schemaPath: '#/properties/b/type', params: { type: 'string' }, message: 'b', }, { keyword: 'type', - dataPath: '/c', + instancePath: '/c', schemaPath: '#/properties/c/type', params: { type: 'array' }, message: 'c', }, { keyword: 'type', - dataPath: '/c', + instancePath: '/c', schemaPath: '#/properties/c/type', params: { type: 'object' }, message: 'd', }, { keyword: 'type', - dataPath: '/c', + instancePath: '/c', schemaPath: '#/properties/c/type', params: { type: 'null' }, message: 'e', @@ -373,21 +373,21 @@ describe('filterErrorsByVisibility', () => { const errors = [ { keyword: 'required', - dataPath: '/a', + instancePath: '/a', schemaPath: '#/properties/o/required', params: { missingProperty: 'a' }, message: 'a', }, { keyword: 'required', - dataPath: '/b', + instancePath: '/b', schemaPath: '#/properties/o/required', params: { missingProperty: 'b' }, message: 'b', }, { keyword: 'required', - dataPath: '/c', + instancePath: '/c', schemaPath: '#/properties/o/required', params: { missingProperty: 'c' }, message: 'c', diff --git a/packages/config-loader/src/lib/schema/filtering.ts b/packages/config-loader/src/lib/schema/filtering.ts index 2855979b1f..e9a0327e06 100644 --- a/packages/config-loader/src/lib/schema/filtering.ts +++ b/packages/config-loader/src/lib/schema/filtering.ts @@ -173,7 +173,7 @@ export function filterErrorsByVisibility( } const vis = - visibilityByDataPath.get(error.dataPath) ?? DEFAULT_CONFIG_VISIBILITY; + visibilityByDataPath.get(error.instancePath) ?? DEFAULT_CONFIG_VISIBILITY; return vis && includeVisibilities.includes(vis); }); } diff --git a/packages/config-loader/src/lib/schema/load.test.ts b/packages/config-loader/src/lib/schema/load.test.ts index 8510ccb1d8..41971f7d01 100644 --- a/packages/config-loader/src/lib/schema/load.test.ts +++ b/packages/config-loader/src/lib/schema/load.test.ts @@ -105,7 +105,7 @@ describe('loadConfigSchema', () => { expect(() => schema2.process([...configs, { data: { key1: 3 }, context: 'test2' }]), ).toThrow( - 'Config validation failed, Config should be string { type=string } at /key1', + 'Config validation failed, Config must be string { type=string } at /key1', ); await expect( @@ -142,7 +142,7 @@ describe('loadConfigSchema', () => { ]; expect(() => schema.process(configs)).toThrow( - 'Config validation failed, Config should be number { type=number } at /key2', + 'Config validation failed, Config must be number { type=number } at /key2', ); expect(schema.process(configs, { visibility: ['frontend'] })).toEqual([ { @@ -151,7 +151,7 @@ describe('loadConfigSchema', () => { }, ]); expect(() => schema.process(configs, { visibility: ['secret'] })).toThrow( - 'Config validation failed, Config should be number { type=number } at /key2', + 'Config validation failed, Config must be number { type=number } at /key2', ); }); @@ -203,12 +203,12 @@ describe('loadConfigSchema', () => { }, ]); expect(() => schema.process(mkConfig({ y: 1 }))).toThrow( - 'Config validation failed, Config should be string { type=string } at /nested/0/y', + 'Config validation failed, Config must be string { type=string } at /nested/0/y', ); expect(() => schema.process(mkConfig({ y: 1 }), { visibility: ['frontend'] }), ).toThrow( - 'Config validation failed, Config should be string { type=string } at /nested/0/y', + 'Config validation failed, Config must be string { type=string } at /nested/0/y', ); expect( schema.process(mkConfig({ x: 'a' }), { visibility: ['frontend'] }), @@ -229,7 +229,7 @@ describe('loadConfigSchema', () => { expect(() => schema.process(mkConfig({ y: 'aaaa' }), { visibility: ['frontend'] }), ).toThrow( - 'Config validation failed, Config should match pattern "^...$" { pattern=^...$ } at /nested/0/y', + 'Config validation failed, Config must match pattern "^...$" { pattern=^...$ } at /nested/0/y', ); // This is a bit of an edge case where we have a structural error, these should always be reported @@ -238,7 +238,7 @@ describe('loadConfigSchema', () => { visibility: ['frontend'], }), ).toThrow( - 'Config validation failed, Config should be array { type=array } at /nested', + 'Config validation failed, Config must be array { type=array } at /nested', ); }); }); @@ -273,7 +273,7 @@ describe('loadConfigSchema', () => { visibility: ['frontend'], }), ).toThrow( - "Config should have required property 'x a' { missingProperty=x a } at /other", + "Config must have required property 'x a' { missingProperty=x a } at /other", ); }); }); diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index aa7ed06305..959b438371 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -41,11 +41,11 @@ export type LoadConfigSchemaOptions = }; function errorsToError(errors: ValidationError[]): Error { - const messages = errors.map(({ dataPath, message, params }) => { + const messages = errors.map(({ instancePath, message, params }) => { const paramStr = Object.entries(params) .map(([name, value]) => `${name}=${value}`) .join(' '); - return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; + return `Config ${message || ''} { ${paramStr} } at ${instancePath}`; }); const error = new Error(`Config validation failed, ${messages.join('; ')}`); (error as any).messages = messages; diff --git a/packages/config-loader/src/lib/schema/types.ts b/packages/config-loader/src/lib/schema/types.ts index 75993b3c78..cc62192e30 100644 --- a/packages/config-loader/src/lib/schema/types.ts +++ b/packages/config-loader/src/lib/schema/types.ts @@ -53,7 +53,7 @@ export const DEFAULT_CONFIG_VISIBILITY: ConfigVisibility = 'backend'; */ export type ValidationError = { keyword: string; - dataPath: string; + instancePath: string; schemaPath: string; params: Record; propertyName?: string; diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 9a3287e285..7ceb2e8548 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -226,7 +226,7 @@ export async function loadConfig( } } - const envConfigs = await readEnvConfig(process.env); + const envConfigs = readEnvConfig(process.env); const watchConfigFile = (watchProp: LoadConfigOptionsWatch) => { let watchedFiles = Array.from(loadedPaths); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index 2f4ec6b536..157abf9d7c 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -90,4 +90,32 @@ describe('PluginProtocolResolverFetchMiddleware', () => { expect(resolve).toHaveBeenLastCalledWith(host); }, ); + + it('properly supports transferring request bodies too', async () => { + const resolve = jest.fn(); + const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const middleware = new PluginProtocolResolverFetchMiddleware(discoveryApi); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + resolve.mockResolvedValue('https://elsewhere.com'); + + await outer('plugin://a', { + method: 'POST', + body: '123', + }); + + expect(inner.mock.calls[0][0]).toBe('https://elsewhere.com'); + expect(inner.mock.calls[0][1].body).toBe('123'); + + await outer( + new Request('plugin://a', { + method: 'POST', + body: '123', + }), + ); + + expect(inner.mock.calls[1][0]).toBe('https://elsewhere.com'); + expect(inner.mock.calls[1][1].body).toEqual(Buffer.from('123', 'utf8')); + }); }); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts index 72834731fe..6265e8d3d2 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts @@ -55,7 +55,7 @@ export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware { } const target = `${join(base, pathname)}${search}${hash}`; - return next(target, request); + return next(target, typeof input === 'string' ? init : input); }; } } diff --git a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts index 83d6035171..3f8622890b 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts @@ -185,7 +185,7 @@ describe('GheAuth AuthSessionStore', () => { await expect( withLogCollector(async () => { - await secondStore.setSession('no' as any); + secondStore.setSession('no' as any); }), ).resolves.toMatchObject({ warn: [ diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 84bca54a90..d86acb291a 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -35,6 +35,7 @@ export type AwsS3IntegrationConfig = { accessKeyId?: string; secretAccessKey?: string; roleArn?: string; + externalId?: string; }; // @public diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index dc62695385..e2d26be28e 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -225,6 +225,12 @@ export interface Config { * @visibility backend */ roleArn?: string; + + /** + * External ID to use when assuming role + * @visibility backend + */ + externalId?: string; }>; }; } diff --git a/packages/integration/src/awsS3/config.ts b/packages/integration/src/awsS3/config.ts index 45cbdbe210..477741b839 100644 --- a/packages/integration/src/awsS3/config.ts +++ b/packages/integration/src/awsS3/config.ts @@ -59,6 +59,11 @@ export type AwsS3IntegrationConfig = { * (Optional) ARN of role to be assumed */ roleArn?: string; + + /** + * (Optional) External ID to use when assuming role + */ + externalId?: string; }; /** @@ -98,6 +103,7 @@ export function readAwsS3IntegrationConfig( const accessKeyId = config.getOptionalString('accessKeyId'); const secretAccessKey = config.getOptionalString('secretAccessKey'); const roleArn = config.getOptionalString('roleArn'); + const externalId = config.getOptionalString('externalId'); return { host, @@ -106,6 +112,7 @@ export function readAwsS3IntegrationConfig( accessKeyId, secretAccessKey, roleArn, + externalId, }; } diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 39e5da3df2..32ca3dfc02 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -48,14 +48,13 @@ describe('wrapInTestApp', () => { await Promise.resolve(); }); - expect(error).toEqual([ - expect.stringMatching( - /^Warning: An update to %s inside a test was not wrapped in act\(...\)/, + expect( + error.some(e => + e.includes( + 'Warning: An update to %s inside a test was not wrapped in act(...)', + ), ), - expect.stringMatching( - /^Warning: An update to %s inside a test was not wrapped in act\(...\)/, - ), - ]); + ).toBeTruthy(); }); it('should render a component in a test app without warning about missing act()', async () => { diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 7ba3059626..25e6052afa 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend +## 0.12.3 + +### Patch Changes + +- Fix migrations to do the right thing on sqlite databases, and reapply the column type fix for those who are _not_ on sqlite databases. + + Reconstruction of #10317 in the form of a patch release instead. + ## 0.12.2 ### Patch Changes diff --git a/plugins/auth-backend/migrations/20210326100300_timestamptz.js b/plugins/auth-backend/migrations/20210326100300_timestamptz.js index 79b839dc6a..4a7aa3e97d 100644 --- a/plugins/auth-backend/migrations/20210326100300_timestamptz.js +++ b/plugins/auth-backend/migrations/20210326100300_timestamptz.js @@ -21,7 +21,7 @@ */ exports.up = async function up(knex) { // Sqlite does not support alter column. - if (knex.client.config.client.includes('sqlite3')) { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('signing_keys', table => { table .timestamp('created_at', { useTz: true, precision: 0 }) @@ -38,7 +38,7 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { // Sqlite does not support alter column. - if (knex.client.config.client.includes('sqlite3')) { + if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('signing_keys', table => { table .timestamp('created_at', { useTz: false, precision: 0 }) diff --git a/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js b/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js new file mode 100644 index 0000000000..ab7e713b96 --- /dev/null +++ b/plugins/auth-backend/migrations/20220321100910_timestamptz_again.js @@ -0,0 +1,58 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +// NOTE: This may look like a plain duplicate of the previous one, but that +// file had a bug added when improving sqlite driver support: +// https://github.com/backstage/backstage/pull/10053/files#diff-30bb343265e71ca2f1cdcccd5ac8fdbb2a597507c5531bf26945059783377b15R24 +// Since the old file was released to end users, those who created a new +// Backstage app specifically for PostgreSQL since the release will be missing +// this fix on their table. So we re-apply it. + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // Sqlite does not support alter column. + if (!knex.client.config.client.includes('sqlite3')) { + await knex.schema.alterTable('signing_keys', table => { + table + .timestamp('created_at', { useTz: true, precision: 0 }) + .notNullable() + .defaultTo(knex.fn.now()) + .comment('The creation time of the key') + .alter({ alterType: true }); + }); + } +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + // Sqlite does not support alter column. + if (!knex.client.config.client.includes('sqlite3')) { + await knex.schema.alterTable('signing_keys', table => { + table + .timestamp('created_at', { useTz: false, precision: 0 }) + .notNullable() + .defaultTo(knex.fn.now()) + .comment('The creation time of the key') + .alter({ alterType: true }); + }); + } +}; diff --git a/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketRepositoryParser.test.ts index 58845fc3d1..41f467ebaa 100644 --- a/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketRepositoryParser.test.ts +++ b/plugins/catalog-backend-module-bitbucket/src/lib/BitbucketRepositoryParser.test.ts @@ -30,7 +30,7 @@ describe('BitbucketRepositoryParser', () => { presence: 'optional', }), ]; - const actual = await defaultRepositoryParser({ + const actual = defaultRepositoryParser({ target: `${browseUrl}${path}`, }); diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index 061be0a767..c9fe0da0a3 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -164,8 +164,9 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const { toAdd, toUpsert, toRemove } = await this.createDelta(tx, options); if (toRemove.length) { - // TODO(freben): Batch split, to not hit variable limits? - /* + let removedCount = 0; + for (const refs of lodash.chunk(toRemove, 1000)) { + /* WITH RECURSIVE -- All the nodes that can be reached downwards from our root descendants(root_id, entity_ref) AS ( @@ -200,78 +201,79 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { -- Exclude all lines that had such a foreign ancestor WHERE ancestors.root_id IS NULL; */ - const removedCount = await tx('refresh_state') - .whereIn('entity_ref', function orphanedEntityRefs(orphans) { - return ( - orphans - // All the nodes that can be reached downwards from our root - .withRecursive('descendants', function descendants(outer) { - return outer - .select({ root_id: 'id', entity_ref: 'target_entity_ref' }) - .from('refresh_state_references') - .where('source_key', options.sourceKey) - .whereIn('target_entity_ref', toRemove) - .union(function recursive(inner) { - return inner - .select({ - root_id: 'descendants.root_id', - entity_ref: - 'refresh_state_references.target_entity_ref', - }) - .from('descendants') - .join('refresh_state_references', { - 'descendants.entity_ref': - 'refresh_state_references.source_entity_ref', - }); - }); - }) - // All the nodes that can be reached upwards from the descendants - .withRecursive('ancestors', function ancestors(outer) { - return outer - .select({ - root_id: tx.raw('CAST(NULL as INT)', []), - via_entity_ref: 'entity_ref', - to_entity_ref: 'entity_ref', - }) - .from('descendants') - .union(function recursive(inner) { - return inner - .select({ - root_id: tx.raw( - 'CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END', - [], - ), - via_entity_ref: 'source_entity_ref', - to_entity_ref: 'ancestors.to_entity_ref', - }) - .from('ancestors') - .join('refresh_state_references', { - target_entity_ref: 'ancestors.via_entity_ref', - }); - }); - }) - // Start out with all of the descendants - .select('descendants.entity_ref') - .from('descendants') - // Expand with all ancestors that point to those, but aren't the current root - .leftOuterJoin('ancestors', function keepaliveRoots() { - this.on( - 'ancestors.to_entity_ref', - '=', - 'descendants.entity_ref', - ); - this.andOnNotNull('ancestors.root_id'); - this.andOn('ancestors.root_id', '!=', 'descendants.root_id'); - }) - .whereNull('ancestors.root_id') - ); - }) - .delete(); + removedCount += await tx('refresh_state') + .whereIn('entity_ref', function orphanedEntityRefs(orphans) { + return ( + orphans + // All the nodes that can be reached downwards from our root + .withRecursive('descendants', function descendants(outer) { + return outer + .select({ root_id: 'id', entity_ref: 'target_entity_ref' }) + .from('refresh_state_references') + .where('source_key', options.sourceKey) + .whereIn('target_entity_ref', refs) + .union(function recursive(inner) { + return inner + .select({ + root_id: 'descendants.root_id', + entity_ref: + 'refresh_state_references.target_entity_ref', + }) + .from('descendants') + .join('refresh_state_references', { + 'descendants.entity_ref': + 'refresh_state_references.source_entity_ref', + }); + }); + }) + // All the nodes that can be reached upwards from the descendants + .withRecursive('ancestors', function ancestors(outer) { + return outer + .select({ + root_id: tx.raw('CAST(NULL as INT)', []), + via_entity_ref: 'entity_ref', + to_entity_ref: 'entity_ref', + }) + .from('descendants') + .union(function recursive(inner) { + return inner + .select({ + root_id: tx.raw( + 'CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END', + [], + ), + via_entity_ref: 'source_entity_ref', + to_entity_ref: 'ancestors.to_entity_ref', + }) + .from('ancestors') + .join('refresh_state_references', { + target_entity_ref: 'ancestors.via_entity_ref', + }); + }); + }) + // Start out with all of the descendants + .select('descendants.entity_ref') + .from('descendants') + // Expand with all ancestors that point to those, but aren't the current root + .leftOuterJoin('ancestors', function keepaliveRoots() { + this.on( + 'ancestors.to_entity_ref', + '=', + 'descendants.entity_ref', + ); + this.andOnNotNull('ancestors.root_id'); + this.andOn('ancestors.root_id', '!=', 'descendants.root_id'); + }) + .whereNull('ancestors.root_id') + ); + }) + .delete(); - await tx('refresh_state_references') - .where('source_key', '=', options.sourceKey) - .whereIn('target_entity_ref', toRemove) - .delete(); + await tx('refresh_state_references') + .where('source_key', '=', options.sourceKey) + .whereIn('target_entity_ref', refs) + .delete(); + } this.options.logger.debug( `removed, ${removedCount} entities: ${JSON.stringify(toRemove)}`, diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index fdb07658d0..f1278c6f60 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -69,6 +69,7 @@ export const EntitySearchBar = () => { { ); return ( toggleStarredEntity()} diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index d1625cb65e..bc7d2cb029 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -34,6 +34,7 @@ const mockedJenkinsClient = { const mockedJenkins = jenkins as jest.Mocked; mockedJenkins.mockReturnValue(mockedJenkinsClient); +const resourceRef = 'component:default/example-component'; const jobFullName = 'example-jobName/foo'; const buildNumber = 19; const jenkinsInfo: JenkinsInfo = { @@ -413,7 +414,7 @@ describe('JenkinsApi', () => { ); }); it('buildProject', async () => { - await jenkinsApi.buildProject(jenkinsInfo, jobFullName); + await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, @@ -431,7 +432,7 @@ describe('JenkinsApi', () => { ]); await expect(() => - jenkinsApi.buildProject(jenkinsInfo, jobFullName), + jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef), ).rejects.toThrow(NotAllowedError); }); @@ -442,7 +443,7 @@ describe('JenkinsApi', () => { }, ]); - await jenkinsApi.buildProject(jenkinsInfo, jobFullName); + await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, headers: jenkinsInfo.headers, @@ -453,7 +454,7 @@ describe('JenkinsApi', () => { it('buildProject with crumbIssuer option', async () => { const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true }; - await jenkinsApi.buildProject(info, jobFullName); + await jenkinsApi.buildProject(info, jobFullName, resourceRef); expect(mockedJenkins).toHaveBeenCalledWith({ baseUrl: jenkinsInfo.baseUrl, diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index da16bbc553..ddc2bb0ea3 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -139,7 +139,7 @@ export class JenkinsApiImpl { async buildProject( jenkinsInfo: JenkinsInfo, jobFullName: string, - resourceRef?: string, + resourceRef: string, options?: { token?: string }, ) { const client = await JenkinsApiImpl.getClient(jenkinsInfo); diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index d73cb391fc..f1ab23c320 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -20,6 +20,7 @@ import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -126,6 +127,22 @@ export type LogEvent = { taskId: string; }; +// @alpha +export type NextRouterProps = { + components?: { + TemplateCardComponent?: React_2.ComponentType<{ + template: TemplateEntityV1beta3; + }>; + TaskPageComponent?: React_2.ComponentType<{}>; + }; + groups?: TemplateGroupFilter[]; +}; + +// @alpha +export const NextScaffolderPage: ( + props: PropsWithChildren, +) => JSX.Element; + // @public export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< string, @@ -354,6 +371,12 @@ export type TaskPageProps = { loadingText?: string; }; +// @alpha (undocumented) +export type TemplateGroupFilter = { + title?: React_2.ReactNode; + filter: (entity: Entity) => boolean; +}; + // @public export type TemplateParameterSchema = { title: string; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 91f67b4669..974c6ce3fc 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -9,7 +9,8 @@ "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "frontend-plugin" @@ -24,7 +25,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "start": "backstage-cli package start", "lint": "backstage-cli package lint", "test": "backstage-cli package test", @@ -95,6 +96,7 @@ "msw": "^0.35.0" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index bdca2a0952..fc132f644c 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -55,6 +55,11 @@ export { RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin, + NextScaffolderPage, } from './plugin'; export * from './components'; export type { TaskPageProps } from './components/TaskPage'; + +/** next exports */ +export type { NextRouterProps } from './next'; +export type { TemplateGroupFilter } from './next'; diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx new file mode 100644 index 0000000000..5094c786e0 --- /dev/null +++ b/plugins/scaffolder/src/next/Router/Router.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TemplateListPage } from '../TemplateListPage'; +import { TemplateWizardPage } from '../TemplateWizardPage'; +import { Router } from './Router'; +import { renderInTestApp } from '@backstage/test-utils'; +import { + createScaffolderFieldExtension, + ScaffolderFieldExtensions, +} from '../../extensions'; +import { scaffolderPlugin } from '../../plugin'; + +jest.mock('../TemplateListPage', () => ({ + TemplateListPage: jest.fn(() => null), +})); + +jest.mock('../TemplateWizardPage', () => ({ + TemplateWizardPage: jest.fn(() => null), +})); + +describe('Router', () => { + beforeEach(() => { + (TemplateWizardPage as jest.Mock).mockClear(); + (TemplateListPage as jest.Mock).mockClear(); + }); + describe('/', () => { + it('should render the TemplateListPage', async () => { + await renderInTestApp(); + + expect(TemplateListPage).toHaveBeenCalled(); + }); + }); + + describe('/templates/:templateName', () => { + it('should render the TemplateWizard page', async () => { + await renderInTestApp(, { routeEntries: ['/templates/foo'] }); + + expect(TemplateWizardPage).toHaveBeenCalled(); + }); + + it('should extract the fieldExtensions and pass them through', async () => { + const mockComponent = () => null; + const CustomFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + name: 'custom', + component: mockComponent, + }), + ); + + await renderInTestApp( + + + + + , + { routeEntries: ['/templates/foo'] }, + ); + + const mock = TemplateWizardPage as jest.Mock; + const [{ customFieldExtensions }] = mock.mock.calls[0]; + + expect(customFieldExtensions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'custom', component: mockComponent }), + ]), + ); + }); + }); +}); diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx new file mode 100644 index 0000000000..580d41d27b --- /dev/null +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { PropsWithChildren } from 'react'; +import { Routes, Route, useOutlet } from 'react-router'; +import { TemplateListPage } from '../TemplateListPage'; +import { SecretsContextProvider } from '../TemplateWizardPage/SecretsContext'; +import { TemplateWizardPage } from '../TemplateWizardPage'; +import { + FieldExtensionOptions, + FIELD_EXTENSION_WRAPPER_KEY, + FIELD_EXTENSION_KEY, + DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, +} from '../../extensions'; + +import { useElementFilter } from '@backstage/core-plugin-api'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; + +/** + * The Props for the Scaffolder Router + * + * @alpha + */ +export type NextRouterProps = { + components?: { + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; + TaskPageComponent?: React.ComponentType<{}>; + }; + groups?: TemplateGroupFilter[]; +}; + +/** + * The Scaffolder Router + * + * @alpha + */ +export const Router = (props: PropsWithChildren) => { + const { components: { TemplateCardComponent } = {} } = props; + + const outlet = useOutlet() || props.children; + + const customFieldExtensions = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: FIELD_EXTENSION_WRAPPER_KEY, + }) + .findComponentData({ + key: FIELD_EXTENSION_KEY, + }), + ); + + const fieldExtensions = [ + ...customFieldExtensions, + ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( + ({ name }) => + !customFieldExtensions.some( + customFieldExtension => customFieldExtension.name === name, + ), + ), + ]; + + return ( + + + } + /> + + + + + } + /> + + ); +}; diff --git a/plugins/scaffolder/src/next/Router/index.ts b/plugins/scaffolder/src/next/Router/index.ts new file mode 100644 index 0000000000..dac1db7b3a --- /dev/null +++ b/plugins/scaffolder/src/next/Router/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { Router } from './Router'; +export type { NextRouterProps } from './Router'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx new file mode 100644 index 0000000000..f02ca51354 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx @@ -0,0 +1,152 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; +import { CategoryPicker } from './CategoryPicker'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { fireEvent } from '@testing-library/react'; + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntityTypeFilter: jest.fn(), +})); + +describe('CategoryPicker', () => { + const mockAlertApi = { post: jest.fn() }; + + beforeEach(() => { + mockAlertApi.post.mockClear(); + }); + + it('should post the error to errorApi if an errors is returned', async () => { + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + error: new Error('something broked'), + }); + + await renderInTestApp( + + + , + ); + + expect(mockAlertApi.post).toHaveBeenCalledWith({ + message: expect.stringContaining('something broked'), + severity: 'error', + }); + }); + + it('should render loading if the hook is loading', async () => { + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + loading: true, + }); + + const { findByTestId } = await renderInTestApp( + + + , + ); + + expect(await findByTestId('progress')).toBeInTheDocument(); + }); + + it('should not render if there is no available types', async () => { + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: null, + }); + + const { queryByText } = await renderInTestApp( + + + , + ); + + expect(queryByText('Categories')).not.toBeInTheDocument(); + }); + + it('renders the autocomplete with the availableTypes', async () => { + const mockAvailableTypes = ['foo', 'bar']; + + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: mockAvailableTypes, + }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const openButton = getByRole('button', { name: 'Open' }); + openButton.click(); + + expect(getByRole('checkbox', { name: 'Foo' })).toBeInTheDocument(); + expect(getByRole('checkbox', { name: 'Bar' })).toBeInTheDocument(); + }); + + it('should call setSelectedTypes when one of the options are called', async () => { + const mockAvailableTypes = ['foo', 'bar']; + const mockSetSelectedTypes = jest.fn(); + + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: mockAvailableTypes, + setSelectedTypes: mockSetSelectedTypes, + }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const openButton = getByRole('button', { name: 'Open' }); + await fireEvent(openButton, new MouseEvent('click', { bubbles: true })); + + const fooCheckbox = getByRole('checkbox', { name: 'Foo' }); + await fireEvent(fooCheckbox, new MouseEvent('click', { bubbles: true })); + + expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo']); + + await fireEvent(openButton, new MouseEvent('click', { bubbles: true })); + + const barCheckbox = getByRole('checkbox', { name: 'Bar' }); + await fireEvent(barCheckbox, new MouseEvent('click', { bubbles: true })); + + expect(mockSetSelectedTypes).toHaveBeenCalledWith(['foo', 'bar']); + }); + + it('should render the selectedTypes already in the document', async () => { + const mockAvailableTypes = ['foo', 'bar']; + const mockSelectedTypes = ['foo']; + + (useEntityTypeFilter as jest.Mock).mockReturnValue({ + availableTypes: mockAvailableTypes, + selectedTypes: mockSelectedTypes, + }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const openButton = getByRole('button', { name: 'Open' }); + await fireEvent(openButton, new MouseEvent('click', { bubbles: true })); + + const fooCheckbox = getByRole('checkbox', { name: 'Foo' }); + expect(fooCheckbox).toBeChecked(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx new file mode 100644 index 0000000000..483751f2e2 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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 capitalize from 'lodash/capitalize'; +import { Progress } from '@backstage/core-components'; +import { + Box, + Checkbox, + FormControlLabel, + TextField, + Typography, +} from '@material-ui/core'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { Autocomplete } from '@material-ui/lab'; +import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; +import { alertApiRef, useApi } from '@backstage/core-plugin-api'; + +const icon = ; +const checkedIcon = ; + +/** + * The Category Picker that is rendered on the left side for picking + * categories and filtering the template list. + */ +export const CategoryPicker = () => { + const alertApi = useApi(alertApiRef); + const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = + useEntityTypeFilter(); + + if (loading) return ; + + if (error) { + alertApi.post({ + message: `Failed to load entity types with error: ${error}`, + severity: 'error', + }); + return null; + } + + if (!availableTypes) return null; + + return ( + + Categories + setSelectedTypes(value)} + renderOption={(option, { selected }) => ( + + } + label={capitalize(option)} + /> + )} + size="small" + popupIcon={} + renderInput={params => } + /> + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx new file mode 100644 index 0000000000..e6022d16fb --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { RegisterExistingButton } from './RegisterExistingButton'; +import { usePermission } from '@backstage/plugin-permission-react'; + +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), +})); + +describe('RegisterExistingButton', () => { + beforeEach(() => { + (usePermission as jest.Mock).mockClear(); + }); + + it('should not render if to is unset', async () => { + (usePermission as jest.Mock).mockReturnValue({ allowed: true }); + + const { queryByText } = await renderInTestApp( + , + ); + + expect(await queryByText('Pick me')).not.toBeInTheDocument(); + }); + + it('should not render if permissions are not allowed', async () => { + (usePermission as jest.Mock).mockReturnValue({ allowed: false }); + const { queryByText } = await renderInTestApp( + , + ); + + expect(await queryByText('Pick me')).not.toBeInTheDocument(); + }); + + it('should render the button with the text', async () => { + (usePermission as jest.Mock).mockReturnValue({ allowed: true }); + const { queryByText } = await renderInTestApp( + , + ); + + expect(await queryByText('Pick me')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx new file mode 100644 index 0000000000..8269602703 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2021 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 { BackstageTheme } from '@backstage/theme'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import React from 'react'; +import { Link as RouterLink, LinkProps } from 'react-router-dom'; +import AddCircleOutline from '@material-ui/icons/AddCircleOutline'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { usePermission } from '@backstage/plugin-permission-react'; + +/** + * Properties for {@link RegisterExistingButton} + * + * @alpha + */ +export type RegisterExistingButtonProps = { + title: string; +} & Partial>; + +/** + * A button that helps users to register an existing component. + * @alpha + */ +export const RegisterExistingButton = (props: RegisterExistingButtonProps) => { + const { title, to } = props; + const { allowed } = usePermission(catalogEntityCreatePermission); + const isXSScreen = useMediaQuery(theme => + theme.breakpoints.down('xs'), + ); + + if (!to || !allowed) { + return null; + } + + return isXSScreen ? ( + + + + ) : ( + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx new file mode 100644 index 0000000000..287d813ca7 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.test.tsx @@ -0,0 +1,188 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { CardHeader } from './CardHeader'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import { starredEntitiesApiRef } from '@backstage/plugin-catalog-react'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; +import Observable from 'zen-observable'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +describe('CardHeader', () => { + it('should select the correct theme from the theme provider from the header', () => { + // Can't really test what we want here. + // But we can check that we call the getPage theme with the right type of template at least. + const mockTheme = { + ...lightTheme, + getPageTheme: jest.fn(lightTheme.getPageTheme), + }; + + render( + + + + + , + ); + + expect(mockTheme.getPageTheme).toHaveBeenCalledWith({ themeId: 'service' }); + }); + + it('should render the type', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('service')).toBeInTheDocument(); + }); + + it('should enable favoriting of the entity', async () => { + const starredEntitiesApi = { + starredEntitie$: () => new Observable(() => {}), + toggleStarred: jest.fn(async () => {}), + }; + + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByRole } = await renderInTestApp( + + + , + ); + + const favorite = getByRole('button', { name: 'favorite' }); + + await fireEvent.click(favorite); + + expect(starredEntitiesApi.toggleStarred).toHaveBeenCalledWith( + stringifyEntityRef(mockTemplate), + ); + }); + + it('should render the name of the entity', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('bob')).toBeInTheDocument(); + }); + + it('should render the title of the entity in favor of the name if it is provided', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('Iamtitle')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx new file mode 100644 index 0000000000..3a5785a4f6 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/CardHeader.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { makeStyles, useTheme } from '@material-ui/core'; +import { ItemCardHeader } from '@backstage/core-components'; +import { BackstageTheme } from '@backstage/theme'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { FavoriteEntity } from '@backstage/plugin-catalog-react'; + +const useStyles = makeStyles( + () => ({ + header: { + backgroundImage: ({ cardBackgroundImage }) => cardBackgroundImage, + }, + subtitleWrapper: { + display: 'flex', + justifyContent: 'space-between', + }, + }), +); + +/** + * Props for the CardHeader component + */ +export interface CardHeaderProps { + template: TemplateEntityV1beta3; +} + +/** + * The Card Header with the background for the TemplateCard. + */ +export const CardHeader = (props: CardHeaderProps) => { + const { + template: { + metadata: { title, name }, + spec: { type }, + }, + } = props; + const { getPageTheme } = useTheme(); + const themeForType = getPageTheme({ themeId: type }); + + const styles = useStyles({ + cardBackgroundImage: themeForType.backgroundImage, + }); + + const SubtitleComponent = ( +
+
{type}
+
+ +
+
+ ); + + return ( + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx new file mode 100644 index 0000000000..45f4dc6317 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.test.tsx @@ -0,0 +1,239 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; +import { + entityRouteRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import { TemplateCard } from './TemplateCard'; +import React from 'react'; +import { rootRouteRef } from '../../../routes'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; + +describe('TemplateCard', () => { + it('should render the card title', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByText('bob')).toBeInTheDocument(); + }); + + it('should render the description as markdown', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', description: 'hello **test**' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + const description = getByText('hello'); + expect(description.querySelector('strong')).toBeInTheDocument(); + }); + + it('should render no descroption if none is provided through the template', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob' }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByText('No description')).toBeInTheDocument(); + }); + + it('should render the tags', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', tags: ['cpp', 'react'] }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + for (const tag of mockTemplate.metadata.tags!) { + expect(getByText(tag)).toBeInTheDocument(); + } + }); + + it('should render a link to the owner', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', tags: ['cpp', 'react'] }, + spec: { + steps: [], + type: 'service', + }, + relations: [ + { + targetRef: 'group:default/my-test-user', + type: RELATION_OWNED_BY, + }, + ], + }; + + const { getByRole } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + '/catalog/:kind/:namespace/:name': entityRouteRef, + }, + }, + ); + + expect(getByRole('link', { name: 'my-test-user' })).toBeInTheDocument(); + expect(getByRole('link', { name: 'my-test-user' })).toHaveAttribute( + 'href', + '/catalog/group/default/my-test-user', + ); + }); + + it('should render the choose button to navigate to the selected template', async () => { + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'bob', tags: ['cpp', 'react'] }, + spec: { + steps: [], + type: 'service', + }, + }; + + const { getByRole } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + '/catalog/:kind/:namespace/:name': entityRouteRef, + }, + }, + ); + + expect(getByRole('button', { name: 'Choose' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Choose' })).toHaveAttribute( + 'href', + '/templates/bob', + ); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx new file mode 100644 index 0000000000..0e67ecd82a --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/TemplateCard.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { + Box, + Card, + CardActions, + CardContent, + Chip, + Divider, + makeStyles, +} from '@material-ui/core'; +import { CardHeader } from './CardHeader'; +import { MarkdownContent, UserIcon, Button } from '@backstage/core-components'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { + EntityRefLinks, + getEntityRelations, +} from '@backstage/plugin-catalog-react'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { selectedTemplateRouteRef } from '../../../routes'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles(theme => ({ + box: { + overflow: 'hidden', + textOverflow: 'ellipsis', + display: '-webkit-box', + '-webkit-line-clamp': 10, + '-webkit-box-orient': 'vertical', + /** to make the styles for React Markdown not leak into the description */ + '& p:first-child': { + marginTop: 0, + marginBottom: theme.spacing(2), + }, + }, + label: { + color: theme.palette.text.secondary, + textTransform: 'uppercase', + fontWeight: 'bold', + letterSpacing: 0.5, + lineHeight: 1, + fontSize: '0.75rem', + }, + margin: { + marginBottom: theme.spacing(2), + }, + footer: { + display: 'flex', + justifyContent: 'space-between', + flex: 1, + alignItems: 'center', + }, + ownedBy: { + display: 'flex', + alignItems: 'center', + flex: 1, + color: theme.palette.link, + }, +})); + +/** + * The Props for the Template Card component + * @alpha + */ +export interface TemplateCardProps { + template: TemplateEntityV1beta3; + deprecated?: boolean; +} + +/** + * The Template Card component that is rendered in a list for each template + * @alpha + */ +export const TemplateCard = (props: TemplateCardProps) => { + const { template } = props; + const styles = useStyles(); + const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY); + const templateRoute = useRouteRef(selectedTemplateRouteRef); + const href = templateRoute({ templateName: template.metadata.name }); + + return ( + + + + + + + {(template.metadata.tags?.length ?? 0) > 0 && ( + <> + + + {template.metadata.tags?.map(tag => ( + + ))} + + + )} + + +
+
+ {ownedByRelations.length > 0 && ( + <> + + + + )} +
+ +
+
+
+ ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts new file mode 100644 index 0000000000..99af0cbb50 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TemplateCard } from './TemplateCard'; +export type { TemplateCardProps } from './TemplateCard'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx new file mode 100644 index 0000000000..59c6989c0d --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.test.tsx @@ -0,0 +1,143 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +jest.mock('./TemplateCard', () => ({ TemplateCard: jest.fn(() => null) })); + +import React from 'react'; +import { TemplateGroup } from './TemplateGroup'; +import { render } from '@testing-library/react'; +import { TemplateCard } from './TemplateCard'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +describe('TemplateGroup', () => { + it('should return a message when no templates are passed in', async () => { + const { getByText } = render(); + + expect( + getByText(/No templates found that match your filter/), + ).toBeInTheDocument(); + }); + + it('should render a card for each template with the template being passed as a prop', () => { + const mockTemplates: TemplateEntityV1beta3[] = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { + parameters: [], + steps: [], + type: 'website', + }, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test2' }, + spec: { + parameters: [], + steps: [], + type: 'service', + }, + }, + ]; + + render(); + + expect(TemplateCard).toHaveBeenCalledTimes(2); + + for (const template of mockTemplates) { + expect(TemplateCard).toHaveBeenCalledWith( + expect.objectContaining({ template }), + {}, + ); + } + }); + + it('should use the passed in TemplateCard prop to render the template card', () => { + const mockTemplateCardComponent = jest.fn(() => null); + + const mockTemplates: TemplateEntityV1beta3[] = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { + parameters: [], + steps: [], + type: 'website', + }, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test2' }, + spec: { + parameters: [], + steps: [], + type: 'service', + }, + }, + ]; + + render( + , + ); + + expect(mockTemplateCardComponent).toHaveBeenCalledTimes(2); + + for (const template of mockTemplates) { + expect(mockTemplateCardComponent).toHaveBeenCalledWith( + expect.objectContaining({ template }), + {}, + ); + } + }); + + it('should render the title when no templates passed', () => { + const { getByText } = render(); + expect(getByText('Test')).toBeInTheDocument(); + }); + + it('should render the title when there are templates in the list', () => { + const mockTemplates: TemplateEntityV1beta3[] = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'test' }, + spec: { parameters: [], steps: [], type: 'website' }, + }, + ]; + + const { getByText } = render( + , + ); + + expect(getByText('Test')).toBeInTheDocument(); + }); + + it('should allow for passing through a user given title component', () => { + const TitleComponent =

Im a custom header

; + const { getByText } = render( + , + ); + + expect(getByText('Im a custom header')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx new file mode 100644 index 0000000000..4344c195ae --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroup.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import React from 'react'; +import { + Content, + ContentHeader, + ItemCardGrid, + Link, +} from '@backstage/core-components'; +import { Typography } from '@material-ui/core'; +import { TemplateCard, TemplateCardProps } from './TemplateCard'; +import { stringifyEntityRef } from '@backstage/catalog-model'; + +export interface TemplateGroupProps { + templates: TemplateEntityV1beta3[]; + title: React.ReactNode; + components?: { + CardComponent?: React.ComponentType; + }; +} + +export const TemplateGroup = (props: TemplateGroupProps) => { + const { templates, title, components: { CardComponent } = {} } = props; + const titleComponent = + typeof title === 'string' ? : title; + + if (templates.length === 0) { + return ( + + {titleComponent} + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + + ); + } + + const Card = CardComponent || TemplateCard; + + return ( + + {titleComponent} + + {templates.map(template => ( + + ))} + + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx new file mode 100644 index 0000000000..2d8cdc3150 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx @@ -0,0 +1,172 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntityList: jest.fn(), +})); + +jest.mock('./TemplateGroup', () => ({ + TemplateGroup: jest.fn(() => null), +})); + +import React from 'react'; +import { render } from '@testing-library/react'; +import { useEntityList } from '@backstage/plugin-catalog-react'; +import { TemplateGroups } from './TemplateGroups'; +import { TestApiProvider } from '@backstage/test-utils'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { TemplateGroup } from './TemplateGroup'; + +describe('TemplateGroups', () => { + it('should return progress if the hook is loading', async () => { + (useEntityList as jest.Mock).mockReturnValue({ loading: true }); + + const { findByTestId } = render( + + + , + ); + + expect(await findByTestId('progress')).toBeInTheDocument(); + }); + + it('should use the error api if there is an error with the retrieval of entitylist', async () => { + const mockError = new Error('tings went poop'); + (useEntityList as jest.Mock).mockReturnValue({ + error: mockError, + }); + const errorApi = { + post: jest.fn(), + }; + render( + + + , + ); + + expect(errorApi.post).toHaveBeenCalledWith(mockError); + }); + + it('should return a no templates message if entities is unset', async () => { + (useEntityList as jest.Mock).mockReturnValue({ + entities: null, + loading: false, + error: null, + }); + + const { findByText } = render( + + + , + ); + + expect(await findByText(/No templates found/)).toBeInTheDocument(); + }); + + it('should return a no templates message if entities has no values in it', async () => { + (useEntityList as jest.Mock).mockReturnValue({ + entities: [], + loading: false, + error: null, + }); + + const { findByText } = render( + + + , + ); + + expect(await findByText(/No templates found/)).toBeInTheDocument(); + }); + + it('should call the template group with the components', async () => { + const mockEntities = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't1', + }, + spec: {}, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't2', + }, + spec: {}, + }, + ]; + + (useEntityList as jest.Mock).mockReturnValue({ + entities: mockEntities, + loading: false, + error: null, + }); + + render( + + true }]} /> + , + ); + + expect(TemplateGroup).toHaveBeenCalledWith( + expect.objectContaining({ templates: mockEntities }), + {}, + ); + }); + + it('should apply the filter for each group', async () => { + const mockEntities = [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't1', + }, + spec: {}, + }, + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 't2', + }, + spec: {}, + }, + ]; + + (useEntityList as jest.Mock).mockReturnValue({ + entities: mockEntities, + loading: false, + error: null, + }); + + render( + + e.metadata.name === 't1' }]} + /> + , + ); + + expect(TemplateGroup).toHaveBeenCalledWith( + expect.objectContaining({ templates: [mockEntities[0]] }), + {}, + ); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx new file mode 100644 index 0000000000..f402c0ad19 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TemplateGroup } from './TemplateGroup'; +import { Entity } from '@backstage/catalog-model'; +import { useEntityList } from '@backstage/plugin-catalog-react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { Progress, Link } from '@backstage/core-components'; +import { Typography } from '@material-ui/core'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +/** + * @alpha + */ +export type TemplateGroupFilter = { + title?: React.ReactNode; + filter: (entity: Entity) => boolean; +}; + +export interface TemplateGroupsProps { + groups: TemplateGroupFilter[]; + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; +} + +export const TemplateGroups = (props: TemplateGroupsProps) => { + const { loading, error, entities } = useEntityList(); + const { groups, TemplateCardComponent } = props; + const errorApi = useApi(errorApiRef); + + if (loading) { + return ; + } + + if (error) { + errorApi.post(error); + return null; + } + + if (!entities || !entities.length) { + return ( + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + ); + } + + return ( + <> + {groups.map(({ title, filter }, index) => ( + + filter(e), + )} + title={title} + components={{ CardComponent: TemplateCardComponent }} + /> + ))} + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx new file mode 100644 index 0000000000..e8037e8f98 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; +import { + catalogApiRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import React from 'react'; +import { rootRouteRef } from '../../routes'; +import { TemplateListPage } from './TemplateListPage'; + +describe('TemplateListPage', () => { + const mockCatalogApi = { + getEntities: async () => ({ + items: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { name: 'blob', tags: ['blob'] }, + spec: { + type: 'service', + }, + }, + ], + }), + getEntityFacets: async () => ({ + facets: { 'spec.type': [{ value: 'service', count: 1 }] }, + }), + }; + + it('should render the search bar for templates', async () => { + const { getByPlaceholderText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByPlaceholderText('Search')).toBeInTheDocument(); + }); + + it('should render the all and starred filters', async () => { + const { getByRole } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByRole('menuitem', { name: 'All' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'Starred' })).toBeInTheDocument(); + }); + + it('should render the category picker', async () => { + const { getByText } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(getByText('Categories')).toBeInTheDocument(); + }); + + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should render the EntityTag picker', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText('Tags')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx new file mode 100644 index 0000000000..1b18dce805 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; + +import { + Content, + ContentHeader, + Header, + Page, + SupportButton, +} from '@backstage/core-components'; +import { + EntityKindPicker, + EntityListProvider, + EntitySearchBar, + EntityTagPicker, + CatalogFilterLayout, + UserListPicker, +} from '@backstage/plugin-catalog-react'; +import { CategoryPicker } from './CategoryPicker'; +import { RegisterExistingButton } from './RegisterExistingButton'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { registerComponentRouteRef } from '../../routes'; +import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; + +export type TemplateListPageProps = { + TemplateCardComponent?: React.ComponentType<{ + template: TemplateEntityV1beta3; + }>; + groups?: TemplateGroupFilter[]; +}; + +const defaultGroup: TemplateGroupFilter = { + title: 'All Templates', + filter: () => true, +}; + +export const TemplateListPage = (props: TemplateListPageProps) => { + const registerComponentLink = useRouteRef(registerComponentRouteRef); + const { TemplateCardComponent, groups = [defaultGroup] } = props; + + return ( + + +
+ + + + + Create new software components using standard templates. Different + templates create different kinds of components (services, + websites, documentation, ...). + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateListPage/index.ts b/plugins/scaffolder/src/next/TemplateListPage/index.ts new file mode 100644 index 0000000000..0436cfeeeb --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TemplateListPage } from './TemplateListPage'; +export type { TemplateGroupFilter } from './TemplateGroups'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx new file mode 100644 index 0000000000..37d35c9015 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useContext } from 'react'; +import { + useTemplateSecrets, + SecretsContextProvider, + SecretsContext, +} from './SecretsContext'; +import { renderHook, act } from '@testing-library/react-hooks'; + +describe('SecretsContext', () => { + it('should allow the setting of secrets in the context', async () => { + const { result } = renderHook( + () => ({ + hook: useTemplateSecrets(), + context: useContext(SecretsContext), + }), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + expect(result.current.context?.secrets.foo).toEqual(undefined); + + act(() => result.current.hook.setSecret({ foo: 'bar' })); + + expect(result.current.context?.secrets.foo).toEqual('bar'); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx new file mode 100644 index 0000000000..d6ca47d41c --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/SecretsContext.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { + useState, + useCallback, + useContext, + createContext, + PropsWithChildren, +} from 'react'; + +type SecretsContextContents = { + secrets: Record; + setSecrets: React.Dispatch>>; +}; + +/** + * The actual context object. + */ +export const SecretsContext = createContext( + undefined, +); + +/** + * The Context Provider that holds the state for the secrets. + * + * @alpha + */ +export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { + const [secrets, setSecrets] = useState>({}); + + return ( + + {children} + + ); +}; + +/** + * Hook to access the secrets context. + * @alpha + */ +export const useTemplateSecrets = () => { + const value = useContext(SecretsContext); + if (!value) { + throw new Error( + 'useTemplateSecrets must be used within a SecretsContextProvider', + ); + } + + const { setSecrets } = value; + + const setSecret = useCallback( + (input: Record) => { + setSecrets(currentSecrets => ({ ...currentSecrets, ...input })); + }, + [setSecrets], + ); + + return { setSecret }; +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts new file mode 100644 index 0000000000..65b530dea1 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/SecretsContext/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + useTemplateSecrets, + SecretsContext, + SecretsContextProvider, +} from './SecretsContext'; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx new file mode 100644 index 0000000000..4d160548ca --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FieldExtensionOptions } from '../../extensions'; + +export interface TemplateWizardPageProps { + customFieldExtensions: FieldExtensionOptions[]; +} + +export const TemplateWizardPage = (_props: TemplateWizardPageProps) => { + return null; +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/index.ts new file mode 100644 index 0000000000..146a7a9afc --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TemplateWizardPage } from './TemplateWizardPage'; diff --git a/plugins/scaffolder/src/next/index.ts b/plugins/scaffolder/src/next/index.ts new file mode 100644 index 0000000000..089c268b0b --- /dev/null +++ b/plugins/scaffolder/src/next/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './Router'; +export * from './TemplateListPage'; +export * from './TemplateWizardPage'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ffa0efaaa0..9bbb52a71f 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -150,3 +150,15 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( name: 'EntityTagsPicker', }), ); + +/** + * @alpha + * The Router and main entrypoint to the Alpha Scaffolder plugin. + */ +export const NextScaffolderPage = scaffolderPlugin.provide( + createRoutableExtension({ + name: 'NextScaffolderPage', + component: () => import('./next/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index b36c24803e..05e7d458f3 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -39,7 +39,7 @@ "@backstage/errors": "^1.0.0", "@backstage/plugin-tech-insights-common": "^0.2.4", "@backstage/plugin-tech-insights-node": "^0.2.8", - "ajv": "^7.0.3", + "ajv": "^8.10.0", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", "luxon": "^2.0.2", diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 275adaa15c..60c3477efe 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -92,8 +92,8 @@ FactRetrieverRegistration also accepts an optional `lifecycle` configuration val ```ts const maxItems = { maxItems: 7 }; // Deletes all but 7 latest facts for each id/entity pair -const ttl = { ttl: 1209600000 }; // (2 weeks) Deletes items older than 2 weeks -const ttlWithAHumanReadableValue = { ttl: { weeks: 2 } }; // Deletes items older than 2 weeks +const ttl = { timeToLive: 1209600000 }; // (2 weeks) Deletes items older than 2 weeks +const ttlWithAHumanReadableValue = { timeToLive: { weeks: 2 } }; // Deletes items older than 2 weeks ``` To register these fact retrievers to your application you can modify the example `techInsights.ts` file shown above like this: @@ -235,3 +235,130 @@ const myFactCheckerFactory = new JsonRulesEngineFactCheckerFactory({ }), ``` + +## Included FactRetrievers + +There are three FactRetrievers that come out of the box with Tech Insights: + +- `entityMetadataFactRetriever`: Generates facts which indicate the completeness of entity metadata +- `entityOwnershipFactRetriever`: Generates facts which indicate the quality of data in the spec.owner field +- `techdocsFactRetriever`: Generates facts related to the completeness of techdocs configuration for entities + +## Backend Example + +Here's an example backend setup that will use the three included fact retrievers so you can get an idea of how this all works. This will be the entire contents of your `techInsights.ts` file found at `\packages\backend\src\plugins` as per [Adding the plugin to your `packages/backend`](#adding-the-plugin-to-your-packagesbackend) + +```ts +import { + createRouter, + buildTechInsightsContext, + createFactRetrieverRegistration, + entityOwnershipFactRetriever, + entityMetadataFactRetriever, + techdocsFactRetriever, +} from '@backstage/plugin-tech-insights-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { + JsonRulesEngineFactCheckerFactory, + JSON_RULE_ENGINE_CHECK_TYPE, +} from '@backstage/plugin-tech-insights-backend-module-jsonfc'; + +const ttlTwoWeeks = { timeToLive: { weeks: 2 } }; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const techInsightsContext = await buildTechInsightsContext({ + logger: env.logger, + config: env.config, + database: env.database, + discovery: env.discovery, + factRetrievers: [ + createFactRetrieverRegistration({ + cadence: '0 */6 * * *', // Run every 6 hours - https://crontab.guru/#0_*/6_*_*_* + factRetriever: entityOwnershipFactRetriever, + lifecycle: ttlTwoWeeks, + }), + createFactRetrieverRegistration({ + cadence: '0 */6 * * *', + factRetriever: entityMetadataFactRetriever, + lifecycle: ttlTwoWeeks, + }), + createFactRetrieverRegistration({ + cadence: '0 */6 * * *', + factRetriever: techdocsFactRetriever, + lifecycle: ttlTwoWeeks, + }), + ], + factCheckerFactory: new JsonRulesEngineFactCheckerFactory({ + logger: env.logger, + checks: [ + { + id: 'groupOwnerCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'Group Owner Check', + description: + 'Verifies that a Group has been set as the owner for this entity', + factIds: ['entityOwnershipFactRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'hasGroupOwner', + operator: 'equal', + value: true, + }, + ], + }, + }, + }, + { + id: 'titleCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'Title Check', + description: + 'Verifies that a Title, used to improve readability, has been set for this entity', + factIds: ['entityMetadataFactRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'hasTitle', + operator: 'equal', + value: true, + }, + ], + }, + }, + }, + { + id: 'techDocsCheck', + type: JSON_RULE_ENGINE_CHECK_TYPE, + name: 'TechDocs Check', + description: + 'Verifies that TechDocs has been enabled for this entity', + factIds: ['techdocsFactRetriever'], + rule: { + conditions: { + all: [ + { + fact: 'hasAnnotationBackstageIoTechdocsRef', + operator: 'equal', + value: true, + }, + ], + }, + }, + }, + ], + }), + }); + + return await createRouter({ + ...techInsightsContext, + logger: env.logger, + config: env.config, + }); +} +``` diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts index 8d1f7180a3..1b5dad1f33 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.test.ts @@ -144,7 +144,7 @@ describe('FactRetrieverEngine', () => { engine.schedule(); const job: any = engine.getJob('test-factretriever'); job.triggerScheduledJobNow(); - expect(job.cadence!!).toEqual(cadence); + expect(job.cadence!).toEqual(cadence); expect(testFactRetriever.handler).toHaveBeenCalledWith( expect.objectContaining({ entityFilter: testFactRetriever.entityFilter }), ); diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts index 535aede115..26b1e4274b 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.ts @@ -53,7 +53,7 @@ export const entityMetadataFactRetriever: FactRetriever = { return entities.items.map((entity: Entity) => { return { entity: { - namespace: entity.metadata.namespace!!, + namespace: entity.metadata.namespace!, kind: entity.kind, name: entity.metadata.name, }, diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts index 8f61f182ad..b9a91e67f7 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.ts @@ -51,7 +51,7 @@ export const entityOwnershipFactRetriever: FactRetriever = { return entities.items.map((entity: Entity) => { return { entity: { - namespace: entity.metadata.namespace!!, + namespace: entity.metadata.namespace!, kind: entity.kind, name: entity.metadata.name, }, diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts index c1c3ba5d20..2717f94475 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.ts @@ -49,7 +49,7 @@ export const techdocsFactRetriever: FactRetriever = { return entities.items.map((entity: Entity) => { return { entity: { - namespace: entity.metadata.namespace!!, + namespace: entity.metadata.namespace!, kind: entity.kind, name: entity.metadata.name, }, diff --git a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts index ddb90b75cd..3507e280a1 100644 --- a/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts +++ b/plugins/tech-insights-backend/src/service/persistence/TechInsightsDatabase.ts @@ -62,7 +62,7 @@ export class TechInsightsDatabase implements TechInsightsStore { return Object.values(groupedSchemas) .map(schemas => { const sorted = rsort(schemas.map(it => it.version)); - return schemas.find(it => it.version === sorted[0])!!; + return schemas.find(it => it.version === sorted[0])!; }) .map((it: RawDbFactSchemaRow) => ({ ...omit(it, 'schema'), @@ -188,7 +188,7 @@ export class TechInsightsDatabase implements TechInsightsStore { throw new Error(`No schema found for ${id}. `); } const sorted = rsort(existingSchemas.map(it => it.version)); - return existingSchemas.find(it => it.version === sorted[0])!!; + return existingSchemas.find(it => it.version === sorted[0])!; } private async deleteExpiredFactsByDate( diff --git a/plugins/tech-insights/README.md b/plugins/tech-insights/README.md index db69808df1..068f88c035 100644 --- a/plugins/tech-insights/README.md +++ b/plugins/tech-insights/README.md @@ -47,12 +47,8 @@ const serviceEntityPage = ( It is not obligatory to pass title and description props to `EntityTechInsightsScorecardContent`. If those are left out, default values from `defaultCheckResultRenderers` in `CheckResultRenderer` will be taken, hence `Boolean scorecard` and `This card represents an overview of default boolean Backstage checks`. -### Customize scorecards overview title and description: +## Boolean Scorecard Example -```tsx -// packages/app/src/components/catalog/EntityPage.tsx +If you follow the [Backend Example](https://github.com/backstage/backstage/tree/master/plugins/tech-insights-backend#backend-example), once the needed facts have been generated the boolean scorecard will look like this: -## Links - -- [The Backstage homepage](https://backstage.io) -``` +![Boolean Scorecard Example](./docs/boolean-scorecard-example.png) diff --git a/plugins/tech-insights/docs/boolean-scorecard-example.png b/plugins/tech-insights/docs/boolean-scorecard-example.png new file mode 100644 index 0000000000..f7860afc2c Binary files /dev/null and b/plugins/tech-insights/docs/boolean-scorecard-example.png differ diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 1740fc66b1..8ae665508a 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -12,6 +12,11 @@ "backstage": { "role": "frontend-plugin" }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/tech-insights" + }, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", diff --git a/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts b/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts index 312674a5bf..b6a0f7db56 100644 --- a/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts +++ b/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts @@ -85,6 +85,15 @@ describe('createCacheMiddleware', () => { expect(cache.set).not.toHaveBeenCalled(); }); + it('checks cache for head requests', async () => { + cache.get.mockResolvedValueOnce(getMockHttpResponseFor('xyz')); + + await request(app).head('/static/docs/foo.html').expect(200); + + await waitForSocketClose(); + expect(cache.set).not.toHaveBeenCalled(); + }); + it('sets cache when content is cacheable', async () => { const expectedPath = 'default/api/xyz/index.html'; await request(app) @@ -105,5 +114,13 @@ describe('createCacheMiddleware', () => { await waitForSocketClose(); expect(cache.set).not.toHaveBeenCalled(); }); + + it('does not set cache on head requests', async () => { + const expectedPath = 'default/api/xyz/index.html'; + await request(app).head(`/static/docs/${expectedPath}`).expect(200); + + await waitForSocketClose(); + expect(cache.set).not.toHaveBeenCalled(); + }); }); }); diff --git a/plugins/techdocs-backend/src/cache/cacheMiddleware.ts b/plugins/techdocs-backend/src/cache/cacheMiddleware.ts index cb59681304..51cce6d220 100644 --- a/plugins/techdocs-backend/src/cache/cacheMiddleware.ts +++ b/plugins/techdocs-backend/src/cache/cacheMiddleware.ts @@ -36,6 +36,7 @@ export const createCacheMiddleware = ({ cacheMiddleware.use(async (req, res, next) => { const socket = res.socket; const isCacheable = req.path.startsWith('/static/docs/'); + const isGetRequest = req.method === 'GET'; // Continue early if this is non-cacheable, or there's no socket. if (!isCacheable || !socket) { @@ -69,7 +70,12 @@ export const createCacheMiddleware = ({ socket.on('close', async hadError => { const content = Buffer.concat(chunks); const head = content.toString('utf8', 0, 12); - if (writeToCache && !hadError && head.match(/HTTP\/\d\.\d 200/)) { + if ( + isGetRequest && + writeToCache && + !hadError && + head.match(/HTTP\/\d\.\d 200/) + ) { await cache.set(reqPath, content); } }); diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts index b06762399b..896f909be4 100644 --- a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts +++ b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts @@ -35,11 +35,25 @@ describe('scrollIntoAnchor', () => { it('scroll to the hash anchor element', async () => { const scrollIntoView = jest.fn(); dom.querySelector.mockReturnValue({ scrollIntoView }); - const hash = '#hash'; - window.location.hash = hash; + window.location.hash = '#hash'; transformer(dom as unknown as Element); jest.advanceTimersByTime(200); - expect(dom.querySelector).toHaveBeenCalledWith(`#${hash.slice(1)}`); + expect(dom.querySelector).toHaveBeenCalledWith( + expect.stringMatching('[id="hash"]'), + ); + expect(scrollIntoView).toHaveBeenCalledWith(); + window.location.hash = ''; + }); + + it('works for anchor starting with number', async () => { + const scrollIntoView = jest.fn(); + dom.querySelector.mockReturnValue({ scrollIntoView }); + window.location.hash = '#1-hash'; + transformer(dom as unknown as Element); + jest.advanceTimersByTime(200); + expect(dom.querySelector).toHaveBeenCalledWith( + expect.stringMatching('[id="1-hash"]'), + ); expect(scrollIntoView).toHaveBeenCalledWith(); window.location.hash = ''; }); diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts index 0fa977865e..7ba02bcdfc 100644 --- a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts +++ b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts @@ -22,7 +22,8 @@ export const scrollIntoAnchor = (): Transformer => { // Scroll to the desired anchor on initial navigation if (window.location.hash) { const hash = window.location.hash.slice(1); - dom?.querySelector(`#${hash}`)?.scrollIntoView(); + // fix invalid selector error for anchor starting with number + dom?.querySelector(`[id="${hash}"]`)?.scrollIntoView(); } }, 200); return dom; diff --git a/plugins/xcmetrics/src/components/Trend/Trend.test.tsx b/plugins/xcmetrics/src/components/Trend/Trend.test.tsx index 90981e5401..07085150a5 100644 --- a/plugins/xcmetrics/src/components/Trend/Trend.test.tsx +++ b/plugins/xcmetrics/src/components/Trend/Trend.test.tsx @@ -24,7 +24,7 @@ describe('Trend', () => { const rendered = await renderInTestApp( , ); - expect(rendered.findAllByText('testTitle')).toBeTruthy(); + expect(await rendered.findAllByText('testTitle')).toBeTruthy(); }); it('should render empty state', async () => { @@ -32,6 +32,6 @@ describe('Trend', () => { const rendered = await renderInTestApp( , ); - expect(rendered.findAllByText('testTitle')).toBeTruthy(); + expect(await rendered.findAllByText('testTitle')).toBeTruthy(); }); }); diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index c4a790b2cc..ad783c4609 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -259,7 +259,7 @@ async function updateBackstageReleaseVersion(repo, type) { } } else if (type === 'patch') { if (preMode) { - throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); + throw new Error(`Unexpected pre mode ${preMode} on current branch`); } nextVersion = semver.inc(currentVersion, 'patch'); } diff --git a/yarn.lock b/yarn.lock index 082b364567..0e4b8e29c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -271,13 +271,21 @@ dependencies: debug "^4.1.1" -"@azure/msal-node@^1.1.0", "@azure/msal-node@^1.3.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.4.0.tgz#660685804fbdc533b10cc699f16323e27ec582c6" - integrity sha512-Ek6hqOFUi5QEAxZ55awM8y1N+9SzS9Qh8ijF4RDLtFuHzqP7xXmMnVC1lae45FlH55DUOo7dg/smuDJnb4kw6g== +"@azure/msal-common@^6.1.0": + version "6.1.0" + resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-6.1.0.tgz#00b0c625d10f4c8e48e7b48a353f7d8d20a5ceca" + integrity sha512-IGjAHttOgKDPQr0Qxx1NjABR635ZNuN7LHjxI0Y7SEA2thcaRGTccy+oaXTFabM/rZLt4F2VrPKUX4BnR9hW9g== dependencies: - "@azure/msal-common" "^5.2.0" + debug "^4.1.1" + +"@azure/msal-node@^1.1.0", "@azure/msal-node@^1.3.0": + version "1.7.0" + resolved "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.7.0.tgz#9026971b1fd953f15bfddde9c43e180356a80d77" + integrity sha512-qDkW+Z4b0SGkkYrM1x+0s5WJ3z96vgiNZ20iwpmtCUHVfSrDiGFB8s8REKVaz7JZJi2L1s0vQRbUahly8EoGnw== + dependencies: + "@azure/msal-common" "^6.1.0" axios "^0.21.4" + https-proxy-agent "^5.0.0" jsonwebtoken "^8.5.1" uuid "^8.3.0" @@ -2167,44 +2175,28 @@ tslib "~2.3.0" "@graphql-codegen/typescript-resolvers@^2.4.3": - version "2.4.3" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-2.4.3.tgz#556dbaf23eac0ff9c321d3ce7126d96a839f793f" - integrity sha512-4m3E0zKLSXjGirZcYHHaZ0bxjy/gxvuumShFCKFmYTkHwTfqBaeh/pMhWqLkwC9wimrH6mQoPIYSQHLaF6Eqng== + version "2.6.1" + resolved "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-2.6.1.tgz#5963da60de79cfb847b55b2433cd9f65a33d7aad" + integrity sha512-tb8qiIYRDbypHvEVBi04OlItABPeormhd6gh26Q0FO/dD+tLx1V85A6wnHC4jExWPP9+/3PwHJL1pq/0n1kxlA== dependencies: - "@graphql-codegen/plugin-helpers" "^2.3.2" - "@graphql-codegen/typescript" "^2.4.2" - "@graphql-codegen/visitor-plugin-common" "2.5.2" + "@graphql-codegen/plugin-helpers" "^2.4.0" + "@graphql-codegen/typescript" "^2.4.8" + "@graphql-codegen/visitor-plugin-common" "2.7.4" "@graphql-tools/utils" "^8.1.1" auto-bind "~4.0.0" tslib "~2.3.0" -"@graphql-codegen/typescript@^2.4.2": - version "2.4.7" - resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.4.7.tgz#e72e87cd47a51b2bc232293762573a86182de469" - integrity sha512-1YAK+m9SZRS1Uy8hdXVQAaAG/WF2jdFD76wbMRQ8Lf5e9YbmtJuy1RpI13nZJTZbhFopd9zqoWFJHsU/oy4i7g== +"@graphql-codegen/typescript@^2.4.2", "@graphql-codegen/typescript@^2.4.8": + version "2.4.8" + resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.4.8.tgz#e8110baba9713cde72d57a5c95aa27400363ed9a" + integrity sha512-tVsHIkuyenBany7c5IMU1yi4S1er2hgyXJGNY7PcyhpJMx0eChmbqz1VTiZxDEwi8mDBS2mn3TaSJMh6xuJM5g== dependencies: "@graphql-codegen/plugin-helpers" "^2.4.0" "@graphql-codegen/schema-ast" "^2.4.1" - "@graphql-codegen/visitor-plugin-common" "2.7.3" + "@graphql-codegen/visitor-plugin-common" "2.7.4" auto-bind "~4.0.0" tslib "~2.3.0" -"@graphql-codegen/visitor-plugin-common@2.5.2": - version "2.5.2" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.5.2.tgz#90aa4add41e17bca83f1c7c8ad674f2a06065efd" - integrity sha512-qDMraPmumG+vEGAz42/asRkdgIRmQWH5HTc320UX+I6CY6eE/Ey85cgzoqeQGLV8gu4sj3UkNx/3/r79eX4u+Q== - dependencies: - "@graphql-codegen/plugin-helpers" "^2.3.2" - "@graphql-tools/optimize" "^1.0.1" - "@graphql-tools/relay-operation-optimizer" "^6.3.7" - "@graphql-tools/utils" "^8.3.0" - auto-bind "~4.0.0" - change-case-all "1.0.14" - dependency-graph "^0.11.0" - graphql-tag "^2.11.0" - parse-filepath "^1.0.2" - tslib "~2.3.0" - "@graphql-codegen/visitor-plugin-common@2.7.1": version "2.7.1" resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.7.1.tgz#bdcded24d1d6f329d32b90704397d6001374b893" @@ -2221,10 +2213,10 @@ parse-filepath "^1.0.2" tslib "~2.3.0" -"@graphql-codegen/visitor-plugin-common@2.7.3": - version "2.7.3" - resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.7.3.tgz#f374e8974ae4ab7b1059a7f8748db6393f20c138" - integrity sha512-rODPGkrHEf9aHD8SgVWS0xie8VmJoYXLWgQR8PFKrwCUFjf7nvDuATAL2lTFiHATlVBWg1meXVPdUlkKeJj/0Q== +"@graphql-codegen/visitor-plugin-common@2.7.4": + version "2.7.4" + resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.7.4.tgz#fbc8aec9df0035b8f29fa64a6356cbb02062da5d" + integrity sha512-aaDoEudDD+B7DK/UwDSL2Fzej75N9hNJ3N8FQuTIeDyw6FNGWUxmkjVBLQGlzfnYfK8IYkdfYkrPn3Skq0pVxA== dependencies: "@graphql-codegen/plugin-helpers" "^2.4.0" "@graphql-tools/optimize" "^1.0.1" @@ -3216,9 +3208,9 @@ ioredis "^4.28.5" "@kubernetes/client-node@^0.16.0": - version "0.16.1" - resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.16.1.tgz#c78ef667579777c1a532983922807e228dbc9b90" - integrity sha512-/Ah+3gFSjXFeqDMGGTyYBKug44Eu2D2qowKLdiZqxCkHdSNgy+CNk6FU1Vy80WrTvGkF/CZr4az6O5AopAiJEw== + version "0.16.3" + resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.16.3.tgz#a26a5abbd6e45603b4f75f0baff00e19853e5be7" + integrity sha512-L7IckuyuPfhd+/Urib8MRas9D6sfKEq8IaITYcaE6LlU+Y8MeD7MTbuW6Yb2WdeRuFN8HPSS47mxPnOUNYBXEg== dependencies: "@types/js-yaml" "^4.0.1" "@types/node" "^10.12.0" @@ -3235,9 +3227,9 @@ openid-client "^4.1.1" request "^2.88.0" rfc4648 "^1.3.0" - shelljs "^0.8.4" + shelljs "^0.8.5" stream-buffers "^3.0.2" - tar "^6.0.2" + tar "^6.1.11" tmp-promise "^3.0.2" tslib "^1.9.3" underscore "^1.9.1" @@ -5794,7 +5786,7 @@ "@types/express" "*" "@types/xml2js" "*" -"@types/express@*", "@types/express@4.17.13", "@types/express@^4.17.6": +"@types/express@*", "@types/express@4.17.13", "@types/express@^4.17.13", "@types/express@^4.17.6": version "4.17.13" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -6020,9 +6012,9 @@ "@types/json-schema" "*" "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + version "7.0.10" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz#9b05b7896166cd00e9cbd59864853abf65d9ac23" + integrity sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A== "@types/json-stable-stringify@^1.0.32": version "1.0.32" @@ -6810,23 +6802,15 @@ eslint-utils "^3.0.0" "@typescript-eslint/parser@^5.9.0": - version "5.13.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.13.0.tgz#0394ed8f2f849273c0bf4b811994d177112ced5c" - integrity sha512-GdrU4GvBE29tm2RqWOM0P5QfCtgCyN4hXICj/X9ibKED16136l9ZpoJvCL5pSKtmJzA+NRDzQ312wWMejCVVfg== + version "5.16.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.16.0.tgz#e4de1bde4b4dad5b6124d3da227347616ed55508" + integrity sha512-fkDq86F0zl8FicnJtdXakFs4lnuebH6ZADDw6CYQv0UZeIjHvmEw87m9/29nk2Dv5Lmdp0zQ3zDQhiMWQf/GbA== dependencies: - "@typescript-eslint/scope-manager" "5.13.0" - "@typescript-eslint/types" "5.13.0" - "@typescript-eslint/typescript-estree" "5.13.0" + "@typescript-eslint/scope-manager" "5.16.0" + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/typescript-estree" "5.16.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.13.0": - version "5.13.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.13.0.tgz#cf6aff61ca497cb19f0397eea8444a58f46156b6" - integrity sha512-T4N8UvKYDSfVYdmJq7g2IPJYCRzwtp74KyDZytkR4OL3NRupvswvmJQJ4CX5tDSurW2cvCc1Ia1qM7d0jpa7IA== - dependencies: - "@typescript-eslint/types" "5.13.0" - "@typescript-eslint/visitor-keys" "5.13.0" - "@typescript-eslint/scope-manager@5.14.0": version "5.14.0" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.14.0.tgz#ea518962b42db8ed0a55152ea959c218cb53ca7b" @@ -6835,6 +6819,14 @@ "@typescript-eslint/types" "5.14.0" "@typescript-eslint/visitor-keys" "5.14.0" +"@typescript-eslint/scope-manager@5.16.0": + version "5.16.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz#7e7909d64bd0c4d8aef629cdc764b9d3e1d3a69a" + integrity sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ== + dependencies: + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/visitor-keys" "5.16.0" + "@typescript-eslint/scope-manager@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.9.0.tgz#02dfef920290c1dcd7b1999455a3eaae7a1a3117" @@ -6852,34 +6844,21 @@ debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.13.0": - version "5.13.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.13.0.tgz#da1de4ae905b1b9ff682cab0bed6b2e3be9c04e5" - integrity sha512-LmE/KO6DUy0nFY/OoQU0XelnmDt+V8lPQhh8MOVa7Y5k2gGRd6U9Kp3wAjhB4OHg57tUO0nOnwYQhRRyEAyOyg== - "@typescript-eslint/types@5.14.0": version "5.14.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.14.0.tgz#96317cf116cea4befabc0defef371a1013f8ab11" integrity sha512-BR6Y9eE9360LNnW3eEUqAg6HxS9Q35kSIs4rp4vNHRdfg0s+/PgHgskvu5DFTM7G5VKAVjuyaN476LCPrdA7Mw== +"@typescript-eslint/types@5.16.0": + version "5.16.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.16.0.tgz#5827b011982950ed350f075eaecb7f47d3c643ee" + integrity sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g== + "@typescript-eslint/types@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.9.0.tgz#e5619803e39d24a03b3369506df196355736e1a3" integrity sha512-mWp6/b56Umo1rwyGCk8fPIzb9Migo8YOniBGPAQDNC6C52SeyNGN4gsVwQTAR+RS2L5xyajON4hOLwAGwPtUwg== -"@typescript-eslint/typescript-estree@5.13.0": - version "5.13.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.13.0.tgz#b37c07b748ff030a3e93d87c842714e020b78141" - integrity sha512-Q9cQow0DeLjnp5DuEDjLZ6JIkwGx3oYZe+BfcNuw/POhtpcxMTy18Icl6BJqTSd+3ftsrfuVb7mNHRZf7xiaNA== - dependencies: - "@typescript-eslint/types" "5.13.0" - "@typescript-eslint/visitor-keys" "5.13.0" - debug "^4.3.2" - globby "^11.0.4" - is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@5.14.0": version "5.14.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.14.0.tgz#78b7f7385d5b6f2748aacea5c9b7f6ae62058314" @@ -6893,6 +6872,19 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.16.0": + version "5.16.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz#32259459ec62f5feddca66adc695342f30101f61" + integrity sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ== + dependencies: + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/visitor-keys" "5.16.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + "@typescript-eslint/typescript-estree@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.0.tgz#0e5c6f03f982931abbfbc3c1b9df5fbf92a3490f" @@ -6918,14 +6910,6 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.13.0": - version "5.13.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.13.0.tgz#f45ff55bcce16403b221ac9240fbeeae4764f0fd" - integrity sha512-HLKEAS/qA1V7d9EzcpLFykTePmOQqOFim8oCvhY3pZgQ8Hi38hYpHd9e5GN6nQBFQNecNhws5wkS9Y5XIO0s/g== - dependencies: - "@typescript-eslint/types" "5.13.0" - eslint-visitor-keys "^3.0.0" - "@typescript-eslint/visitor-keys@5.14.0": version "5.14.0" resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.14.0.tgz#1927005b3434ccd0d3ae1b2ecf60e65943c36986" @@ -6934,6 +6918,14 @@ "@typescript-eslint/types" "5.14.0" eslint-visitor-keys "^3.0.0" +"@typescript-eslint/visitor-keys@5.16.0": + version "5.16.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz#f27dc3b943e6317264c7492e390c6844cd4efbbb" + integrity sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g== + dependencies: + "@typescript-eslint/types" "5.16.0" + eslint-visitor-keys "^3.0.0" + "@typescript-eslint/visitor-keys@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.0.tgz#7585677732365e9d27f1878150fab3922784a1a6" @@ -7248,20 +7240,10 @@ ajv@^6.10.0, ajv@^6.10.1, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" - integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.8.0: - version "8.9.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" - integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.8.0: + version "8.10.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d" + integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -11295,10 +11277,10 @@ engine.io@~3.5.0: engine.io-parser "~2.2.0" ws "~7.4.2" -enhanced-resolve@^5.8.3: - version "5.8.3" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" - integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== +enhanced-resolve@^5.9.2: + version "5.9.2" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" + integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -11404,70 +11386,75 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild-android-arm64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz#fb051169a63307d958aec85ad596cfc7d7770303" - integrity sha512-k1Uu4uC4UOFgrnTj2zuj75EswFSEBK+H6lT70/DdS4mTAOfs2ECv2I9ZYvr3w0WL0T4YItzJdK7fPNxcPw6YmQ== +esbuild-android-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.27.tgz#b868bbd9955a92309c69df628d8dd1945478b45c" + integrity sha512-LuEd4uPuj/16Y8j6kqy3Z2E9vNY9logfq8Tq+oTE2PZVuNs3M1kj5Qd4O95ee66yDGb3isaOCV7sOLDwtMfGaQ== -esbuild-darwin-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz#615ea0a9de67b57a293a7128d7ac83ee307a856d" - integrity sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw== +esbuild-android-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.27.tgz#e7d6430555e8e9c505fd87266bbc709f25f1825c" + integrity sha512-E8Ktwwa6vX8q7QeJmg8yepBYXaee50OdQS3BFtEHKrzbV45H4foMOeEE7uqdjGQZFBap5VAqo7pvjlyA92wznQ== -esbuild-darwin-arm64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.22.tgz#82054dcfcecb15ccfd237093b8008e7745a99ad9" - integrity sha512-YAt9Tj3SkIUkswuzHxkaNlT9+sg0xvzDvE75LlBo4DI++ogSgSmKNR6B4eUhU5EUUepVXcXdRIdqMq9ppeRqfw== +esbuild-darwin-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.27.tgz#4dc7484127564e89b4445c0a560a3cb50b3d68e1" + integrity sha512-czw/kXl/1ZdenPWfw9jDc5iuIYxqUxgQ/Q+hRd4/3udyGGVI31r29LCViN2bAJgGvQkqyLGVcG03PJPEXQ5i2g== -esbuild-freebsd-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.22.tgz#778a818c5b078d5cdd6bb6c0e0797217d196999b" - integrity sha512-ek1HUv7fkXMy87Qm2G4IRohN+Qux4IcnrDBPZGXNN33KAL0pEJJzdTv0hB/42+DCYWylSrSKxk3KUXfqXOoH4A== +esbuild-darwin-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.27.tgz#469e59c665f84a8ed323166624c5e7b9b2d22ac1" + integrity sha512-BEsv2U2U4o672oV8+xpXNxN9bgqRCtddQC6WBh4YhXKDcSZcdNh7+6nS+DM2vu7qWIWNA4JbRG24LUUYXysimQ== -esbuild-freebsd-arm64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.22.tgz#18da93b9f3db2e036f72383bfe73b28b73bb332c" - integrity sha512-zPh9SzjRvr9FwsouNYTqgqFlsMIW07O8mNXulGeQx6O5ApgGUBZBgtzSlBQXkHi18WjrosYfsvp5nzOKiWzkjQ== +esbuild-freebsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.27.tgz#895df03bf5f87094a56c9a5815bf92e591903d70" + integrity sha512-7FeiFPGBo+ga+kOkDxtPmdPZdayrSzsV9pmfHxcyLKxu+3oTcajeZlOO1y9HW+t5aFZPiv7czOHM4KNd0tNwCA== -esbuild-linux-32@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.22.tgz#d0d5d9f5bb3536e17ac097e9512019c65b7c0234" - integrity sha512-SnpveoE4nzjb9t2hqCIzzTWBM0RzcCINDMBB67H6OXIuDa4KqFqaIgmTchNA9pJKOVLVIKd5FYxNiJStli21qg== +esbuild-freebsd-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.27.tgz#0b72a41a6b8655e9a8c5608f2ec1afdcf6958441" + integrity sha512-8CK3++foRZJluOWXpllG5zwAVlxtv36NpHfsbWS7TYlD8S+QruXltKlXToc/5ZNzBK++l6rvRKELu/puCLc7jA== -esbuild-linux-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.22.tgz#2773d540971999ea7f38107ef92fca753f6a8c30" - integrity sha512-Zcl9Wg7gKhOWWNqAjygyqzB+fJa19glgl2JG7GtuxHyL1uEnWlpSMytTLMqtfbmRykIHdab797IOZeKwk5g0zg== +esbuild-linux-32@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.27.tgz#43b8ba3803b0bbe7f051869c6a8bf6de1e95de28" + integrity sha512-qhNYIcT+EsYSBClZ5QhLzFzV5iVsP1YsITqblSaztr3+ZJUI+GoK8aXHyzKd7/CKKuK93cxEMJPpfi1dfsOfdw== -esbuild-linux-arm64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.22.tgz#5d4480ce6d6bffab1dd76a23158f5a5ab33e7ba4" - integrity sha512-8q/FRBJtV5IHnQChO3LHh/Jf7KLrxJ/RCTGdBvlVZhBde+dk3/qS9fFsUy+rs3dEi49aAsyVitTwlKw1SUFm+A== +esbuild-linux-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.27.tgz#dc8072097327ecfadba1735562824ce8c05dd0bd" + integrity sha512-ESjck9+EsHoTaKWlFKJpPZRN26uiav5gkI16RuI8WBxUdLrrAlYuYSndxxKgEn1csd968BX/8yQZATYf/9+/qg== -esbuild-linux-arm@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.22.tgz#c6391b3f7c8fa6d3b99a7e893ce0f45f3a921eef" - integrity sha512-soPDdbpt/C0XvOOK45p4EFt8HbH5g+0uHs5nUKjHVExfgR7du734kEkXR/mE5zmjrlymk5AA79I0VIvj90WZ4g== +esbuild-linux-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.27.tgz#c52b58cbe948426b1559910f521b0a3f396f10b8" + integrity sha512-no6Mi17eV2tHlJnqBHRLekpZ2/VYx+NfGxKcBE/2xOMYwctsanCaXxw4zapvNrGE9X38vefVXLz6YCF8b1EHiQ== -esbuild-linux-mips64le@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.22.tgz#2c8dabac355c502e86c38f9f292b3517d8e181f3" - integrity sha512-SiNDfuRXhGh1JQLLA9JPprBgPVFOsGuQ0yDfSPTNxztmVJd8W2mX++c4FfLpAwxuJe183mLuKf7qKCHQs5ZnBQ== +esbuild-linux-arm@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.27.tgz#df869dbd67d4ee3a04b3c7273b6bd2b233e78a18" + integrity sha512-JnnmgUBdqLQO9hoNZQqNHFWlNpSX82vzB3rYuCJMhtkuaWQEmQz6Lec1UIxJdC38ifEghNTBsF9bbe8dFilnCw== -esbuild-linux-ppc64le@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.22.tgz#69d71b2820d5c94306072dac6094bae38e77d1c0" - integrity sha512-6t/GI9I+3o1EFm2AyN9+TsjdgWCpg2nwniEhjm2qJWtJyJ5VzTXGUU3alCO3evopu8G0hN2Bu1Jhz2YmZD0kng== +esbuild-linux-mips64le@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.27.tgz#a2b646d9df368b01aa970a7b8968be6dd6b01d19" + integrity sha512-NolWP2uOvIJpbwpsDbwfeExZOY1bZNlWE/kVfkzLMsSgqeVcl5YMen/cedRe9mKnpfLli+i0uSp7N+fkKNU27A== -esbuild-linux-riscv64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.22.tgz#c0ec0fc3a23624deebf657781550d2329cec4213" - integrity sha512-AyJHipZKe88sc+tp5layovquw5cvz45QXw5SaDgAq2M911wLHiCvDtf/07oDx8eweCyzYzG5Y39Ih568amMTCQ== +esbuild-linux-ppc64le@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.27.tgz#9a21af766a0292578a3009c7408b8509cac7cefd" + integrity sha512-/7dTjDvXMdRKmsSxKXeWyonuGgblnYDn0MI1xDC7J1VQXny8k1qgNp6VmrlsawwnsymSUUiThhkJsI+rx0taNA== -esbuild-linux-s390x@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.22.tgz#ec2af4572d63336cfb27f5a5c851fb1b6617dd91" - integrity sha512-Sz1NjZewTIXSblQDZWEFZYjOK6p8tV6hrshYdXZ0NHTjWE+lwxpOpWeElUGtEmiPcMT71FiuA9ODplqzzSxkzw== +esbuild-linux-riscv64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.27.tgz#344a27f91568056a5903ad5841b447e00e78d740" + integrity sha512-D+aFiUzOJG13RhrSmZgrcFaF4UUHpqj7XSKrIiCXIj1dkIkFqdrmqMSOtSs78dOtObWiOrFCDDzB24UyeEiNGg== + +esbuild-linux-s390x@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.27.tgz#73a7309bd648a07ef58f069658f989a5096130db" + integrity sha512-CD/D4tj0U4UQjELkdNlZhQ8nDHU5rBn6NGp47Hiz0Y7/akAY5i0oGadhEIg0WCY/HYVXFb3CsSPPwaKcTOW3bg== esbuild-loader@^2.18.0: version "2.18.0" @@ -11481,60 +11468,61 @@ esbuild-loader@^2.18.0: tapable "^2.2.0" webpack-sources "^2.2.0" -esbuild-netbsd-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.22.tgz#0e283278e9fdbaa7f0930f93ee113d7759cd865e" - integrity sha512-TBbCtx+k32xydImsHxvFgsOCuFqCTGIxhzRNbgSL1Z2CKhzxwT92kQMhxort9N/fZM2CkRCPPs5wzQSamtzEHA== +esbuild-netbsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.27.tgz#482a587cdbd18a6c264a05136596927deb46c30a" + integrity sha512-h3mAld69SrO1VoaMpYl3a5FNdGRE/Nqc+E8VtHOag4tyBwhCQXxtvDDOAKOUQexBGca0IuR6UayQ4ntSX5ij1Q== -esbuild-openbsd-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz#2a73bba04e16d8ef278fbe2be85248e12a2f2cc2" - integrity sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA== +esbuild-openbsd-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.27.tgz#e99f8cdc63f1628747b63edd124d53cf7796468d" + integrity sha512-xwSje6qIZaDHXWoPpIgvL+7fC6WeubHHv18tusLYMwL+Z6bEa4Pbfs5IWDtQdHkArtfxEkIZz77944z8MgDxGw== -esbuild-sunos-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.22.tgz#8fe03513b8b2e682a6d79d5e3ca5849651a3c1d8" - integrity sha512-/mbJdXTW7MTcsPhtfDsDyPEOju9EOABvCjeUU2OJ7fWpX/Em/H3WYDa86tzLUbcVg++BScQDzqV/7RYw5XNY0g== +esbuild-sunos-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.27.tgz#8611d825bcb8239c78d57452e83253a71942f45c" + integrity sha512-/nBVpWIDjYiyMhuqIqbXXsxBc58cBVH9uztAOIfWShStxq9BNBik92oPQPJ57nzWXRNKQUEFWr4Q98utDWz7jg== -esbuild-windows-32@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.22.tgz#a75df61e3e49df292a1842be8e877a3153ee644f" - integrity sha512-1vRIkuvPTjeSVK3diVrnMLSbkuE36jxA+8zGLUOrT4bb7E/JZvDRhvtbWXWaveUc/7LbhaNFhHNvfPuSw2QOQg== +esbuild-windows-32@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.27.tgz#c06374206d4d92dd31d4fda299b09f51a35e82f6" + integrity sha512-Q9/zEjhZJ4trtWhFWIZvS/7RUzzi8rvkoaS9oiizkHTTKd8UxFwn/Mm2OywsAfYymgUYm8+y2b+BKTNEFxUekw== -esbuild-windows-64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz#d06cf8bbe4945b8bf95a730d871e54a22f635941" - integrity sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw== +esbuild-windows-64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.27.tgz#756631c1d301dfc0d1a887deed2459ce4079582f" + integrity sha512-b3y3vTSl5aEhWHK66ngtiS/c6byLf6y/ZBvODH1YkBM+MGtVL6jN38FdHUsZasCz9gFwYs/lJMVY9u7GL6wfYg== -esbuild-windows-arm64@0.14.22: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.22.tgz#f8b1b05c548073be8413a5ecb12d7c2f6e717227" - integrity sha512-5wvQ+39tHmRhNpu2Fx04l7QfeK3mQ9tKzDqqGR8n/4WUxsFxnVLfDRBGirIfk4AfWlxk60kqirlODPoT5LqMUg== +esbuild-windows-arm64@0.14.27: + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.27.tgz#ad7e187193dcd18768b16065a950f4441d7173f4" + integrity sha512-I/reTxr6TFMcR5qbIkwRGvldMIaiBu2+MP0LlD7sOlNXrfqIl9uNjsuxFPGEG4IRomjfQ5q8WT+xlF/ySVkqKg== esbuild@^0.14.1, esbuild@^0.14.10, esbuild@^0.14.6: - version "0.14.22" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.22.tgz#2b55fde89d7aa5aaaad791816d58ff9dfc5ed085" - integrity sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA== + version "0.14.27" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.27.tgz#41fe0f1b6b68b9f77cac025009bc54bb96e616f1" + integrity sha512-MZQt5SywZS3hA9fXnMhR22dv0oPGh6QtjJRIYbgL1AeqAoQZE+Qn5ppGYQAoHv/vq827flj4tIJ79Mrdiwk46Q== optionalDependencies: - esbuild-android-arm64 "0.14.22" - esbuild-darwin-64 "0.14.22" - esbuild-darwin-arm64 "0.14.22" - esbuild-freebsd-64 "0.14.22" - esbuild-freebsd-arm64 "0.14.22" - esbuild-linux-32 "0.14.22" - esbuild-linux-64 "0.14.22" - esbuild-linux-arm "0.14.22" - esbuild-linux-arm64 "0.14.22" - esbuild-linux-mips64le "0.14.22" - esbuild-linux-ppc64le "0.14.22" - esbuild-linux-riscv64 "0.14.22" - esbuild-linux-s390x "0.14.22" - esbuild-netbsd-64 "0.14.22" - esbuild-openbsd-64 "0.14.22" - esbuild-sunos-64 "0.14.22" - esbuild-windows-32 "0.14.22" - esbuild-windows-64 "0.14.22" - esbuild-windows-arm64 "0.14.22" + esbuild-android-64 "0.14.27" + esbuild-android-arm64 "0.14.27" + esbuild-darwin-64 "0.14.27" + esbuild-darwin-arm64 "0.14.27" + esbuild-freebsd-64 "0.14.27" + esbuild-freebsd-arm64 "0.14.27" + esbuild-linux-32 "0.14.27" + esbuild-linux-64 "0.14.27" + esbuild-linux-arm "0.14.27" + esbuild-linux-arm64 "0.14.27" + esbuild-linux-mips64le "0.14.27" + esbuild-linux-ppc64le "0.14.27" + esbuild-linux-riscv64 "0.14.27" + esbuild-linux-s390x "0.14.27" + esbuild-netbsd-64 "0.14.27" + esbuild-openbsd-64 "0.14.27" + esbuild-sunos-64 "0.14.27" + esbuild-windows-32 "0.14.27" + esbuild-windows-64 "0.14.27" + esbuild-windows-arm64 "0.14.27" escalade@^3.1.1: version "3.1.1" @@ -11622,6 +11610,15 @@ eslint-plugin-cypress@^2.10.3: dependencies: globals "^11.12.0" +eslint-plugin-deprecation@^1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.3.2.tgz#a8125d28c56158cdfa1a685197e6be8ed86f189e" + integrity sha512-z93wbx9w7H/E3ogPw6AZMkkNJ6m51fTZRNZPNQqxQLmx+KKt7aLkMU9wN67s71i+VVHN4tLOZ3zT3QLbnlC0Mg== + dependencies: + "@typescript-eslint/experimental-utils" "^5.0.0" + tslib "^2.3.1" + tsutils "^3.21.0" + eslint-plugin-graphql@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/eslint-plugin-graphql/-/eslint-plugin-graphql-4.0.0.tgz#d238ff2baee4d632cfcbe787a7a70a1f50428358" @@ -13874,9 +13871,9 @@ http-proxy-agent@^5.0.0: debug "4" http-proxy-middleware@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289" - integrity sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA== + version "2.0.4" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" + integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -15801,11 +15798,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.2, json5@^2.1.3, json5@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" + version "2.2.1" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== jsonc-parser@^3.0.0: version "3.0.0" @@ -16856,6 +16851,13 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" +magic-string@^0.26.1: + version "0.26.1" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.26.1.tgz#ba9b651354fa9512474199acecf9c6dbe93f97fd" + integrity sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg== + dependencies: + sourcemap-codec "^1.4.8" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -17201,7 +17203,7 @@ media-typer@0.3.0: vinyl "^2.0.1" vinyl-file "^3.0.0" -memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1: +memfs@^3.1.2, memfs@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== @@ -17669,9 +17671,9 @@ min-indent@^1.0.0: integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= mini-css-extract-plugin@^2.4.2: - version "2.5.3" - resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz#c5c79f9b22ce9b4f164e9492267358dbe35376d9" - integrity sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw== + version "2.6.0" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz#578aebc7fc14d32c0ad304c2c34f08af44673f5e" + integrity sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w== dependencies: schema-utils "^4.0.0" @@ -20908,27 +20910,7 @@ react-universal-interface@^0.6.2: resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== -react-use@^17.2.4: - version "17.2.4" - resolved "https://registry.npmjs.org/react-use/-/react-use-17.2.4.tgz#1f89be3db0a8237c79253db0a15e12bbe3cfeff1" - integrity sha512-vQGpsAM0F5UIlshw5UI8ULGPS4yn5rm7/qvn3T1Gnkrz7YRMEEMh+ynKcmRloOyiIeLvKWiQjMiwRGtdbgs5qQ== - dependencies: - "@types/js-cookie" "^2.2.6" - "@xobotyi/scrollbar-width" "^1.9.5" - copy-to-clipboard "^3.3.1" - fast-deep-equal "^3.1.3" - fast-shallow-equal "^1.0.0" - js-cookie "^2.2.1" - nano-css "^5.3.1" - react-universal-interface "^0.6.2" - resize-observer-polyfill "^1.5.1" - screenfull "^5.1.0" - set-harmonic-interval "^1.0.1" - throttle-debounce "^3.0.1" - ts-easing "^0.2.0" - tslib "^2.1.0" - -react-use@^17.3.1, react-use@^17.3.2: +react-use@^17.2.4, react-use@^17.3.1, react-use@^17.3.2: version "17.3.2" resolved "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz#448abf515f47c41c32455024db28167cb6e53be8" integrity sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw== @@ -21751,13 +21733,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rollup-plugin-dts@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-4.1.0.tgz#63b1e7de3970bb6d50877e60df2150a3892bc49c" - integrity sha512-rriXIm3jdUiYeiAAd1Fv+x2AxK6Kq6IybB2Z/IdoAW95fb4uRUurYsEYKa8L1seedezDeJhy8cfo8FEL9aZzqg== + version "4.2.0" + resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-4.2.0.tgz#f7ae4646f5b3aec07560bc341c0dc4a85f8a0dff" + integrity sha512-lx6irWVhz/x4//tIqRhzk4FOqGQ0n37ZM2wpPCn4uafl/EmiV92om7ZdAsq7Bzho6C+Xh5GfsyuP9H+Udv72Lg== dependencies: - magic-string "^0.25.7" + magic-string "^0.26.1" optionalDependencies: - "@babel/code-frame" "^7.16.0" + "@babel/code-frame" "^7.16.7" rollup-plugin-esbuild@^4.7.2: version "4.7.2" @@ -22225,7 +22207,7 @@ shell-quote@^1.7.3: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== -shelljs@^0.8.4, shelljs@^0.8.5: +shelljs@^0.8.5: version "0.8.5" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== @@ -23444,7 +23426,7 @@ tar@^4.4.12: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2, tar@^6.1.0, tar@^6.1.2: +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: version "6.1.11" resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -23966,7 +23948,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@~2.3.0: +tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@~2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -24886,30 +24868,31 @@ webidl-conversions@^6.1.0: resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-dev-middleware@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.0.tgz#8fc02dba6e72e1d373eca361623d84610f27be7c" - integrity sha512-MouJz+rXAm9B1OTOYaJnn6rtD/lWZPy2ufQCH3BPs8Rloh/Du6Jze4p7AeLYHkVi0giJnYLaSGDC7S+GM9arhg== +webpack-dev-middleware@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz#aa079a8dedd7e58bfeab358a9af7dab304cee57f" + integrity sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg== dependencies: colorette "^2.0.10" - memfs "^3.2.2" + memfs "^3.4.1" mime-types "^2.1.31" range-parser "^1.2.1" schema-utils "^4.0.0" webpack-dev-server@^4.7.3: - version "4.7.3" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz#4e995b141ff51fa499906eebc7906f6925d0beaa" - integrity sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q== + version "4.7.4" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz#d0ef7da78224578384e795ac228d8efb63d5f945" + integrity sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" "@types/serve-index" "^1.9.1" "@types/sockjs" "^0.3.33" "@types/ws" "^8.2.2" ansi-html-community "^0.0.8" bonjour "^3.5.0" - chokidar "^3.5.2" + chokidar "^3.5.3" colorette "^2.0.10" compression "^1.7.4" connect-history-api-fallback "^1.6.0" @@ -24929,8 +24912,8 @@ webpack-dev-server@^4.7.3: sockjs "^0.3.21" spdy "^4.0.2" strip-ansi "^7.0.0" - webpack-dev-middleware "^5.3.0" - ws "^8.1.0" + webpack-dev-middleware "^5.3.1" + ws "^8.4.2" webpack-node-externals@^3.0.0: version "3.0.0" @@ -24951,9 +24934,9 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5, webpack@^5.66.0: - version "5.69.1" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz#8cfd92c192c6a52c99ab00529b5a0d33aa848dc5" - integrity sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A== + version "5.70.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d" + integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^0.0.51" @@ -24964,7 +24947,7 @@ webpack@^5, webpack@^5.66.0: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.3" + enhanced-resolve "^5.9.2" es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" @@ -25249,7 +25232,7 @@ ws@7.4.5: resolved "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== -ws@8.5.0, ws@^8.1.0: +ws@8.5.0, ws@^8.4.2: version "8.5.0" resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==