diff --git a/.changeset/funny-pants-unite.md b/.changeset/funny-pants-unite.md deleted file mode 100644 index 349cf54b8f..0000000000 --- a/.changeset/funny-pants-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-import': patch ---- - -Fix importing yaml files from URLs with trailing query parameters. diff --git a/.changeset/new-ligers-drum.md b/.changeset/new-ligers-drum.md deleted file mode 100644 index 0edee17b4d..0000000000 --- a/.changeset/new-ligers-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-xcmetrics': patch ---- - -Enable browsing detailed build information such as host configuration, errors, warnings, metadata and a timeline for all targets diff --git a/.changeset/ninety-ducks-relate.md b/.changeset/ninety-ducks-relate.md deleted file mode 100644 index 2901efaea5..0000000000 --- a/.changeset/ninety-ducks-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add Buffer to `ProvidePlugin` since this is no longer provided in `webpack@5` diff --git a/.changeset/quick-trains-flow.md b/.changeset/quick-trains-flow.md deleted file mode 100644 index 447dc0aacc..0000000000 --- a/.changeset/quick-trains-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins': patch ---- - -Make `isJenkinsAvailable` recognise both the old and the new Jenkins annotation. diff --git a/.changeset/short-ants-notice.md b/.changeset/short-ants-notice.md deleted file mode 100644 index 779839387e..0000000000 --- a/.changeset/short-ants-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-xcmetrics': patch ---- - -Internal refactoring diff --git a/.changeset/three-eggs-punch.md b/.changeset/three-eggs-punch.md new file mode 100644 index 0000000000..28dbd2439d --- /dev/null +++ b/.changeset/three-eggs-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +GitHub branch protection option 'Require review from Code Owners' can be enabled by adding `requireCodeOwnersReview: true` in context input. diff --git a/.changeset/wicked-spoons-perform.md b/.changeset/wicked-spoons-perform.md deleted file mode 100644 index 61c489df13..0000000000 --- a/.changeset/wicked-spoons-perform.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/integration': patch ---- - -Add AWS S3 URL Reader diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fda500ed02..781feb1f04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,7 @@ /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core /plugins/ilert @yacut +/plugins/home @backstage/techdocs-core /packages/search-common @backstage/techdocs-core /packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/silver-lining diff --git a/ADOPTERS.md b/ADOPTERS.md index 72b6cce072..020a7e3355 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -21,7 +21,7 @@ | [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | | [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | | [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | | [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | | [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | | [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | @@ -42,3 +42,4 @@ | [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | | [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | | [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 7ec6ec7117..2c36ddcd7f 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -128,20 +128,20 @@ export default async function createPlugin({ google: createGoogleProvider({ signIn: { resolver: async ({ profile: { email } }, ctx) => { - const [sub] = email?.split('@') ?? ''; + const [id] = email?.split('@') ?? ''; // Fetch from an external system that returns entity claims like: // ['user:default/breanna.davison', ...] const ent = await externalSystemClient.getUsernames(email); // Resolve group membership from the Backstage catalog const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [sub].concat(ent), + entityRefs: [id].concat(ent), logger: ctx.logger, }); const token = await ctx.tokenIssuer.issueToken({ - claims: { sub, ent: fullEnt }, + claims: { sub: id, ent: fullEnt }, }); - return { sub, token }; + return { id, token }; }, }, }), diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index 69a223b29e..a0302ce279 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -43,7 +43,7 @@ provides decent results and performs well with ten thousands of indexed documents. The connection to postgres is established via the database manager also used by other plugins. -> **Important**: The search plugin requires at least Postgres 11! +> **Important**: The search plugin requires at least Postgres 12! To use the `PgSearchEngine`, make sure that you have a Postgres database configured and make the following changes to your backend: diff --git a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md index 25ff2a2f3a..28001cce0b 100644 --- a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md +++ b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md @@ -325,7 +325,7 @@ spec: output: links: - url: '{{steps.publish.output.remoteUrl}}' - text: 'Go to Repo' + title: 'Go to Repo' ``` ## Questions? diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 53a90e2954..a5773a6aff 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -131,6 +131,9 @@ want to have those as well as your new one, you'll need to do the following: ```ts import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; +import { ScmIntegrations } from '@backstage/integration'; + +const integrations = ScmIntegrations.fromConfig(config); const builtInActions = createBuiltinActions({ containerRunner, diff --git a/microsite/data/plugins/firehydrant.yaml b/microsite/data/plugins/firehydrant.yaml new file mode 100644 index 0000000000..e4dd5e7416 --- /dev/null +++ b/microsite/data/plugins/firehydrant.yaml @@ -0,0 +1,9 @@ +--- +title: FireHydrant +author: FireHydrant +authorUrl: https://firehydrant.io/ +category: Incident Management +description: View service incidents information from FireHydrant, such as active incidents and incident metrics, directly within Backstage. +documentation: https://github.com/backstage/backstage/blob/master/plugins/firehydrant/README.md +iconUrl: https://raw.githubusercontent.com/backstage/backstage/master/plugins/firehydrant/doc/firehydrant_logo.png +npmPackageName: '@backstage/plugin-firehydrant' diff --git a/microsite/data/plugins/gitlab.yaml b/microsite/data/plugins/gitlab.yaml new file mode 100644 index 0000000000..7a5493c7f4 --- /dev/null +++ b/microsite/data/plugins/gitlab.yaml @@ -0,0 +1,12 @@ +--- +title: GitLab +author: Loblaw +authorUrl: https://github.com/loblaw-sre/backstage-plugin-gitlab +category: CI/CD +description: View GitLab pipelines, merge requests, languages and contributors. +documentation: https://github.com/loblaw-sre/backstage-plugin-gitlab +iconUrl: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png +npmPackageName: '@loblaw/backstage-plugin-gitlab' +tags: + - ci + - cd diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 5ce4f341e1..ed60186679 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,22 @@ # example-app +## 0.2.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@0.11.1 + - @backstage/plugin-home@0.2.0 + - @backstage/cli@0.7.9 + - @backstage/core-app-api@0.1.9 + - @backstage/plugin-catalog-import@0.5.18 + - @backstage/plugin-catalog@0.6.12 + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/plugin-sentry@0.3.19 + - @backstage/plugin-jenkins@0.5.4 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + ## 0.2.41 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index dcf14c88c1..78ebe32e25 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,20 +1,20 @@ { "name": "example-app", - "version": "0.2.41", + "version": "0.2.42", "private": true, "bundled": true, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/core-components": "^0.3.1", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/integration-react": "^0.1.7", "@backstage/plugin-api-docs": "^0.6.6", "@backstage/plugin-badges": "^0.2.7", - "@backstage/plugin-catalog": "^0.6.11", - "@backstage/plugin-catalog-import": "^0.5.17", - "@backstage/plugin-catalog-react": "^0.4.2", + "@backstage/plugin-catalog": "^0.6.12", + "@backstage/plugin-catalog-import": "^0.5.18", + "@backstage/plugin-catalog-react": "^0.4.3", "@backstage/plugin-circleci": "^0.2.21", "@backstage/plugin-cloudbuild": "^0.2.21", "@backstage/plugin-code-coverage": "^0.1.9", @@ -23,7 +23,8 @@ "@backstage/plugin-gcp-projects": "^0.3.2", "@backstage/plugin-github-actions": "^0.4.16", "@backstage/plugin-graphiql": "^0.2.14", - "@backstage/plugin-jenkins": "^0.5.3", + "@backstage/plugin-home": "^0.2.0", + "@backstage/plugin-jenkins": "^0.5.4", "@backstage/plugin-kafka": "^0.2.13", "@backstage/plugin-kubernetes": "^0.4.11", "@backstage/plugin-lighthouse": "^0.2.23", @@ -31,24 +32,24 @@ "@backstage/plugin-org": "^0.3.19", "@backstage/plugin-pagerduty": "0.3.11", "@backstage/plugin-rollbar": "^0.3.12", - "@backstage/plugin-scaffolder": "^0.11.0", + "@backstage/plugin-scaffolder": "^0.11.1", "@backstage/plugin-search": "^0.4.7", - "@backstage/plugin-sentry": "^0.3.18", + "@backstage/plugin-sentry": "^0.3.19", "@backstage/plugin-shortcuts": "^0.1.6", "@backstage/plugin-tech-radar": "^0.4.5", "@backstage/plugin-techdocs": "^0.10.4", "@backstage/plugin-todo": "^0.1.7", "@backstage/plugin-user-settings": "^0.3.3", "@backstage/search-common": "^0.1.3", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.5.3", - "@roadiehq/backstage-plugin-buildkite": "^1.0.6", - "@roadiehq/backstage-plugin-github-insights": "^1.1.20", - "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.10", - "@roadiehq/backstage-plugin-travis-ci": "^1.0.8", + "@roadiehq/backstage-plugin-buildkite": "^1.0.8", + "@roadiehq/backstage-plugin-github-insights": "^1.1.23", + "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13", + "@roadiehq/backstage-plugin-travis-ci": "^1.0.11", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^16.12.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 1132368a91..a73039fed2 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,6 +20,7 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; +import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { CatalogEntityPage, @@ -45,10 +46,6 @@ import { ScaffolderPage, scaffolderPlugin, ScaffolderFieldExtensions, - RepoUrlPickerFieldExtension, - OwnerPickerFieldExtension, - EntityPickerFieldExtension, - EntityNamePickerFieldExtension, } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; @@ -67,6 +64,8 @@ import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions'; +import { HomePage } from './components/home/HomePage'; + import { providers } from './identityProviders'; import * as plugins from './plugins'; @@ -112,6 +111,10 @@ const AppRouter = app.getRouter(); const routes = ( + {/* TODO(rubenl): Move this to / once its more mature and components exist */} + }> + + } /> }> - - - - diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx new file mode 100644 index 0000000000..e171e22ae9 --- /dev/null +++ b/packages/app/src/components/home/HomePage.tsx @@ -0,0 +1,72 @@ +/* + * 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 Grid from '@material-ui/core/Grid'; +import { + RandomJokeHomePageComponent, + ComponentAccordion, + ComponentTabs, + ComponentTab, +} from '@backstage/plugin-home'; + +export const HomePage = () => ( + + + + + + + + + + + ( + + ), + }, + { + label: 'Any', + Component: () => ( + + ), + }, + ]} + /> + + +); diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 297f07b0b4..ed9bc6b625 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/backend-common +## 0.8.10 + +### Patch Changes + +- 8543d9890: Add an optional `info` parameter to the `readTree` filter option with a `size` property. +- 4d909268c: Read responses in `UrlReader#read()` as array buffer instead of as text to allow reading non-text locations such as images. +- 9b4604b38: Add support for watching configuration by implementing the `subscribe` method in the configuration returned by `loadBackendConfig`. +- b8cb12009: Add AWS S3 URL Reader +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/config-loader@0.6.7 + - @backstage/integration@0.6.1 + ## 0.8.9 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2652dad122..736899d1ed 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -593,7 +593,7 @@ export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; // src/cache/types.d.ts:34:5 - (ae-forgotten-export) The symbol "ClientOptions" needs to be exported by the entry point index.d.ts // src/middleware/errorHandler.d.ts:17:26 - (tsdoc-malformed-html-name) Invalid HTML element: A space is not allowed here // src/reading/AzureUrlReader.d.ts:9:9 - (ae-forgotten-export) The symbol "ReadTreeResponseFactory" needs to be exported by the entry point index.d.ts -// src/reading/types.d.ts:106:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts +// src/reading/types.d.ts:108:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts // src/service/types.d.ts:12:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/service/types.d.ts:22:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/service/types.d.ts:30:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 6d25ece793..2ce1f70163 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.8.9", + "version": "0.8.10", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,10 +30,10 @@ }, "dependencies": { "@backstage/cli-common": "^0.1.2", - "@backstage/config": "^0.1.6", - "@backstage/config-loader": "^0.6.6", + "@backstage/config": "^0.1.7", + "@backstage/config-loader": "^0.6.7", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.1", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", @@ -77,7 +77,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.9", "@backstage/test-utils": "^0.1.17", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 1316642ae1..7c660ee4e6 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,32 +18,142 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader } from '@backstage/config'; +import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; +class ObservableConfigProxy implements Config { + private config: Config = new ConfigReader({}); + + private readonly subscribers: (() => void)[] = []; + + constructor(private readonly logger: Logger) {} + + setConfig(config: Config) { + this.config = config; + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + this.logger.error(`Config subscriber threw error, ${error}`); + } + } + } + + subscribe(onChange: () => void): { unsubscribe: () => void } { + this.subscribers.push(onChange); + return { + unsubscribe: () => { + const index = this.subscribers.indexOf(onChange); + if (index >= 0) { + this.subscribers.splice(index, 1); + } + }, + }; + } + + has(key: string): boolean { + return this.config.has(key); + } + keys(): string[] { + return this.config.keys(); + } + get(key?: string): T { + return this.config.get(key); + } + getOptional(key?: string): T | undefined { + return this.config.getOptional(key); + } + getConfig(key: string): Config { + return this.config.getConfig(key); + } + getOptionalConfig(key: string): Config | undefined { + return this.config.getOptionalConfig(key); + } + getConfigArray(key: string): Config[] { + return this.config.getConfigArray(key); + } + getOptionalConfigArray(key: string): Config[] | undefined { + return this.config.getOptionalConfigArray(key); + } + getNumber(key: string): number { + return this.config.getNumber(key); + } + getOptionalNumber(key: string): number | undefined { + return this.config.getOptionalNumber(key); + } + getBoolean(key: string): boolean { + return this.config.getBoolean(key); + } + getOptionalBoolean(key: string): boolean | undefined { + return this.config.getOptionalBoolean(key); + } + getString(key: string): string { + return this.config.getString(key); + } + getOptionalString(key: string): string | undefined { + return this.config.getOptionalString(key); + } + getStringArray(key: string): string[] { + return this.config.getStringArray(key); + } + getOptionalStringArray(key: string): string[] | undefined { + return this.config.getOptionalStringArray(key); + } +} + type Options = { logger: Logger; // process.argv or any other overrides argv: string[]; }; +// A global used to ensure that only a single file watcher is active at a time. +let currentCancelFunc: () => void; + /** - * Load configuration for a Backend + * Load configuration for a Backend. + * + * This function should only be called once, during the initialization of the backend. */ export async function loadBackendConfig(options: Options): Promise { const args = parseArgs(options.argv); - const configOpts: string[] = [args.config ?? []].flat(); + const configPaths: string[] = [args.config ?? []].flat(); + + const config = new ObservableConfigProxy(options.logger); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); + const configs = await loadConfig({ configRoot: paths.targetRoot, - configPaths: configOpts.map(opt => resolvePath(opt)), + configPaths: configPaths.map(opt => resolvePath(opt)), + watch: { + onChange(newConfigs) { + options.logger.info( + `Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`, + ); + + config.setConfig(ConfigReader.fromConfigs(newConfigs)); + }, + stopSignal: new Promise(resolve => { + if (currentCancelFunc) { + currentCancelFunc(); + } + currentCancelFunc = resolve; + + // For reloads of this module we need to use a dispose handler rather than the global. + if (module.hot) { + module.hot.addDisposeHandler(resolve); + } + }), + }, }); options.logger.info( `Loaded config from ${configs.map(c => c.context).join(', ')}`, ); - return ConfigReader.fromConfigs(configs); + config.setConfig(ConfigReader.fromConfigs(configs)); + + return config; } diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index fe45931d53..084f7e3e13 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -160,7 +160,7 @@ describe('AwsS3UrlReader', () => { const response = await awsS3UrlReader.read( 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', ); - expect(response.toString()).toBe('site_name: Test\n'); + expect(response.toString().trim()).toBe('site_name: Test'); }); it('rejects unknown targets', async () => { @@ -214,7 +214,7 @@ describe('AwsS3UrlReader', () => { 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', ); const buffer = await response.buffer(); - expect(buffer.toString()).toBe('site_name: Test\n'); + expect(buffer.toString().trim()).toBe('site_name: Test'); }); it('rejects unknown targets', async () => { diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index d2c056508d..7b4b1e345e 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -70,7 +70,7 @@ export class AzureUrlReader implements UrlReader { // for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html if (response.ok && response.status !== 203) { - return Buffer.from(await response.text()); + return Buffer.from(await response.arrayBuffer()); } const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 2a979a464f..7925212b20 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -86,7 +86,7 @@ export class BitbucketUrlReader implements UrlReader { } if (response.ok) { - return Buffer.from(await response.text()); + return Buffer.from(await response.arrayBuffer()); } const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`; diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts index 3177ee1f8e..6b45d99cc3 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -82,7 +82,7 @@ export class FetchUrlReader implements UrlReader { if (response.ok) { return { - buffer: async () => Buffer.from(await response.text()), + buffer: async () => Buffer.from(await response.arrayBuffer()), etag: response.headers.get('ETag') ?? undefined, }; } diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 40d36fb2a0..5dc1430ea8 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -119,7 +119,7 @@ export class GithubUrlReader implements UrlReader { if (response.ok) { return { - buffer: async () => Buffer.from(await response.text()), + buffer: async () => Buffer.from(await response.arrayBuffer()), etag: response.headers.get('ETag') ?? undefined, }; } diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 27c31c7673..baee64fab2 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -84,7 +84,7 @@ export class GitlabUrlReader implements UrlReader { if (response.ok) { return { - buffer: async () => Buffer.from(await response.text()), + buffer: async () => Buffer.from(await response.arrayBuffer()), etag: response.headers.get('ETag') ?? undefined, }; } diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index 173f6a886a..f7ab7f5349 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -43,7 +43,7 @@ export class TarArchiveResponse implements ReadTreeResponse { private readonly subPath: string, private readonly workDir: string, public readonly etag: string, - private readonly filter?: (path: string) => boolean, + private readonly filter?: (path: string, info: { size: number }) => boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -92,7 +92,7 @@ export class TarArchiveResponse implements ReadTreeResponse { const path = relativePath.slice(this.subPath.length); if (this.filter) { - if (!this.filter(path)) { + if (!this.filter(path, { size: entry.remain })) { entry.resume(); return; } @@ -155,7 +155,7 @@ export class TarArchiveResponse implements ReadTreeResponse { tar.extract({ strip, cwd: dir, - filter: path => { + filter: (path, stat) => { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. const relativePath = stripFirstDirectoryFromPath(path); @@ -164,7 +164,7 @@ export class TarArchiveResponse implements ReadTreeResponse { } if (this.filter) { const innerPath = path.split('/').slice(strip).join('/'); - return this.filter(innerPath); + return this.filter(innerPath, { size: stat.size }); } return true; }, diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 45c6880a55..5c12a383ee 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -37,7 +37,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { private readonly subPath: string, private readonly workDir: string, public readonly etag: string, - private readonly filter?: (path: string) => boolean, + private readonly filter?: (path: string, info: { size: number }) => boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -75,7 +75,11 @@ export class ZipArchiveResponse implements ReadTreeResponse { } } if (this.filter) { - return this.filter(this.getInnerPath(entry.path)); + return this.filter(this.getInnerPath(entry.path), { + size: + (entry.vars as { uncompressedSize?: number }).uncompressedSize ?? + entry.vars.compressedSize, + }); } return true; } diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index 8efc833ead..93f287d4fe 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -104,7 +104,7 @@ export type ReadTreeOptions = { * * If no filter is provided all files are extracted. */ - filter?(path: string): boolean; + filter?(path: string, info?: { size: number }): boolean; /** * An etag can be provided to check whether readTree's response has changed from a previous execution. @@ -164,7 +164,7 @@ export type FromArchiveOptions = { // etag of the blob etag: string; // Filter passed on from the ReadTreeOptions - filter?: (path: string) => boolean; + filter?: (path: string, info?: { size: number }) => boolean; }; export interface ReadTreeResponseFactory { diff --git a/packages/backend/package.json b/packages/backend/package.json index b2877dedf5..8f0e84eac9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -58,9 +58,11 @@ "example-app": "^0.2.41", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "express-prom-bundle": "^6.3.6", "knex": "^0.95.1", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", + "prom-client": "^13.2.0", "sqlite3": "^5.0.1", "winston": "^3.2.1" }, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index aecb233581..78c6c4c20b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -36,6 +36,7 @@ import { } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; +import { metricsInit, metricsHandler } from './metrics'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import codeCoverage from './plugins/codecoverage'; @@ -72,6 +73,7 @@ function makeCreateEnv(config: Config) { } async function main() { + metricsInit(); const logger = getRootLogger(); logger.info( @@ -124,6 +126,7 @@ async function main() { const service = createServiceBuilder(module) .loadConfig(config) .addRouter('', await healthcheck(healthcheckEnv)) + .addRouter('', metricsHandler()) .addRouter('/api', apiRouter) .addRouter('', await app(appEnv)); diff --git a/packages/backend/src/metrics.ts b/packages/backend/src/metrics.ts new file mode 100644 index 0000000000..8834022cd9 --- /dev/null +++ b/packages/backend/src/metrics.ts @@ -0,0 +1,63 @@ +/* + * 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 { useHotCleanup } from '@backstage/backend-common'; +import { RequestHandler, Request } from 'express'; +import promBundle from 'express-prom-bundle'; +import prom from 'prom-client'; +import * as url from 'url'; + +/** + * Experimental Prometheus metrics used to benchmark the performance of the + * software catalog. Use this at your own risk. + */ +const rootRegEx = new RegExp('^/([^/]*)/.*'); +const apiRegEx = new RegExp('^/api/([^/]*)/.*'); + +function normalizePath(req: Request): string { + const path = url.parse(req.originalUrl || req.url).pathname || '/'; + + // Capture /api/ and the plugin name + if (apiRegEx.test(path)) { + return path.replace(apiRegEx, '/api/$1'); + } + + // Only the first path segment at root level + return path.replace(rootRegEx, '/$1'); +} + +export function metricsInit(): void { + prom.collectDefaultMetrics({ prefix: 'backstage_' }); +} + +/** + * Adds a /metrics endpoint, register default runtime metrics and instrument the router. + */ +export function metricsHandler(): RequestHandler { + // We can only initialize the metrics once and have to clean them up between hot reloads + useHotCleanup(module, () => prom.register.clear()); + + return promBundle({ + includeMethod: true, + includePath: true, + // Using includePath alone is problematic, as it will include path labels with high + // cardinality (e.g. path params). Instead we would have to template them. However, this + // is difficult, as every backend plugin might use different routes. Instead we only take + // the first directory of the path, to have at least an idea how each plugin performs: + normalizePath, + promClient: { collectDefaultMetrics: {} }, + }); +} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 848a106db0..064438d17e 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/cli +## 0.7.9 + +### Patch Changes + +- f3bba3d2b: Remove debug logging +- 8ea1e96b3: Fix file path handling in diff commands on Windows. +- 2518aab58: Compensate for error formatting mismatch between Webpack 5 and react-dev-utils +- 1ac2961c3: Reintroduce Node.js shims that were removed in the Webpack 5 migration. +- 8d07a8b03: Add Buffer to `ProvidePlugin` since this is no longer provided in `webpack@5` +- fe506a0cf: Remove Webpack deprecation message when running build. +- 485438a56: Fix `backstage-cli backend:dev` argument passing +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/config-loader@0.6.7 + ## 0.7.8 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 890fc8ed01..dcb5b4465e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.7.8", + "version": "0.7.9", "private": false, "publishConfig": { "access": "public" @@ -31,8 +31,8 @@ "@babel/core": "^7.4.4", "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@backstage/cli-common": "^0.1.2", - "@backstage/config": "^0.1.6", - "@backstage/config-loader": "^0.6.6", + "@backstage/config": "^0.1.7", + "@backstage/config-loader": "^0.6.7", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", @@ -85,6 +85,7 @@ "json-schema": "^0.3.0", "lodash": "^4.17.19", "mini-css-extract-plugin": "^1.4.1", + "node-libs-browser": "^2.2.1", "ora": "^5.3.0", "postcss": "^8.1.0", "process": "^0.11.10", @@ -119,14 +120,14 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.8.9", - "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/backend-common": "^0.8.10", + "@backstage/config": "^0.1.7", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/lib/bundler/backend.ts b/packages/cli/src/lib/bundler/backend.ts index 567fc3476a..bd4ab42098 100644 --- a/packages/cli/src/lib/bundler/backend.ts +++ b/packages/cli/src/lib/bundler/backend.ts @@ -28,7 +28,6 @@ export async function serveBackend(options: BackendServeOptions) { const compiler = webpack(config, (err: Error | undefined) => { if (err) { - console.log('here'); console.error(err); } else console.log('Build succeeded'); }); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 082b1247ab..2f23d5cae6 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -117,9 +117,22 @@ async function build(compiler: webpack.Compiler, isCi: boolean) { if (!stats) { throw new Error('No stats provided'); } - const { errors, warnings } = formatWebpackMessages( - stats.toJson({ all: false, warnings: true, errors: true }), - ); + + const serializedStats = stats.toJson({ + all: false, + warnings: true, + errors: true, + }); + // NOTE(freben): The code below that extracts the message part of the errors, + // is due to react-dev-utils not yet being compatible with webpack 5. This + // may be possible to remove (just passing the serialized stats object + // directly into the format function) after a new release of react-dev-utils + // has been made available. + // See https://github.com/facebook/create-react-app/issues/9880 + const { errors, warnings } = formatWebpackMessages({ + errors: serializedStats.errors?.map(e => (e.message ? e.message : e)), + warnings: serializedStats.warnings?.map(e => (e.message ? e.message : e)), + }); if (errors.length) { // Only keep the first error. Others are often indicative diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index c9e858a31c..6e3f445f4a 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -32,6 +32,7 @@ import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types'; import { version } from '../../lib/version'; import { paths as cliPaths } from '../../lib/paths'; import { runPlain } from '../run'; +import pickBy from 'lodash/pickBy'; export function resolveBaseUrl(config: Config): URL { const baseUrl = config.getString('app.baseUrl'); @@ -177,6 +178,7 @@ export async function createConfig( extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], mainFields: ['browser', 'module', 'main'], fallback: { + ...pickBy(require('node-libs-browser')), module: false, dgram: false, dns: false, @@ -209,7 +211,7 @@ export async function createConfig( output: { path: paths.targetDist, publicPath: validBaseUrl.pathname, - filename: isDev ? '[name].js' : 'static/[name].[hash:8].js', + filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', chunkFilename: isDev ? '[name].chunk.js' : 'static/[name].[chunkhash:8].chunk.js', @@ -318,6 +320,7 @@ export async function createBackendConfig( new RunScriptWebpackPlugin({ name: 'main.js', nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined, + args: process.argv.slice(3), // drop `node backstage-cli backend:dev` }), new webpack.HotModuleReplacementPlugin(), ...(checksEnabled diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index 5cbddde0ec..4a17125e5a 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -16,6 +16,7 @@ import chalk from 'chalk'; import { diffLines } from 'diff'; +import { sep, posix } from 'path'; import { FileDiff, PromptFunc, FileHandler, WriteFileFunc } from './types'; function sortObjectKeys(obj: Record) { @@ -281,7 +282,7 @@ export async function handleAllFiles( promptFunc: PromptFunc, ) { for (const file of files) { - const { path } = file; + const path = file.path.split(sep).join(posix.sep); const fileHandler = fileHandlers.find(handler => handler.patterns.some(pattern => typeof pattern === 'string' ? pattern === path : pattern.test(path), diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 192e6810bc..5ac139f35c 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/codemods +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.9 + - @backstage/core-components@0.3.2 + ## 0.1.9 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 8f87631897..3a52c05f09 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.9", + "version": "0.1.10", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 44819b6903..aee4cf453f 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/config-loader +## 0.6.7 + +### Patch Changes + +- 0ade9d02b: Include `devDependencies` and `optionalDependencies` in the detection of Backstage packages when collecting configuration schema. +- 9b8cec063: Add support for config file watching through a new group of `watch` options to `loadConfig`. +- Updated dependencies + - @backstage/config@0.1.7 + ## 0.6.6 ### Patch Changes diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 47bc22c60f..38aa0b1ebb 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -37,6 +37,10 @@ export type LoadConfigOptions = { configPaths: string[]; env?: string; experimentalEnvFunc?: EnvFunc; + watch?: { + onChange: (configs: AppConfig[]) => void; + stopSignal?: Promise; + }; }; // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 33b8acc3b9..f810841d3e 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.6.6", + "version": "0.6.7", "private": false, "publishConfig": { "access": "public", @@ -31,9 +31,10 @@ }, "dependencies": { "@backstage/cli-common": "^0.1.1", - "@backstage/config": "^0.1.6", + "@backstage/config": "^0.1.7", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", + "chokidar": "^3.5.2", "fs-extra": "9.1.0", "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", diff --git a/packages/config-loader/src/lib/schema/collect.test.ts b/packages/config-loader/src/lib/schema/collect.test.ts index 479e63c94c..ef9d38faa9 100644 --- a/packages/config-loader/src/lib/schema/collect.test.ts +++ b/packages/config-loader/src/lib/schema/collect.test.ts @@ -87,6 +87,8 @@ describe('collectConfigSchemas', () => { dependencies: { c1: '0.0.0', c2: '0.0.0', + }, + devDependencies: { '@backstage/mock': '0.0.0', }, configSchema: { ...mockSchema, title: 'b' }, diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index b53389d499..b363e8e5f3 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -74,6 +74,8 @@ export async function collectConfigSchemas( const pkg = await fs.readJson(pkgPath); const depNames = [ ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ...Object.keys(pkg.optionalDependencies ?? {}), ...Object.keys(pkg.peerDependencies ?? {}), ]; diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index b0e236f9c9..3f51e76936 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; import mockFs from 'mock-fs'; +import fs from 'fs-extra'; describe('loadConfig', () => { - beforeAll(() => { + beforeEach(() => { process.env.MY_SECRET = 'is-secret'; process.env.SUBSTITUTE_ME = 'substituted'; @@ -63,7 +65,7 @@ describe('loadConfig', () => { }); }); - afterAll(() => { + afterEach(() => { mockFs.restore(); }); @@ -170,4 +172,81 @@ describe('loadConfig', () => { }, ]); }); + + it('watches config files', async () => { + const onChange = defer(); + const stopSignal = defer(); + + await expect( + loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: onChange.resolve, + stopSignal: stopSignal.promise, + }, + }), + ).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await expect(onChange.promise).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'New Title', + }, + }, + }, + ]); + + stopSignal.resolve(); + }); + + it('stops watching config files', async () => { + const stopSignal = defer(); + + await loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: () => { + expect('not').toBe('called'); + }, + stopSignal: stopSignal.promise, + }, + }); + + stopSignal.resolve(); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + + function defer() { + let resolve: (value: T) => void; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve: resolve! }; + } }); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 3846d6474b..4dbfc007a7 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import yaml from 'yaml'; +import chokidar from 'chokidar'; import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path'; import { AppConfig } from '@backstage/config'; import { @@ -42,13 +43,27 @@ export type LoadConfigOptions = { * @experimental This API is not stable and may change at any point */ experimentalEnvFunc?: EnvFunc; + + /** + * An optional configuration that enables watching of config files. + */ + watch?: { + /** + * A listener that is called when a config file is changed. + */ + onChange: (configs: AppConfig[]) => void; + + /** + * An optional signal that stops the watcher once the promise resolves. + */ + stopSignal?: Promise; + }; }; export async function loadConfig( options: LoadConfigOptions, ): Promise { - const configs = []; - const { configRoot, experimentalEnvFunc: envFunc } = options; + const { configRoot, experimentalEnvFunc: envFunc, watch } = options; const configPaths = options.configPaths.slice(); // If no paths are provided, we default to reading @@ -64,7 +79,9 @@ export async function loadConfig( const env = envFunc ?? (async (name: string) => process.env[name]); - try { + const loadConfigFiles = async () => { + const configs = []; + for (const configPath of configPaths) { if (!isAbsolute(configPath)) { throw new Error(`Config load path is not absolute: '${configPath}'`); @@ -83,13 +100,50 @@ export async function loadConfig( configs.push({ data, context: basename(configPath) }); } + + return configs; + }; + + let fileConfigs; + try { + fileConfigs = await loadConfigFiles(); } catch (error) { throw new Error( `Failed to read static configuration file, ${error.message}`, ); } - configs.push(...readEnvConfig(process.env)); + const envConfigs = await readEnvConfig(process.env); - return configs; + // Set up config file watching if requested by the caller + if (watch) { + let currentSerializedConfig = JSON.stringify(fileConfigs); + + const watcher = chokidar.watch(configPaths, { + usePolling: process.env.NODE_ENV === 'test', + }); + watcher.on('change', async () => { + try { + const newConfigs = await loadConfigFiles(); + const newSerializedConfig = JSON.stringify(newConfigs); + + if (currentSerializedConfig === newSerializedConfig) { + return; + } + currentSerializedConfig = newSerializedConfig; + + watch.onChange([...newConfigs, ...envConfigs]); + } catch (error) { + console.error(`Failed to reload configuration files, ${error}`); + } + }); + + if (watch.stopSignal) { + watch.stopSignal.then(() => { + watcher.close(); + }); + } + } + + return [...fileConfigs, ...envConfigs]; } diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 63aa34b034..39828ad689 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config +## 0.1.7 + +### Patch Changes + +- 90f25476a: Extended the `Config` interface to have an optional `subscribe` method that can be used be notified of updates to the configuration. + ## 0.1.6 ### Patch Changes diff --git a/packages/config/api-report.md b/packages/config/api-report.md index eff4236026..91f37894c0 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -16,6 +16,9 @@ export type AppConfig = { // // @public (undocumented) export type Config = { + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; has(key: string): boolean; keys(): string[]; get(key?: string): T; diff --git a/packages/config/package.json b/packages/config/package.json index 5d9bf94446..5c150f3f32 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.6", + "version": "0.1.7", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 1135e7d554..84de5dae70 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -26,6 +26,17 @@ export type AppConfig = { }; export type Config = { + /** + * Subscribes to the configuration object in order to receive a notification + * whenever any value within the configuration has changed. + * + * This method is optional to implement, and consumers need to check if it is + * implemented before invoking it. + */ + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; + has(key: string): boolean; keys(): string[]; diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 1ec944ac4a..0e8ef7e45b 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-app-api +## 0.1.9 + +### Patch Changes + +- 72a31c29a: Add support for additional app origins +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + ## 0.1.8 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 2b66a991f1..0ceb39067a 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.8", + "version": "0.1.9", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.1", - "@backstage/config": "^0.1.6", + "@backstage/core-components": "^0.3.2", + "@backstage/config": "^0.1.7", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@types/react": "*", @@ -44,7 +44,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.9", "@backstage/test-utils": "^0.1.17", "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index ce5df9fc91..28125f6fa8 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -126,7 +126,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production', + url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&env=production', }); await expect(sessionPromise).resolves.toEqual({ @@ -174,7 +174,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production', + url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&env=production', }); }); }); diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index d707acdc9b..261a008d4b 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -152,7 +152,10 @@ export class DefaultAuthConnector private async showPopup(scopes: Set): Promise { const scope = this.joinScopesFunc(scopes); - const popupUrl = await this.buildUrl('/start', { scope }); + const popupUrl = await this.buildUrl('/start', { + scope, + origin: location.origin, + }); const payload = await showLoginPopup({ url: popupUrl, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 0241234574..b30ba93655 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-components +## 0.3.2 + +### Patch Changes + +- a3f3cff3b: Change the default hover experience for the sidebar to be not jumpy & add visual separation between sidebar & Entity Page tabs for dark mode. +- 6b1afe8c0: Add a configurable `palette.bursts.gradient` property to the Backstage theme, to support customizing the gradients in the `ItemCard` header. +- Updated dependencies + - @backstage/config@0.1.7 + - @backstage/theme@0.2.10 + ## 0.3.1 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 42404fb0c0..b4402f67ac 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -28,6 +28,7 @@ import { LinkProps as LinkProps_2 } from '@material-ui/core'; import { LinkProps as LinkProps_3 } from 'react-router-dom'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; +import { Palette } from '@material-ui/core/styles/createPalette'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 69f2019943..35e4df6336 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.3.1", + "version": "0.3.2", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.6", + "@backstage/config": "^0.1.7", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -70,8 +70,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.8", - "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.9", + "@backstage/cli": "^0.7.9", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx index 6f8b17701d..58c7c1501d 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx @@ -17,18 +17,18 @@ import { createStyles, makeStyles, - Theme, Typography, WithStyles, } from '@material-ui/core'; import React from 'react'; +import { BackstageTheme } from '../../../../theme/src'; -const styles = (theme: Theme) => +const styles = (theme: BackstageTheme) => createStyles({ root: { color: theme.palette.common.white, padding: theme.spacing(2, 2, 3), - backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + backgroundImage: theme.palette.bursts.gradient.linear, backgroundPosition: 0, backgroundSize: 'inherit', }, diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 65da4d2d75..f928c4b7c4 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -42,6 +42,7 @@ const useStyles = makeStyles(theme => ({ msOverflowStyle: 'none', scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, + borderRight: `1px solid #383838`, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.shortest, @@ -60,14 +61,11 @@ const useStyles = makeStyles(theme => ({ duration: theme.transitions.duration.shorter, }), }, - drawerPeek: { - width: sidebarConfig.drawerWidthClosed + 4, - }, })); enum State { Closed, - Peek, + Idle, Open, } @@ -103,7 +101,7 @@ export const Sidebar = ({ setState(State.Open); }, openDelayMs); - setState(State.Peek); + setState(State.Idle); } }; @@ -115,7 +113,7 @@ export const Sidebar = ({ clearTimeout(hoverTimerRef.current); hoverTimerRef.current = undefined; } - if (state === State.Peek) { + if (state === State.Idle) { setState(State.Closed); } else if (state === State.Open) { hoverTimerRef.current = window.setTimeout(() => { @@ -143,7 +141,6 @@ export const Sidebar = ({ >
diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index 3d574d03bb..d2a690e38f 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -25,7 +25,7 @@ export const sidebarConfig = { drawerWidthOpen: 224, // As per NN/g's guidance on timing for exposing hidden content // See https://www.nngroup.com/articles/timing-exposing-content/ - defaultOpenDelayMs: 300, + defaultOpenDelayMs: 100, defaultCloseDelayMs: 0, defaultFadeDuration: 200, logoHeight: 32, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index baee775947..1e6c5dc5ff 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,7 @@ # @backstage/create-app +## 0.3.36 + ## 0.3.35 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 3be83ec6be..c3f34565f5 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.35", + "version": "0.3.36", "private": false, "publishConfig": { "access": "public" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 86a6e44a75..a05e39a69b 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/dev-utils +## 0.2.7 + +### Patch Changes + +- be2cd4740: Fixed busy refresh loop that happened when registering a page at `/`. +- Updated dependencies + - @backstage/core-app-api@0.1.9 + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + ## 0.2.6 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 66f4d7bba6..3990296f4c 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.6", + "version": "0.2.7", "private": false, "publishConfig": { "access": "public", @@ -29,14 +29,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.8", - "@backstage/core-components": "^0.3.1", + "@backstage/core-app-api": "^0.1.9", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/catalog-model": "^0.9.0", "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-catalog-react": "^0.4.2", + "@backstage/plugin-catalog-react": "^0.4.3", "@backstage/test-utils": "^0.1.17", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", @@ -50,7 +50,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.9", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 661a93e8b0..38cc0befe1 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -227,7 +227,11 @@ class DevAppBuilder { const DevApp = hot(hotModule)(this.build()); - if (window.location.pathname === '/' && this.defaultPage) { + if ( + window.location.pathname === '/' && + this.defaultPage && + this.defaultPage !== '/' + ) { window.location.pathname = this.defaultPage; } diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 54b2d374cc..daffc834c1 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration +## 0.6.1 + +### Patch Changes + +- b8cb12009: Add AWS S3 URL Reader +- Updated dependencies + - @backstage/config@0.1.7 + ## 0.6.0 ### Minor Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index a17572c47e..ffa7c9db8b 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.6.0", + "version": "0.6.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.6", + "@backstage/config": "^0.1.7", "cross-fetch": "^3.0.6", "git-url-parse": "~11.4.4", "@octokit/rest": "^18.5.3", @@ -37,8 +37,8 @@ "luxon": "^2.0.2" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/config-loader": "^0.6.6", + "@backstage/cli": "^0.7.9", + "@backstage/config-loader": "^0.6.7", "@backstage/test-utils": "^0.1.17", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", diff --git a/packages/integration/src/azure/config.test.ts b/packages/integration/src/azure/config.test.ts index d43536cf47..385086fbcf 100644 --- a/packages/integration/src/azure/config.test.ts +++ b/packages/integration/src/azure/config.test.ts @@ -30,9 +30,18 @@ describe('readAzureIntegrationConfig', () => { async function buildFrontendConfig( data: Partial, ): Promise { - const schema = await loadConfigSchema({ + const fullSchema = await loadConfigSchema({ dependencies: [require('../../package.json').name], }); + const serializedSchema = fullSchema.serialize() as { + schemas: { path: string }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // grab the schema from this package only + schemas: serializedSchema.schemas.filter(s => s.path === 'config.d.ts'), + }, + }); const processed = schema.process( [{ data: { integrations: { azure: [data] } }, context: 'app' }], { visibility: ['frontend'] }, diff --git a/packages/integration/src/bitbucket/config.test.ts b/packages/integration/src/bitbucket/config.test.ts index f3d076e1b6..b887566e35 100644 --- a/packages/integration/src/bitbucket/config.test.ts +++ b/packages/integration/src/bitbucket/config.test.ts @@ -30,9 +30,18 @@ describe('readBitbucketIntegrationConfig', () => { async function buildFrontendConfig( data: Partial, ): Promise { - const schema = await loadConfigSchema({ + const fullSchema = await loadConfigSchema({ dependencies: [require('../../package.json').name], }); + const serializedSchema = fullSchema.serialize() as { + schemas: { path: string }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // grab the schema from this package only + schemas: serializedSchema.schemas.filter(s => s.path === 'config.d.ts'), + }, + }); const processed = schema.process( [{ data: { integrations: { bitbucket: [data] } }, context: 'app' }], { visibility: ['frontend'] }, diff --git a/packages/integration/src/github/config.test.ts b/packages/integration/src/github/config.test.ts index 10d076f85d..eb084b14a9 100644 --- a/packages/integration/src/github/config.test.ts +++ b/packages/integration/src/github/config.test.ts @@ -30,9 +30,18 @@ describe('readGitHubIntegrationConfig', () => { async function buildFrontendConfig( data: Partial, ): Promise { - const schema = await loadConfigSchema({ + const fullSchema = await loadConfigSchema({ dependencies: [require('../../package.json').name], }); + const serializedSchema = fullSchema.serialize() as { + schemas: { path: string }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // grab the schema from this package only + schemas: serializedSchema.schemas.filter(s => s.path === 'config.d.ts'), + }, + }); const processed = schema.process( [{ data: { integrations: { github: [data] } }, context: 'app' }], { visibility: ['frontend'] }, diff --git a/packages/integration/src/gitlab/config.test.ts b/packages/integration/src/gitlab/config.test.ts index 4c235ff668..1308704e9d 100644 --- a/packages/integration/src/gitlab/config.test.ts +++ b/packages/integration/src/gitlab/config.test.ts @@ -30,9 +30,18 @@ describe('readGitLabIntegrationConfig', () => { async function buildFrontendConfig( data: Partial, ): Promise { - const schema = await loadConfigSchema({ + const fullSchema = await loadConfigSchema({ dependencies: [require('../../package.json').name], }); + const serializedSchema = fullSchema.serialize() as { + schemas: { path: string }[]; + }; + const schema = await loadConfigSchema({ + serialized: { + ...serializedSchema, // grab the schema from this package only + schemas: serializedSchema.schemas.filter(s => s.path === 'config.d.ts'), + }, + }); const processed = schema.process( [{ data: { integrations: { gitlab: [data] } }, context: 'app' }], { visibility: ['frontend'] }, diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 8b00be65f5..e3d01123fc 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.2.10 + +### Patch Changes + +- 6b1afe8c0: Add a configurable `palette.bursts.gradient` property to the Backstage theme, to support customizing the gradients in the `ItemCard` header. + ## 0.2.9 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index f041134cd5..0a0429a8fb 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.9", + "version": "0.2.10", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.7.6" + "@backstage/cli": "^0.7.9" }, "files": [ "dist" diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index b31786aae9..d7a4a6d785 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -38,6 +38,9 @@ export const lightTheme = createTheme({ backgroundColor: { default: '#7C3699', }, + gradient: { + linear: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + }, }, primary: { main: '#2E77D0', @@ -100,6 +103,9 @@ export const darkTheme = createTheme({ backgroundColor: { default: '#7C3699', }, + gradient: { + linear: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + }, }, primary: { main: '#9CC9FF', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c2758c29e7..76a1b7eb5c 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -58,6 +58,9 @@ type PaletteAdditions = { backgroundColor: { default: string; }; + gradient: { + linear: string; + }; }; pinSidebarButton: { icon: string; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index f3bacf0a13..2f4ebebc34 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -31,11 +31,11 @@ "dependencies": { "@asyncapi/react-component": "^0.23.0", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog": "^0.6.11", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,9 +52,9 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 23a0445929..20886b215f 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend +## 0.3.21 + +### Patch Changes + +- 72a31c29a: Add support for additional app origins +- Updated dependencies + - @backstage/backend-common@0.8.10 + - @backstage/config@0.1.7 + ## 0.3.20 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index a66584ba85..6e65342828 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -112,6 +112,11 @@ export const createOktaProvider: ( _options?: OktaProviderOptions | undefined, ) => AuthProviderFactory; +// Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createOriginFilter(config: Config): (origin: string) => boolean; + // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -324,6 +329,7 @@ export type OAuthStartRequest = express.Request<{}> & { export type OAuthState = { nonce: string; env: string; + origin?: string; }; // Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -416,9 +422,9 @@ export type WebMessageResponse = // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/gitlab/provider.d.ts:38:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/gitlab/provider.d.ts:49:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:105:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:111:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:128:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative +// src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 89116bd425..b0a7be9014 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.3.20", + "version": "0.3.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.9", + "@backstage/backend-common": "^0.8.10", "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", + "@backstage/config": "^0.1.7", "@backstage/errors": "^0.1.1", "@backstage/test-utils": "^0.1.17", "@types/express": "^4.17.6", @@ -51,6 +51,7 @@ "jwt-decode": "^3.1.0", "knex": "^0.95.1", "luxon": "^2.0.2", + "minimatch": "^3.0.3", "morgan": "^1.10.0", "node-cache": "^5.1.2", "openid-client": "^4.2.1", @@ -68,7 +69,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.9", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 0b35ea7bc8..27b629cc07 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -64,6 +64,7 @@ describe('OAuthAdapter', () => { issueToken: async () => 'my-id-token', listPublicKeys: async () => ({ keys: [] }), }, + isOriginAllowed: () => false, }; it('sets the correct headers in start', async () => { @@ -105,6 +106,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, + isOriginAllowed: () => false, }); const state = { nonce: 'nonce', env: 'development' }; @@ -139,6 +141,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, + isOriginAllowed: () => false, }); const mockRequest = { @@ -164,6 +167,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, + isOriginAllowed: () => false, }); const mockRequest = { @@ -190,6 +194,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, + isOriginAllowed: () => false, }); const mockRequest = { @@ -220,6 +225,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, + isOriginAllowed: () => false, }); const mockRequest = { diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index d9a49e17e8..df9fb43cbe 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -22,11 +22,16 @@ import { BackstageIdentity, AuthProviderConfig, } from '../../providers/types'; -import { InputError } from '@backstage/errors'; +import { InputError, NotAllowedError } from '@backstage/errors'; import { TokenIssuer } from '../../identity/types'; -import { verifyNonce } from './helpers'; +import { readState, verifyNonce } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; -import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest } from './types'; +import { + OAuthHandlers, + OAuthStartRequest, + OAuthRefreshRequest, + OAuthState, +} from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -40,6 +45,7 @@ export type Options = { cookiePath: string; appOrigin: string; tokenIssuer: TokenIssuer; + isOriginAllowed: (origin: string) => boolean; }; export class OAuthAdapter implements AuthProviderRouteHandlers { @@ -61,6 +67,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { cookieDomain: url.hostname, cookiePath, secure, + isOriginAllowed: config.isOriginAllowed, }); } @@ -73,6 +80,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { // retrieve scopes from request const scope = req.query.scope?.toString() ?? ''; const env = req.query.env?.toString(); + const origin = req.query.origin?.toString(); if (!env) { throw new InputError('No env provided in request query parameters'); @@ -86,7 +94,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { // set a nonce cookie before redirecting to oauth provider this.setNonceCookie(res, nonce); - const state = { nonce: nonce, env: env }; + const state = { nonce, env, origin }; const forwardReq = Object.assign(req, { scope, state }); const { url, status } = await this.handlers.start( @@ -103,7 +111,22 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { req: express.Request, res: express.Response, ): Promise { + let appOrigin = this.options.appOrigin; + try { + const state: OAuthState = readState(req.query.state?.toString() ?? ''); + + if (state.origin) { + try { + appOrigin = new URL(state.origin).origin; + } catch { + throw new NotAllowedError('App origin is invalid, failed to parse'); + } + if (!this.options.isOriginAllowed(appOrigin)) { + throw new NotAllowedError(`Origin '${appOrigin}' is not allowed`); + } + } + // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); @@ -129,13 +152,13 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { await this.populateIdentity(response.backstageIdentity); // post message back to popup if successful - return postMessageResponse(res, this.options.appOrigin, { + return postMessageResponse(res, appOrigin, { type: 'authorization_response', response, }); } catch (error) { // post error message back to popup if failure - return postMessageResponse(res, this.options.appOrigin, { + return postMessageResponse(res, appOrigin, { type: 'authorization_response', error: { name: error.name, diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index c5ece2b921..7912dd16a5 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -77,6 +77,7 @@ export type OAuthState = { */ nonce: string; env: string; + origin?: string; }; export type OAuthStartRequest = express.Request<{}> & { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index c838005c00..66928ab4a7 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -34,6 +34,11 @@ export type AuthProviderConfig = { * The base URL of the app as provided by app.baseUrl */ appUrl: string; + + /** + * A function that is called to check whether an origin other than the apps default one is allowed. + */ + isOriginAllowed: (origin: string) => boolean; }; export type RedirectInfo = { diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/service/router.test.ts new file mode 100644 index 0000000000..f0f5134848 --- /dev/null +++ b/plugins/auth-backend/src/service/router.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { createOriginFilter } from './router'; + +describe('Auth origin filtering', () => { + const defaultConfigOptions = { + auth: { + experimentalExtraAllowedOrigins: ['https://test-*.example.net'], + }, + }; + const defaultConfig = () => new ConfigReader(defaultConfigOptions); + const getOptionalString = jest.fn(); + const config = defaultConfig(); + config.getOptionalString = getOptionalString; + it('Will explode, invalid origin', () => { + const origin = 'https://test.example.net'; + expect(createOriginFilter(config)(origin)).toBeFalsy(); + }); + it('Will explode, invalid origin domain', () => { + const origin = 'https://test-1234.examplee.net'; + expect(createOriginFilter(config)(origin)).toBeFalsy(); + }); + it("Won't explode, valid origin with numbers", () => { + const origin = 'https://test-1234.example.net'; + expect(createOriginFilter(config)(origin)).toBeTruthy(); + }); + it("Won't explode, valid origin with chars and numbers", () => { + const origin = 'https://test-test1234.example.net'; + expect(createOriginFilter(config)(origin)).toBeTruthy(); + }); +}); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 8173efcbc1..de6c1ffe03 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -32,6 +32,7 @@ import { Config } from '@backstage/config'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; import session from 'express-session'; import passport from 'passport'; +import { Minimatch } from 'minimatch'; type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -88,6 +89,8 @@ export async function createRouter({ const providersConfig = config.getConfig('auth.providers'); const configuredProviders = providersConfig.keys(); + const isOriginAllowed = createOriginFilter(config); + for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { @@ -96,7 +99,7 @@ export async function createRouter({ try { const provider = providerFactory({ providerId, - globalConfig: { baseUrl: authUrl, appUrl }, + globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed }, config: providersConfig.getConfig(providerId), logger, tokenIssuer, @@ -158,3 +161,22 @@ export async function createRouter({ return router; } + +export function createOriginFilter( + config: Config, +): (origin: string) => boolean { + const allowedOrigins = config.getOptionalStringArray( + 'auth.experimentalExtraAllowedOrigins', + ); + if (!allowedOrigins || allowedOrigins.length === 0) { + return () => false; + } + + const allowedOriginPatterns = allowedOrigins.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ); + + return origin => { + return allowedOriginPatterns.some(pattern => pattern.match(origin)); + }; +} diff --git a/plugins/badges/package.json b/plugins/badges/package.json index fb97217d54..f6143df0a3 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -21,11 +21,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.1", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,9 +35,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 88e6b4b000..bd55f93b33 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -38,9 +38,9 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 8c6ae136aa..c93ba4ed19 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.0 + +### Minor Changes + +- 54b441abe: Introduce `LdapOrgEntityProvider` as an alternative to `LdapOrgReaderProcessor`. This also changes the `LdapClient` interface to require a logger. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.13.3 + - @backstage/config@0.1.7 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 5e713eee09..35f647ec70 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -7,6 +7,8 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Client } from 'ldapjs'; import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/config'; import { LocationSpec } from '@backstage/catalog-model'; @@ -87,7 +89,7 @@ export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid'; // // @public export class LdapClient { - constructor(client: Client); + constructor(client: Client, logger: Logger_2); // Warning: (ae-forgotten-export) The symbol "BindConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -103,6 +105,36 @@ export class LdapClient { search(dn: string, options: SearchOptions): Promise; } +// Warning: (ae-missing-release-tag) "LdapOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class LdapOrgEntityProvider implements EntityProvider { + constructor(options: { + id: string; + provider: LdapProviderConfig; + logger: Logger_2; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + }); + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + configRoot: Config, + options: { + id: string; + target: string; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + logger: Logger_2; + }, + ): LdapOrgEntityProvider; + // (undocumented) + getProviderName(): string; + // (undocumented) + read(): Promise; +} + // Warning: (ae-missing-release-tag) "LdapOrgReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index d6b521a422..1712ef5bfd 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.2.2", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,15 +29,15 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", - "@backstage/plugin-catalog-backend": "^0.13.1", + "@backstage/config": "^0.1.7", + "@backstage/plugin-catalog-backend": "^0.13.3", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.15", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.6", + "@backstage/cli": "^0.7.9", "@backstage/test-utils": "^0.1.16", "@types/lodash": "^4.14.151", "msw": "^0.29.0" diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index a99fc3846e..5418577381 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -47,7 +47,7 @@ export class LdapClient { }); if (!bind) { - return new LdapClient(client); + return new LdapClient(client, logger); } return new Promise((resolve, reject) => { @@ -56,13 +56,16 @@ export class LdapClient { if (err) { reject(`LDAP bind failed for ${dn}, ${errorString(err)}`); } else { - resolve(new LdapClient(client)); + resolve(new LdapClient(client, logger)); } }); }); } - constructor(private readonly client: Client) {} + constructor( + private readonly client: Client, + private readonly logger: Logger, + ) {} /** * Performs an LDAP search operation. @@ -72,9 +75,13 @@ export class LdapClient { */ async search(dn: string, options: SearchOptions): Promise { try { - return await new Promise((resolve, reject) => { - const output: SearchEntry[] = []; + const output: SearchEntry[] = []; + const logInterval = setInterval(() => { + this.logger.debug(`Read ${output.length} LDAP entries so far...`); + }, 5000); + + const search = new Promise((resolve, reject) => { this.client.search(dn, options, (err, res) => { if (err) { reject(new Error(errorString(err))); @@ -104,6 +111,10 @@ export class LdapClient { }); }); }); + + return await search.finally(() => { + clearInterval(logInterval); + }); } catch (e) { throw new Error(`LDAP search at DN "${dn}" failed, ${e.message}`); } diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index f3ee09d32d..77ca43a519 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -38,7 +38,7 @@ export async function defaultUserTransformer( const { set, map } = config; const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', + apiVersion: 'backstage.io/v1beta1', kind: 'User', metadata: { name: '', @@ -133,7 +133,7 @@ export async function defaultGroupTransformer( ): Promise { const { set, map } = config; const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', + apiVersion: 'backstage.io/v1beta1', kind: 'Group', metadata: { name: '', diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts new file mode 100644 index 0000000000..c0cd663052 --- /dev/null +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -0,0 +1,204 @@ +/* + * 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 { + Entity, + LOCATION_ANNOTATION, + ORIGIN_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import { merge } from 'lodash'; +import { Logger } from 'winston'; +import { + GroupTransformer, + LdapClient, + LdapProviderConfig, + LDAP_DN_ANNOTATION, + readLdapConfig, + readLdapOrg, + UserTransformer, +} from '../ldap'; + +/** + * Reads user and group entries out of an LDAP service, and provides them as + * User and Group entities for the catalog. + */ +export class LdapOrgEntityProvider implements EntityProvider { + private connection?: EntityProviderConnection; + + static fromConfig( + configRoot: Config, + options: { + /** + * A unique, stable identifier for this provider. + * + * @example "production" + */ + id: string; + /** + * The target that this provider should consume. + * + * Should exactly match the "target" field of one of the "ldap.providers" + * configuration entries. + * + * @example "ldaps://ds-read.example.net" + */ + target: string; + /** + * The function that transforms a user entry in LDAP to an entity. + */ + userTransformer?: UserTransformer; + /** + * The function that transforms a group entry in LDAP to an entity. + */ + groupTransformer?: GroupTransformer; + logger: Logger; + }, + ): LdapOrgEntityProvider { + // TODO(freben): Deprecate the old catalog.processors.ldapOrg config + const config = + configRoot.getOptionalConfig('ldap') || + configRoot.getOptionalConfig('catalog.processors.ldapOrg'); + if (!config) { + throw new TypeError( + `There is no LDAP configuration. Please add it as "ldap.providers".`, + ); + } + + const providers = readLdapConfig(config); + const provider = providers.find(p => options.target === p.target); + if (!provider) { + throw new TypeError( + `There is no LDAP configuration that matches ${options.target}. Please add a configuration entry for it under "ldap.providers".`, + ); + } + + const logger = options.logger.child({ + target: options.target, + }); + + return new LdapOrgEntityProvider({ + id: options.id, + provider, + userTransformer: options.userTransformer, + groupTransformer: options.groupTransformer, + logger, + }); + } + + constructor( + private options: { + id: string; + provider: LdapProviderConfig; + logger: Logger; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + }, + ) {} + + getProviderName() { + return `LdapOrgEntityProvider:${this.options.id}`; + } + + async connect(connection: EntityProviderConnection) { + this.connection = connection; + } + + async read() { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const { markReadComplete } = trackProgress(this.options.logger); + + // Be lazy and create the client each time; even though it's pretty + // inefficient, we usually only do this once per entire refresh loop and + // don't have to worry about timeouts and reconnects etc. + const client = await LdapClient.create( + this.options.logger, + this.options.provider.target, + this.options.provider.bind, + ); + + const { users, groups } = await readLdapOrg( + client, + this.options.provider.users, + this.options.provider.groups, + { + groupTransformer: this.options.groupTransformer, + userTransformer: this.options.userTransformer, + logger: this.options.logger, + }, + ); + + const { markCommitComplete } = markReadComplete({ users, groups }); + + await this.connection.applyMutation({ + type: 'full', + entities: [...users, ...groups].map(entity => ({ + locationKey: `ldap-org-provider:${this.options.id}`, + entity: withLocations(this.options.id, entity), + })), + }); + + markCommitComplete(); + } +} + +// Helps wrap the timing and logging behaviors +function trackProgress(logger: Logger) { + let timestamp = Date.now(); + let summary: string; + + logger.info('Reading LDAP users and groups'); + + function markReadComplete(read: { users: unknown[]; groups: unknown[] }) { + summary = `${read.users.length} LDAP users and ${read.groups.length} LDAP groups`; + const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1); + timestamp = Date.now(); + logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`); + return { markCommitComplete }; + } + + function markCommitComplete() { + const commitDuration = ((Date.now() - timestamp) / 1000).toFixed(1); + logger.info(`Committed ${summary} in ${commitDuration} seconds.`); + } + + return { markReadComplete }; +} + +// Makes sure that emitted entities have a proper location based on their DN +function withLocations(providerId: string, entity: Entity): Entity { + const dn = + entity.metadata.annotations?.[LDAP_DN_ANNOTATION] || entity.metadata.name; + const location = `ldap://${providerId}/${encodeURIComponent(dn)}`; + return merge( + { + metadata: { + annotations: { + [LOCATION_ANNOTATION]: location, + [ORIGIN_LOCATION_ANNOTATION]: location, + }, + }, + }, + entity, + ) as Entity; +} diff --git a/plugins/catalog-backend-module-ldap/src/processors/index.ts b/plugins/catalog-backend-module-ldap/src/processors/index.ts index 6e3f413085..96e1a49cbb 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/index.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export { LdapOrgEntityProvider } from './LdapOrgEntityProvider'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index a719fd0f46..b226c749aa 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.3 + +### Patch Changes + +- 77cdc5a84: Pass along a `UserTransformer` to the read step +- be498d22f: Pass along a `OrganizationTransformer` to the read step +- Updated dependencies + - @backstage/plugin-catalog-backend@0.13.3 + - @backstage/config@0.1.7 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 91f8180e94..7a10ad2995 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -111,14 +111,18 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: MicrosoftGraphProviderConfig[]; logger: Logger_2; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }); // (undocumented) static fromConfig( config: Config, options: { logger: Logger_2; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }, ): MicrosoftGraphOrgReaderProcessor; // (undocumented) @@ -170,7 +174,9 @@ export function readMicrosoftGraphOrg( options: { userFilter?: string; groupFilter?: string; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; logger: Logger_2; }, ): Promise<{ diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index e5b167695f..d0089c99cc 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "dependencies": { "@azure/msal-node": "^1.1.0", "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", - "@backstage/plugin-catalog-backend": "^0.13.0", + "@backstage/config": "^0.1.7", + "@backstage/plugin-catalog-backend": "^0.13.3", "@microsoft/microsoft-graph-types": "^1.25.0", "cross-fetch": "^3.0.6", "lodash": "^4.17.15", @@ -40,8 +40,8 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.8.6", - "@backstage/cli": "^0.7.4", + "@backstage/backend-common": "^0.8.10", + "@backstage/cli": "^0.7.9", "@backstage/test-utils": "^0.1.14", "@types/lodash": "^4.14.151", "msw": "^0.29.0" diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 68f7e216ea..82279b76c6 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -114,6 +114,48 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); + + it('should read users with custom transformer', async () => { + async function* getExampleUsers() { + yield { + id: 'userid', + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + + client.getUsers.mockImplementation(getExampleUsers); + client.getUserPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { users } = await readMicrosoftGraphUsers(client, { + userFilter: 'accountEnabled eq true', + transformer: async () => ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'x' }, + spec: { memberOf: [] }, + }), + logger: getVoidLogger(), + }); + + expect(users).toEqual([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'x' }, + spec: { memberOf: [] }, + }, + ]); + + expect(client.getUsers).toBeCalledTimes(1); + expect(client.getUsers).toBeCalledWith({ + filter: 'accountEnabled eq true', + }); + expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); + expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); + }); }); describe('readMicrosoftGraphOrganization', () => { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 62513b0e10..096cf8a479 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -20,6 +20,7 @@ import { } from '@backstage/catalog-model'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import limiterFactory from 'p-limit'; +import { Logger } from 'winston'; import { MicrosoftGraphClient } from './client'; import { MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, @@ -33,7 +34,6 @@ import { OrganizationTransformer, UserTransformer, } from './types'; -import { Logger } from 'winston'; export async function defaultUserTransformer( user: MicrosoftGraph.User, @@ -212,7 +212,11 @@ export async function defaultGroupTransformer( export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, - options?: { groupFilter?: string; transformer?: GroupTransformer }, + options?: { + groupFilter?: string; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }, ): Promise<{ groups: GroupEntity[]; // With all relations empty rootGroup: GroupEntity | undefined; // With all relations empty @@ -224,13 +228,15 @@ export async function readMicrosoftGraphGroups( const groupMemberOf: Map> = new Map(); const limiter = limiterFactory(10); - const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId); + const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId, { + transformer: options?.organizationTransformer, + }); if (rootGroup) { groupMember.set(rootGroup.metadata.name, new Set()); groups.push(rootGroup); } - const transformer = options?.transformer ?? defaultGroupTransformer; + const transformer = options?.groupTransformer ?? defaultGroupTransformer; const promises: Promise[] = []; for await (const group of client.getGroups({ @@ -382,18 +388,22 @@ export async function readMicrosoftGraphOrg( options: { userFilter?: string; groupFilter?: string; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; logger: Logger; }, ): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { const { users } = await readMicrosoftGraphUsers(client, { userFilter: options.userFilter, + transformer: options.userTransformer, logger: options.logger, }); const { groups, rootGroup, groupMember, groupMemberOf } = await readMicrosoftGraphGroups(client, tenantId, { groupFilter: options?.groupFilter, - transformer: options?.groupTransformer, + groupTransformer: options?.groupTransformer, + organizationTransformer: options?.organizationTransformer, }); resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 88240db618..161cc8799f 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -26,8 +26,10 @@ import { GroupTransformer, MicrosoftGraphClient, MicrosoftGraphProviderConfig, + OrganizationTransformer, readMicrosoftGraphConfig, readMicrosoftGraphOrg, + UserTransformer, } from '../microsoftGraph'; /** @@ -36,11 +38,18 @@ import { export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { private readonly providers: MicrosoftGraphProviderConfig[]; private readonly logger: Logger; + private readonly userTransformer?: UserTransformer; private readonly groupTransformer?: GroupTransformer; + private readonly organizationTransformer?: OrganizationTransformer; static fromConfig( config: Config, - options: { logger: Logger; groupTransformer?: GroupTransformer }, + options: { + logger: Logger; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }, ) { const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); return new MicrosoftGraphOrgReaderProcessor({ @@ -52,11 +61,15 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: MicrosoftGraphProviderConfig[]; logger: Logger; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }) { this.providers = options.providers; this.logger = options.logger; + this.userTransformer = options.userTransformer; this.groupTransformer = options.groupTransformer; + this.organizationTransformer = options.organizationTransformer; } async readLocation( @@ -89,7 +102,9 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { { userFilter: provider.userFilter, groupFilter: provider.groupFilter, + userTransformer: this.userTransformer, groupTransformer: this.groupTransformer, + organizationTransformer: this.organizationTransformer, logger: this.logger, }, ); diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index bd66da47b6..c44f7e8746 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend +## 0.13.3 + +### Patch Changes + +- 61aa6526f: Avoid duplicate work by comparing previous processing rounds with the next +- fe960ad0f: Updates the `DefaultProcessingDatabase` to accept a refresh interval function instead of a fixed refresh interval in seconds which used to default to 100s. The catalog now ships with a default refresh interval function that schedules entities for refresh every 100-150 seconds, this should + help to smooth out bursts that occur when a lot of entities are scheduled for refresh at the same second. + + Custom `RefreshIntervalFunction` can be implemented and passed to the CatalogBuilder using `.setInterval(fn)` + +- 54b441abe: Export the entity provider related types for external use. +- 03bb05af6: Enabled live reload of locations configured in `catalog.locations`. +- 2766b2aa5: Add experimental Prometheus metrics instrumentation to the catalog +- Updated dependencies + - @backstage/backend-common@0.8.10 + - @backstage/config@0.1.7 + - @backstage/integration@0.6.1 + ## 0.13.2 ### Patch Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 7286855d4e..723ea5c50c 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -419,6 +419,16 @@ export function createNextRouter( options: RouterOptions_2, ): Promise; +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (ae-missing-release-tag) "createRandomRefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function createRandomRefreshInterval(options: { + minSeconds: number; + maxSeconds: number; +}): RefreshIntervalFunction; + // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -689,6 +699,38 @@ export type EntityProcessingResult = errors: Error[]; }; +// Warning: (ae-missing-release-tag) "EntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + getProviderName(): string; +} + +// Warning: (ae-missing-release-tag) "EntityProviderConnection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface EntityProviderConnection { + // (undocumented) + applyMutation(mutation: EntityProviderMutation): Promise; +} + +// Warning: (ae-missing-release-tag) "EntityProviderMutation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EntityProviderMutation = + | { + type: 'full'; + entities: DeferredEntity[]; + } + | { + type: 'delta'; + added: DeferredEntity[]; + removed: DeferredEntity[]; + }; + // Warning: (ae-missing-release-tag) "FileReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -889,7 +931,6 @@ export class NextCatalogBuilder { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addEntityPolicy(...policies: EntityPolicy[]): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (ae-forgotten-export) The symbol "EntityProvider" needs to be exported by the entry point index.d.ts addEntityProvider(...providers: EntityProvider[]): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addProcessor(...processors: CatalogProcessor[]): NextCatalogBuilder; @@ -914,6 +955,9 @@ export class NextCatalogBuilder { key: string, resolver: PlaceholderResolver, ): NextCatalogBuilder; + setRefreshInterval( + refreshInterval: RefreshIntervalFunction, + ): NextCatalogBuilder; setRefreshIntervalSeconds(seconds: number): NextCatalogBuilder; } @@ -987,6 +1031,11 @@ export type RecursivePartial = { : T[P]; }; +// Warning: (ae-missing-release-tag) "RefreshIntervalFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type RefreshIntervalFunction = () => number; + // Warning: (ae-missing-release-tag) "relation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1107,8 +1156,8 @@ export class UrlReaderProcessor implements CatalogProcessor { // src/ingestion/types.d.ts:41:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/ingestion/types.d.ts:49:5 - (ae-forgotten-export) The symbol "AnalyzeLocationExistingEntity" needs to be exported by the entry point index.d.ts // src/ingestion/types.d.ts:50:5 - (ae-forgotten-export) The symbol "AnalyzeLocationGenerateEntity" needs to be exported by the entry point index.d.ts -// src/next/NextCatalogBuilder.d.ts:140:9 - (ae-forgotten-export) The symbol "CatalogProcessingEngine" needs to be exported by the entry point index.d.ts -// src/next/NextCatalogBuilder.d.ts:141:9 - (ae-forgotten-export) The symbol "LocationService" needs to be exported by the entry point index.d.ts +// src/next/NextCatalogBuilder.d.ts:147:9 - (ae-forgotten-export) The symbol "CatalogProcessingEngine" needs to be exported by the entry point index.d.ts +// src/next/NextCatalogBuilder.d.ts:148:9 - (ae-forgotten-export) The symbol "LocationService" needs to be exported by the entry point index.d.ts // src/next/processing/types.d.ts:11:5 - (ae-forgotten-export) The symbol "DeferredEntity" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-backend/knexfile.js b/plugins/catalog-backend/knexfile.js new file mode 100644 index 0000000000..4c8be42673 --- /dev/null +++ b/plugins/catalog-backend/knexfile.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +// This file makes it possible to run "yarn knex migrate:make some_file_name" +// to assist in making new migrations +module.exports = { + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + migrations: { + directory: './migrations', + }, +}; diff --git a/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js b/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js new file mode 100644 index 0000000000..29a102e876 --- /dev/null +++ b/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('refresh_state', table => { + table + .text('result_hash') + .nullable() + .comment( + 'A hash of the processed contents, used to avoid duplicate work', + ); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('refresh_state', table => { + table.dropColumn('result_hash'); + }); +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 0bf5a08dfd..63cf8f99c4 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.13.2", + "version": "0.13.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.9", + "@backstage/backend-common": "^0.8.10", "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", + "@backstage/config": "^0.1.7", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.1", "@backstage/plugin-search-backend-node": "^0.4.0", "@backstage/search-common": "^0.1.2", "@octokit/graphql": "^4.5.8", @@ -51,8 +51,10 @@ "glob": "^7.1.6", "knex": "^0.95.1", "lodash": "^4.17.15", + "luxon": "^2.0.2", "morgan": "^1.10.0", "p-limit": "^3.0.2", + "prom-client": "^13.2.0", "qs": "^6.9.4", "uuid": "^8.0.0", "winston": "^3.2.1", @@ -62,7 +64,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.5", - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.9", "@backstage/test-utils": "^0.1.17", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", @@ -73,7 +75,8 @@ "msw": "^0.29.0", "sqlite3": "^5.0.1", "supertest": "^6.1.3", - "wait-for-expect": "^3.0.2" + "wait-for-expect": "^3.0.2", + "luxon": "^2.0.2" }, "files": [ "dist", diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts index 32ce28dc1b..ef3e47eb8b 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts @@ -70,4 +70,61 @@ describe('ConfigLocationEntityProvider', () => { ]), }); }); + + it('should be able to observe the config', async () => { + // Grab the subscriber function and use mutable config data to mock a config file change + let subscriber: () => void; + const mutableConfigData = { + catalog: { + locations: [{ type: 'url', target: 'https://github.com/a/a' }], + }, + }; + + const mockConfig = Object.assign(new ConfigReader(mutableConfigData), { + subscribe: (s: () => void) => { + subscriber = s; + return { unsubscribe: () => {} }; + }, + }); + + const mockConnection = { + applyMutation: jest.fn(), + } as unknown as EntityProviderConnection; + const locationProvider = new ConfigLocationEntityProvider(mockConfig); + + await locationProvider.connect(mockConnection); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + spec: { + target: 'https://github.com/a/a', + type: 'url', + }, + }), + locationKey: 'url:https://github.com/a/a', + }, + ], + }); + + mutableConfigData.catalog.locations[0].target = 'https://github.com/b/b'; + subscriber!(); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + spec: { + target: 'https://github.com/b/b', + type: 'url', + }, + }), + locationKey: 'url:https://github.com/b/b', + }, + ], + }); + }); }); diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts index 71ec84dae7..5deca09944 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts @@ -21,8 +21,6 @@ import { EntityProvider, EntityProviderConnection } from './types'; import { locationSpecToLocationEntity } from './util'; export class ConfigLocationEntityProvider implements EntityProvider { - private connection: EntityProviderConnection | undefined; - constructor(private readonly config: Config) {} getProviderName(): string { @@ -30,12 +28,35 @@ export class ConfigLocationEntityProvider implements EntityProvider { } async connect(connection: EntityProviderConnection): Promise { - this.connection = connection; + const entities = this.getEntitiesFromConfig(); + await connection.applyMutation({ + type: 'full', + entities, + }); + if (this.config.subscribe) { + let currentKey = JSON.stringify(entities); + + this.config.subscribe(() => { + const newEntities = this.getEntitiesFromConfig(); + const newKey = JSON.stringify(newEntities); + + if (currentKey !== newKey) { + currentKey = newKey; + connection.applyMutation({ + type: 'full', + entities: newEntities, + }); + } + }); + } + } + + private getEntitiesFromConfig() { const locationConfigs = this.config.getOptionalConfigArray('catalog.locations') ?? []; - const entities = locationConfigs.map(location => { + return locationConfigs.map(location => { const type = location.getString('type'); const target = location.getString('target'); const entity = locationSpecToLocationEntity({ @@ -45,10 +66,5 @@ export class ConfigLocationEntityProvider implements EntityProvider { const locationKey = getEntityLocationRef(entity); return { entity, locationKey }; }); - - await this.connection.applyMutation({ - type: 'full', - entities, - }); } } diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts index 5f3bab63e7..7d4d2ef8db 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts @@ -15,6 +15,8 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Hash } from 'crypto'; +import { DateTime } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase'; import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine'; @@ -33,6 +35,10 @@ describe('DefaultCatalogProcessingEngine', () => { const stitcher = { stitch: jest.fn(), } as unknown as jest.Mocked; + const hash = { + update: () => hash, + digest: jest.fn(), + } as unknown as jest.Mocked; beforeEach(() => { jest.resetAllMocks(); @@ -57,6 +63,7 @@ describe('DefaultCatalogProcessingEngine', () => { db, orchestrator, stitcher, + () => hash, ); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -76,8 +83,9 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, + resultHash: '', state: new Map(), - nextUpdateAt: '', + nextUpdateAt: DateTime.now().toSQL(), lastDiscoveryAt: '', }, ], @@ -117,6 +125,7 @@ describe('DefaultCatalogProcessingEngine', () => { db, orchestrator, stitcher, + () => hash, ); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -137,8 +146,9 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, + resultHash: '', state: new Map(), - nextUpdateAt: '', + nextUpdateAt: DateTime.now().toSQL(), lastDiscoveryAt: '', }, ], @@ -158,4 +168,71 @@ describe('DefaultCatalogProcessingEngine', () => { }); await engine.stop(); }); + + it('runs fully when hash mismatches, early-outs when hash matches', async () => { + const entity = { + apiVersion: '1', + kind: 'Location', + metadata: { name: 'test' }, + }; + + const refreshState = { + id: '', + entityRef: '', + unprocessedEntity: entity, + resultHash: 'the matching hash', + state: new Map(), + nextUpdateAt: DateTime.now().toSQL(), + lastDiscoveryAt: '', + }; + + hash.digest.mockReturnValue('the matching hash'); + + orchestrator.process.mockResolvedValue({ + ok: true, + completedEntity: entity, + relations: [], + errors: [], + deferredEntities: [], + state: new Map(), + }); + + const engine = new DefaultCatalogProcessingEngine( + getVoidLogger(), + [], + db, + orchestrator, + stitcher, + () => hash, + ); + + db.transaction.mockImplementation(cb => cb((() => {}) as any)); + + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [{ ...refreshState, resultHash: 'NOT RIGHT' }], + }) + .mockResolvedValue({ items: [] }); + + await engine.start(); + + await waitForExpect(() => { + expect(orchestrator.process).toBeCalledTimes(1); + expect(hash.digest).toBeCalledTimes(1); + expect(db.updateProcessedEntity).toBeCalledTimes(1); + }); + + db.getProcessableEntities + .mockReset() + .mockResolvedValueOnce({ items: [refreshState] }) + .mockResolvedValue({ items: [] }); + + await waitForExpect(() => { + expect(orchestrator.process).toBeCalledTimes(2); + expect(hash.digest).toBeCalledTimes(2); + expect(db.updateProcessedEntity).toBeCalledTimes(1); + }); + + await engine.stop(); + }); }); diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts index 0ae20c94b1..41e81b1777 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts @@ -20,8 +20,12 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { serializeError } from '@backstage/errors'; +import { Hash } from 'crypto'; +import stableStringify from 'fast-json-stable-stringify'; +import { DateTime } from 'luxon'; import { Logger } from 'winston'; import { ProcessingDatabase, RefreshStateItem } from './database/types'; +import { createCounterMetric, createSummaryMetric } from './metrics'; import { CatalogProcessingOrchestrator } from './processing/types'; import { Stitcher } from './stitching/Stitcher'; import { startTaskPipeline } from './TaskPipeline'; @@ -82,6 +86,20 @@ class Connection implements EntityProviderConnection { export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { private stopFunc?: () => void; + private readonly metrics = { + processedEntities: createCounterMetric({ + name: 'catalog_processed_entities_count', + help: 'Amount of entities processed', + }), + processingDuration: createSummaryMetric({ + name: 'catalog_processing_duration_seconds', + help: 'Processing duration', + }), + processingQueueDelay: createSummaryMetric({ + name: 'catalog_processing_queue_delay_seconds', + help: 'The amount of delay between being scheduled for processing, and the start of actually being processed', + }), + }; constructor( private readonly logger: Logger, @@ -89,9 +107,14 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { private readonly processingDatabase: ProcessingDatabase, private readonly orchestrator: CatalogProcessingOrchestrator, private readonly stitcher: Stitcher, + private readonly createHash: () => Hash, ) {} async start() { + if (this.stopFunc) { + throw new Error('Processing engine is already started'); + } + for (const provider of this.entityProviders) { await provider.connect( new Connection({ @@ -101,10 +124,6 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { ); } - if (this.stopFunc) { - throw new Error('Processing engine is already started'); - } - this.stopFunc = startTaskPipeline({ lowWatermark: 5, highWatermark: 10, @@ -124,8 +143,24 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { } }, processTask: async item => { + let endTimer; try { - const { id, state, unprocessedEntity, entityRef, locationKey } = item; + this.metrics.processedEntities.inc(1); + this.metrics.processingQueueDelay.observe( + -DateTime.fromSQL(item.nextUpdateAt, { zone: 'UTC' }) + .diffNow() + .as('seconds'), + ); + endTimer = this.metrics.processingDuration.startTimer(); + + const { + id, + state, + unprocessedEntity, + entityRef, + locationKey, + resultHash: previousResultHash, + } = item; const result = await this.orchestrator.process({ entity: unprocessedEntity, state, @@ -142,6 +177,23 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { result.errors.map(e => serializeError(e)), ); + let hashBuilder = this.createHash().update(errorsString); + if (result.ok) { + hashBuilder = hashBuilder + .update(stableStringify({ ...result.completedEntity })) + .update(stableStringify([...result.deferredEntities])) + .update(stableStringify([...result.relations])) + .update(stableStringify(Object.fromEntries(result.state))); + } + + const resultHash = hashBuilder.digest('hex'); + if (resultHash === previousResultHash) { + // If nothing changed in our produced outputs, we cannot have any + // significant effect on our surroundings; therefore, we just abort + // without any updates / stitching. + return; + } + // If the result was marked as not OK, it signals that some part of the // processing pipeline threw an exception. This can happen both as part of // non-catastrophic things such as due to validation errors, as well as if @@ -154,6 +206,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.processingDatabase.updateProcessedEntityErrors(tx, { id, errors: errorsString, + resultHash, }); }); await this.stitcher.stitch( @@ -167,6 +220,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.processingDatabase.updateProcessedEntity(tx, { id, processedEntity: result.completedEntity, + resultHash, state: result.state, errors: errorsString, relations: result.relations, @@ -184,6 +238,8 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.stitcher.stitch(setOfThingsToStitch); } catch (error) { this.logger.warn('Processing failed with:', error); + } finally { + endTimer?.(); } }, }); diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index a525dacd51..675e170727 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -31,6 +31,7 @@ import { } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { createHash } from 'crypto'; import lodash from 'lodash'; import { Logger } from 'winston'; import { @@ -74,6 +75,10 @@ import { DefaultLocationStore } from './DefaultLocationStore'; import { NextEntitiesCatalog } from './NextEntitiesCatalog'; import { DefaultCatalogProcessingOrchestrator } from './processing/DefaultCatalogProcessingOrchestrator'; import { Stitcher } from './stitching/Stitcher'; +import { + createRandomRefreshInterval, + RefreshIntervalFunction, +} from './refresh'; export type CatalogEnvironment = { logger: Logger; @@ -111,7 +116,11 @@ export class NextCatalogBuilder { private processors: CatalogProcessor[]; private processorsReplace: boolean; private parser: CatalogProcessorParser | undefined; - private refreshIntervalSeconds = 100; + private refreshInterval: RefreshIntervalFunction = + createRandomRefreshInterval({ + minSeconds: 100, + maxSeconds: 150, + }); constructor(env: CatalogEnvironment) { this.env = env; @@ -143,11 +152,26 @@ export class NextCatalogBuilder { /** * Refresh interval determines how often entities should be refreshed. - * The default refresh duration is 100, setting this too low will potentially - * deplete request quotas to upstream services. + * Seconds provided will be multiplied by 1.5 + * The default refresh duration is 100-150 seconds. + * setting this too low will potentially deplete request quotas to upstream services. */ setRefreshIntervalSeconds(seconds: number): NextCatalogBuilder { - this.refreshIntervalSeconds = seconds; + this.refreshInterval = createRandomRefreshInterval({ + minSeconds: seconds, + maxSeconds: seconds * 1.5, + }); + return this; + } + + /** + * Overwrites the default refresh interval function used to spread + * entity updates in the catalog. + */ + setRefreshInterval( + refreshInterval: RefreshIntervalFunction, + ): NextCatalogBuilder { + this.refreshInterval = refreshInterval; return this; } @@ -284,7 +308,7 @@ export class NextCatalogBuilder { const processingDatabase = new DefaultProcessingDatabase({ database: dbClient, logger, - refreshIntervalSeconds: this.refreshIntervalSeconds, + refreshInterval: this.refreshInterval, }); const integrations = ScmIntegrations.fromConfig(config); const orchestrator = new DefaultCatalogProcessingOrchestrator({ @@ -310,6 +334,7 @@ export class NextCatalogBuilder { processingDatabase, orchestrator, stitcher, + () => createHash('sha1'), ); const locationsCatalog = new DatabaseLocationsCatalog(db); diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 6a0626afc4..df9c08ad6c 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -19,8 +19,9 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/config'; import { Knex } from 'knex'; -import { Logger } from 'winston'; import * as uuid from 'uuid'; +import { Logger } from 'winston'; +import { DateTime } from 'luxon'; import { DatabaseManager } from './DatabaseManager'; import { DefaultProcessingDatabase } from './DefaultProcessingDatabase'; import { @@ -28,6 +29,7 @@ import { DbRefreshStateRow, DbRelationsRow, } from './tables'; +import { createRandomRefreshInterval } from '../refresh'; describe('Default Processing Database', () => { const defaultLogger = getVoidLogger(); @@ -46,7 +48,10 @@ describe('Default Processing Database', () => { db: new DefaultProcessingDatabase({ database: knex, logger, - refreshIntervalSeconds: 100, + refreshInterval: createRandomRefreshInterval({ + minSeconds: 100, + maxSeconds: 150, + }), }), }; } @@ -61,6 +66,21 @@ describe('Default Processing Database', () => { await db('refresh_state').insert(ref); }; + const parseDate = (date: string | Date): DateTime => { + const parsedDate = + typeof date === 'string' + ? DateTime.fromSQL(date, { zone: 'UTC' }) + : DateTime.fromJSDate(date); + + if (!parsedDate.isValid) { + throw new Error( + `Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`, + ); + } + + return parsedDate; + }; + describe('addUprocessedEntities', () => { function mockEntity(name: string, type: string): Entity { return { @@ -206,6 +226,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + resultHash: '', state: new Map(), relations: [], deferredEntities: [], @@ -224,6 +245,7 @@ describe('Default Processing Database', () => { const options = { id, processedEntity, + resultHash: '', state: new Map(), relations: [], deferredEntities: [], @@ -250,7 +272,11 @@ describe('Default Processing Database', () => { await db.transaction(tx => expect( - db.updateProcessedEntity(tx, { ...options, locationKey: 'fail' }), + db.updateProcessedEntity(tx, { + ...options, + resultHash: '', + locationKey: 'fail', + }), ).rejects.toThrow( `Conflicting write of processing result for ${id} with location key 'fail'`, ), @@ -280,6 +306,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + resultHash: '', state, relations: [], deferredEntities: [], @@ -295,7 +322,9 @@ describe('Default Processing Database', () => { expect(entities[0].processed_entity).toEqual( JSON.stringify(processedEntity), ); - expect(entities[0].cache).toEqual(JSON.stringify(state)); + expect(entities[0].cache).toEqual( + JSON.stringify(Object.fromEntries(state)), + ); expect(entities[0].errors).toEqual("['something broke']"); expect(entities[0].location_key).toEqual('key'); }, @@ -336,6 +365,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + resultHash: '', state: new Map(), relations: relations, deferredEntities: [], @@ -387,6 +417,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + resultHash: '', state: new Map(), relations: [], deferredEntities, @@ -949,5 +980,42 @@ describe('Default Processing Database', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'should update the next_refresh interval with a timestamp that includes refresh spread, %p', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + const entity = JSON.stringify({ + kind: 'Location', + apiVersion: '1.0.0', + metadata: { + name: 'xyz', + }, + } as Entity); + await knex('refresh_state').insert({ + entity_id: '2', + entity_ref: 'location:default/new-root', + unprocessed_entity: entity, + errors: '[]', + next_update_at: '2019-01-01 23:00:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + await db.transaction(async tx => { + // Result does not include the updated timestamp + await db.getProcessableEntities(tx, { + processBatchSize: 1, + }); + }); + const now = DateTime.local(); + const result = await knex('refresh_state') + .where('entity_ref', 'location:default/new-root') + .select(); + const nextUpdate = parseDate(result[0].next_update_at); + expect(nextUpdate.diff(now, 'seconds').seconds).toBeGreaterThanOrEqual( + 100, + ); + }, + 60_000, + ); }); }); diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index 1f1ea4bf84..7326a25a63 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -23,6 +23,8 @@ import { v4 as uuid } from 'uuid'; import type { Logger } from 'winston'; import { Transaction } from '../../database'; import { DeferredEntity } from '../processing/types'; +import { RefreshIntervalFunction } from '../refresh'; +import { initDatabaseMetrics } from './metrics'; import { DbRefreshStateReferencesRow, DbRefreshStateRow, @@ -48,9 +50,11 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { private readonly options: { database: Knex; logger: Logger; - refreshIntervalSeconds: number; + refreshInterval: RefreshIntervalFunction; }, - ) {} + ) { + initDatabaseMetrics(options.database); + } async updateProcessedEntity( txOpaque: Transaction, @@ -60,6 +64,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const { id, processedEntity, + resultHash, state, errors, relations, @@ -69,7 +74,8 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const refreshResult = await tx('refresh_state') .update({ processed_entity: JSON.stringify(processedEntity), - cache: JSON.stringify(state), + result_hash: resultHash, + cache: JSON.stringify(Object.fromEntries(state || [])), errors, location_key: locationKey, }) @@ -122,11 +128,12 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { options: UpdateProcessedEntityOptions, ): Promise { const tx = txOpaque as Knex.Transaction; - const { id, errors } = options; + const { id, errors, resultHash } = options; await tx('refresh_state') .update({ errors, + result_hash: resultHash, }) .where('entity_id', id); } @@ -473,12 +480,10 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { next_update_at: tx.client.config.client === 'sqlite3' ? tx.raw(`datetime('now', ?)`, [ - `${this.options.refreshIntervalSeconds} seconds`, + `${this.options.refreshInterval()} seconds`, ]) : tx.raw( - `now() + interval '${Number( - this.options.refreshIntervalSeconds, - )} seconds'`, + `now() + interval '${this.options.refreshInterval()} seconds'`, ), }); @@ -492,6 +497,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { processedEntity: i.processed_entity ? (JSON.parse(i.processed_entity) as Entity) : undefined, + resultHash: i.result_hash || '', nextUpdateAt: i.next_update_at, lastDiscoveryAt: i.last_discovery_at, state: i.cache diff --git a/plugins/catalog-backend/src/next/database/metrics.ts b/plugins/catalog-backend/src/next/database/metrics.ts new file mode 100644 index 0000000000..a41ad512f0 --- /dev/null +++ b/plugins/catalog-backend/src/next/database/metrics.ts @@ -0,0 +1,72 @@ +/* + * 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 { Knex } from 'knex'; +import { DbLocationsRow } from '../../database/types'; +import { createGaugeMetric } from '../metrics'; +import { DbRefreshStateRow, DbRelationsRow } from './tables'; + +export function initDatabaseMetrics(knex: Knex) { + const seen = new Set(); + return { + entities_count: createGaugeMetric({ + name: 'catalog_entities_count', + help: 'Total amount of entities in the catalog', + labelNames: ['kind'], + async collect() { + const result = await knex('refresh_state').select( + 'entity_ref', + ); + const results = result + .map(row => row.entity_ref.split(':')[0]) + .reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map()); + + results.forEach((value, key) => { + seen.add(key); + this.set({ kind: key }, value); + }); + + // Set all the entities that were not seen to 0 and delete them from the seen set. + seen.forEach(key => { + if (!results.has(key)) { + this.set({ kind: key }, 0); + seen.delete(key); + } + }); + }, + }), + registered_locations: createGaugeMetric({ + name: 'catalog_registered_locations_count', + help: 'Total amount of registered locations in the catalog', + async collect() { + const total = await knex('locations').count({ + count: '*', + }); + this.set(Number(total[0].count)); + }, + }), + relations: createGaugeMetric({ + name: 'catalog_relations_count', + help: 'Total amount of relations between entities', + async collect() { + const total = await knex('relations').count({ + count: '*', + }); + this.set(Number(total[0].count)); + }, + }), + }; +} diff --git a/plugins/catalog-backend/src/next/database/tables.ts b/plugins/catalog-backend/src/next/database/tables.ts index 395694131d..d735607554 100644 --- a/plugins/catalog-backend/src/next/database/tables.ts +++ b/plugins/catalog-backend/src/next/database/tables.ts @@ -25,6 +25,7 @@ export type DbRefreshStateRow = { entity_ref: string; unprocessed_entity: string; processed_entity?: string; + result_hash?: string; cache?: string; next_update_at: string; last_discovery_at: string; // remove? diff --git a/plugins/catalog-backend/src/next/database/types.ts b/plugins/catalog-backend/src/next/database/types.ts index 78199900e4..c93b90084d 100644 --- a/plugins/catalog-backend/src/next/database/types.ts +++ b/plugins/catalog-backend/src/next/database/types.ts @@ -34,6 +34,7 @@ export type AddUnprocessedEntitiesResult = {}; export type UpdateProcessedEntityOptions = { id: string; processedEntity: Entity; + resultHash: string; state?: Map; errors?: string; relations: EntityRelationSpec[]; @@ -44,6 +45,7 @@ export type UpdateProcessedEntityOptions = { export type UpdateProcessedEntityErrorsOptions = { id: string; errors?: string; + resultHash: string; }; export type RefreshStateItem = { @@ -51,6 +53,7 @@ export type RefreshStateItem = { entityRef: string; unprocessedEntity: Entity; processedEntity?: Entity; + resultHash: string; nextUpdateAt: string; lastDiscoveryAt: string; // remove? state: Map; diff --git a/plugins/catalog-backend/src/next/index.ts b/plugins/catalog-backend/src/next/index.ts index 8c1921ae8b..2afdff63fb 100644 --- a/plugins/catalog-backend/src/next/index.ts +++ b/plugins/catalog-backend/src/next/index.ts @@ -17,4 +17,11 @@ export { NextCatalogBuilder } from './NextCatalogBuilder'; export { createNextRouter } from './NextRouter'; export * from './processing'; +export { createRandomRefreshInterval } from './refresh'; +export type { RefreshIntervalFunction } from './refresh'; export * from './stitching'; +export type { + EntityProvider, + EntityProviderConnection, + EntityProviderMutation, +} from './types'; diff --git a/plugins/catalog-backend/src/next/metrics.ts b/plugins/catalog-backend/src/next/metrics.ts new file mode 100644 index 0000000000..4988af82ea --- /dev/null +++ b/plugins/catalog-backend/src/next/metrics.ts @@ -0,0 +1,55 @@ +/* + * 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 { + Counter, + CounterConfiguration, + Gauge, + GaugeConfiguration, + Histogram, + HistogramConfiguration, + register, + Summary, + SummaryConfiguration, +} from 'prom-client'; + +export function createCounterMetric( + config: CounterConfiguration, +): Counter { + const existing = register.getSingleMetric(config.name) as Counter; + return existing || new Counter(config); +} + +export function createGaugeMetric( + config: GaugeConfiguration, +): Gauge { + const existing = register.getSingleMetric(config.name) as Gauge; + return existing || new Gauge(config); +} + +export function createSummaryMetric( + config: SummaryConfiguration, +): Summary { + const existing = register.getSingleMetric(config.name) as Summary; + return existing || new Summary(config); +} + +export function createHistogramMetric( + config: HistogramConfiguration, +): Histogram { + const existing = register.getSingleMetric(config.name) as Histogram; + return existing || new Histogram(config); +} diff --git a/plugins/catalog-backend/src/next/refresh.ts b/plugins/catalog-backend/src/next/refresh.ts new file mode 100644 index 0000000000..aae992f391 --- /dev/null +++ b/plugins/catalog-backend/src/next/refresh.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +/** + * Function that returns the catalog refresh interval in seconds. + */ +export type RefreshIntervalFunction = () => number; + +/** + * Creates a function that returns a random refresh interval between minSeconds and maxSeconds. + * @returns {RefreshIntervalFunction} that provides the next refresh interval + */ +export function createRandomRefreshInterval(options: { + minSeconds: number; + maxSeconds: number; +}): RefreshIntervalFunction { + const { minSeconds, maxSeconds } = options; + return () => { + return Math.random() * (maxSeconds - minSeconds) + minSeconds; + }; +} diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index a60a97759f..9e7b8cd81b 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.5.18 + +### Patch Changes + +- ced85b598: Fix importing yaml files from URLs with trailing query parameters. +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/core-components@0.3.2 + - @backstage/integration@0.6.1 + - @backstage/theme@0.2.10 + ## 0.5.17 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 44609323d5..1e4bc25405 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.5.17", + "version": "0.5.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,12 +32,12 @@ "dependencies": { "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.1", "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -54,9 +54,9 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index f152a6c003..479272bd49 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-react +## 0.4.3 + +### Patch Changes + +- 3bc009287: Clarified messaging around configured locations in the `UnregisterEntityDialog`. +- 2105d608f: Migrate and export `UnregisterEntityDialog` component from `catalog-react` package +- Updated dependencies + - @backstage/core-app-api@0.1.9 + - @backstage/core-components@0.3.2 + - @backstage/integration@0.6.1 + ## 0.4.2 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 57bb5360c2..c5f3226874 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -715,6 +715,17 @@ export function reduceEntityFilters( // @public (undocumented) export const rootRoute: RouteRef; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "UnregisterEntityDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const UnregisterEntityDialog: ({ + open, + onConfirm, + onClose, + entity, +}: Props_3) => JSX.Element; + // Warning: (ae-missing-release-tag) "useEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 426ebb5038..f88cefd4da 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.4.2", + "version": "0.4.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "dependencies": { "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-app-api": "^0.1.8", - "@backstage/core-components": "^0.3.1", + "@backstage/core-app-api": "^0.1.9", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,8 +47,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx similarity index 99% rename from plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx rename to plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx index 55ee42f253..339323c1bf 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { UnregisterEntityDialog } from './UnregisterEntityDialog'; import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { CatalogClient } from '@backstage/catalog-client'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '../../api'; import { screen, waitFor } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import * as state from './useUnregisterEntityDialogState'; diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx similarity index 96% rename from plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx rename to plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index abec90bad1..4dce00e7b7 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntityRefLink } from '@backstage/plugin-catalog-react'; +import { EntityRefLink } from '../EntityRefLink'; import { Box, Button, @@ -155,8 +155,8 @@ const Contents = ({ return ( <> - This entity does not seem to originate from a location. You therefore - only have the option to delete it outright from the catalog. + This entity does not seem to originate from a registered location. You + therefore only have the option to delete it outright from the catalog. + + + ); +}; diff --git a/plugins/home/src/components/index.ts b/plugins/home/src/components/index.ts new file mode 100644 index 0000000000..aae0cbe26d --- /dev/null +++ b/plugins/home/src/components/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { HomepageCompositionRoot } from './HomepageCompositionRoot'; +export { SettingsModal } from './SettingsModal'; diff --git a/plugins/home/src/extensions.tsx b/plugins/home/src/extensions.tsx new file mode 100644 index 0000000000..93fe4ceb4f --- /dev/null +++ b/plugins/home/src/extensions.tsx @@ -0,0 +1,123 @@ +/* + * 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, { Suspense } from 'react'; +import { IconButton } from '@material-ui/core'; +import SettingsIcon from '@material-ui/icons/Settings'; +import { InfoCard } from '@backstage/core-components'; +import { SettingsModal } from './components'; +import { createReactExtension, useApp } from '@backstage/core-plugin-api'; + +export type ComponentRenderer = { + Renderer?: (props: RendererProps) => JSX.Element; +}; + +type ComponentParts = { + Content: () => JSX.Element; + Actions?: () => JSX.Element; + Settings?: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}; + +type RendererProps = { title: string } & ComponentParts; + +export function createCardExtension({ + title, + components, +}: { + title: string; + components: () => Promise; +}) { + return createReactExtension({ + component: { + lazy: () => + components().then(({ Content, Actions, Settings, ContextProvider }) => { + const CardExtension = ({ + Renderer, + title: overrideTitle, + ...childProps + }: ComponentRenderer & { title?: string } & T) => { + const app = useApp(); + const { Progress } = app.getComponents(); + const [settingsOpen, setSettingsOpen] = React.useState(false); + + if (Renderer) { + return ( + }> + + + ); + } + + const cardProps = { + title: overrideTitle ?? title, + ...(Settings + ? { + action: ( + setSettingsOpen(true)}> + Settings + + ), + } + : {}), + ...(Actions + ? { + actions: , + } + : {}), + }; + + const innerContent = ( + + {Settings && ( + setSettingsOpen(false)} + > + + + )} + + + ); + + return ( + }> + {ContextProvider ? ( + + {innerContent} + + ) : ( + innerContent + )} + + ); + }; + return CardExtension; + }), + }, + }); +} diff --git a/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx b/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx new file mode 100644 index 0000000000..cebb0400ab --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx @@ -0,0 +1,29 @@ +/* + * 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 { Button } from '@material-ui/core'; +import { useRandomJoke } from './Context'; + +export const Actions = () => { + const { rerollJoke } = useRandomJoke(); + return ( + + ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Content.tsx b/plugins/home/src/homePageComponents/RandomJoke/Content.tsx new file mode 100644 index 0000000000..231e32356a --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Content.tsx @@ -0,0 +1,31 @@ +/* + * 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 { useRandomJoke } from './Context'; + +export const Content = () => { + const { joke, loading } = useRandomJoke(); + + if (loading) return

Loading...

; + + return ( +
+

{joke.setup}

+

{joke.punchline}

+
+ ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Context.tsx b/plugins/home/src/homePageComponents/RandomJoke/Context.tsx new file mode 100644 index 0000000000..5c2790d8a5 --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Context.tsx @@ -0,0 +1,99 @@ +/* + * 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, { createContext } from 'react'; + +export type JokeType = 'any' | 'programming'; + +type Joke = { + setup: string; + punchline: string; +}; + +type RandomJokeContextValue = { + loading: boolean; + joke: Joke; + type: JokeType; + rerollJoke: Function; + handleChangeType: Function; +}; + +const Context = createContext(undefined); + +const getNewJoke = (type: string): Promise => + fetch( + `https://official-joke-api.appspot.com/jokes${ + type !== 'any' ? `/${type}` : '' + }/random`, + ) + .then(res => res.json()) + .then(data => (Array.isArray(data) ? data[0] : data)); + +export const ContextProvider = ({ + children, + defaultCategory, +}: { + children: JSX.Element; + defaultCategory?: JokeType; +}) => { + const [loading, setLoading] = React.useState(true); + const [joke, setJoke] = React.useState({ + setup: '', + punchline: '', + }); + const [type, setType] = React.useState( + defaultCategory || ('programming' as JokeType), + ); + + const rerollJoke = React.useCallback(() => { + setLoading(true); + getNewJoke(type).then(newJoke => setJoke(newJoke)); + }, [type]); + + const handleChangeType = (newType: JokeType) => { + setType(newType); + }; + + React.useEffect(() => { + setLoading(false); + }, [joke]); + + React.useEffect(() => { + rerollJoke(); + }, [rerollJoke]); + + const value: RandomJokeContextValue = { + loading, + joke, + type, + rerollJoke, + handleChangeType, + }; + + return {children}; +}; + +export const useRandomJoke = () => { + const value = React.useContext(Context); + + if (value === undefined) { + throw new Error('useRandomJoke must be used within a RandomJokeProvider'); + } + + return value; +}; + +export default Context; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx b/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx new file mode 100644 index 0000000000..ee5de69dc6 --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx @@ -0,0 +1,48 @@ +/* + * 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 { + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, +} from '@material-ui/core'; +import React from 'react'; +import { useRandomJoke, JokeType } from './Context'; + +export const Settings = () => { + const { type, handleChangeType } = useRandomJoke(); + const JOKE_TYPES: JokeType[] = ['any' as JokeType, 'programming' as JokeType]; + return ( + + Joke Type + handleChangeType(e.target.value)} + > + {JOKE_TYPES.map(t => ( + } + label={`${t.slice(0, 1).toUpperCase()}${t.slice(1)}`} + /> + ))} + + + ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/index.ts b/plugins/home/src/homePageComponents/RandomJoke/index.ts new file mode 100644 index 0000000000..aafbc17a2e --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { Actions } from './Actions'; +export { Content } from './Content'; +export { Settings } from './Settings'; +export { ContextProvider } from './Context'; diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts new file mode 100644 index 0000000000..812e1b2495 --- /dev/null +++ b/plugins/home/src/index.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export { + homePlugin, + HomepageCompositionRoot, + RandomJokeHomePageComponent, + ComponentAccordion, + ComponentTabs, + ComponentTab, +} from './plugin'; +export { SettingsModal } from './components'; +export { createCardExtension } from './extensions'; diff --git a/plugins/home/src/plugin.test.ts b/plugins/home/src/plugin.test.ts new file mode 100644 index 0000000000..920a9076f4 --- /dev/null +++ b/plugins/home/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { homePlugin } from './plugin'; + +describe('home', () => { + it('should export plugin', () => { + expect(homePlugin).toBeDefined(); + }); +}); diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts new file mode 100644 index 0000000000..5600d2d315 --- /dev/null +++ b/plugins/home/src/plugin.ts @@ -0,0 +1,68 @@ +/* + * 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 { + createComponentExtension, + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; +import { createCardExtension } from './extensions'; + +import { rootRouteRef } from './routes'; + +export const homePlugin = createPlugin({ + id: 'home', + routes: { + root: rootRouteRef, + }, +}); + +export const HomepageCompositionRoot = homePlugin.provide( + createRoutableExtension({ + component: () => + import('./components').then(m => m.HomepageCompositionRoot), + mountPoint: rootRouteRef, + }), +); + +export const ComponentAccordion = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./componentRenderers').then(m => m.ComponentAccordion), + }, + }), +); +export const ComponentTabs = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./componentRenderers').then(m => m.ComponentTabs), + }, + }), +); +export const ComponentTab = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./componentRenderers').then(m => m.ComponentTab), + }, + }), +); + +export const RandomJokeHomePageComponent = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'any' | 'programming' }>({ + title: 'Random Joke', + components: () => import('./homePageComponents/RandomJoke'), + }), +); diff --git a/plugins/home/src/routes.ts b/plugins/home/src/routes.ts new file mode 100644 index 0000000000..2c641d8433 --- /dev/null +++ b/plugins/home/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + title: 'home', +}); diff --git a/plugins/home/src/setupTests.ts b/plugins/home/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/home/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 7bc510f934..b613bfa2e7 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -21,11 +21,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@date-io/luxon": "2.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,9 +38,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 8f2a4bc576..811f5945df 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-jenkins-backend +## 0.1.3 + +### Patch Changes + +- efbb82dd3: Extract `JenkinsConfig` to make writing a custom `JenkinsInfoProvider` easier. +- Updated dependencies + - @backstage/backend-common@0.8.10 + - @backstage/config@0.1.7 + ## 0.1.2 ### Patch Changes diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index b94931e000..01114e74d6 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -34,6 +34,17 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; } +// Warning: (ae-missing-release-tag) "JenkinsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class JenkinsConfig { + constructor(instances: JenkinsInstanceConfig[]); + static fromConfig(config: Config): JenkinsConfig; + getInstanceConfig(jenkinsName?: string): JenkinsInstanceConfig; + // (undocumented) + readonly instances: JenkinsInstanceConfig[]; +} + // Warning: (ae-missing-release-tag) "JenkinsInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -57,6 +68,20 @@ export interface JenkinsInfoProvider { }): Promise; } +// Warning: (ae-missing-release-tag) "JenkinsInstanceConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface JenkinsInstanceConfig { + // (undocumented) + apiKey: string; + // (undocumented) + baseUrl: string; + // (undocumented) + name: string; + // (undocumented) + username: string; +} + // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 6f7beb9444..7a8ea81c27 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-backend", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.7", + "@backstage/backend-common": "^0.8.10", "@backstage/catalog-client": "^0.3.17", "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", + "@backstage/config": "^0.1.7", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.5", + "@backstage/cli": "^0.7.9", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.29.0", diff --git a/plugins/jenkins-backend/src/service/index.ts b/plugins/jenkins-backend/src/service/index.ts index 329092e881..af9986c492 100644 --- a/plugins/jenkins-backend/src/service/index.ts +++ b/plugins/jenkins-backend/src/service/index.ts @@ -14,7 +14,14 @@ * limitations under the License. */ -export type { RouterOptions } from './router'; +export { + DefaultJenkinsInfoProvider, + JenkinsConfig, +} from './jenkinsInfoProvider'; +export type { + JenkinsInfo, + JenkinsInfoProvider, + JenkinsInstanceConfig, +} from './jenkinsInfoProvider'; export { createRouter } from './router'; -export type { JenkinsInfo, JenkinsInfoProvider } from './jenkinsInfoProvider'; -export { DefaultJenkinsInfoProvider } from './jenkinsInfoProvider'; +export type { RouterOptions } from './router'; diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 7d1615ac27..08439297a7 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -14,10 +14,149 @@ * limitations under the License. */ -import { DefaultJenkinsInfoProvider, JenkinsInfo } from './jenkinsInfoProvider'; import { CatalogClient } from '@backstage/catalog-client'; -import { ConfigReader } from '@backstage/config'; import { Entity, EntityName } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultJenkinsInfoProvider, + JenkinsConfig, + JenkinsInfo, +} from './jenkinsInfoProvider'; + +describe('JenkinsConfig', () => { + it('Reads simple config and annotation', async () => { + const config = JenkinsConfig.fromConfig( + new ConfigReader({ + jenkins: { + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + }), + ); + + expect(config.instances).toEqual([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + }); + + it('Reads named default config and annotation', async () => { + const config = JenkinsConfig.fromConfig( + new ConfigReader({ + jenkins: { + instances: [ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ], + }, + }), + ); + + expect(config.instances).toEqual([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + }); + + it('Parses named default config (amongst named other configs)', async () => { + const config = JenkinsConfig.fromConfig( + new ConfigReader({ + jenkins: { + instances: [ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ], + }, + }), + ); + + expect(config.instances).toEqual([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + }); + + it('Gets default Jenkins instance', async () => { + const config = new JenkinsConfig([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + + expect(config.getInstanceConfig()).toEqual({ + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }); + }); + + it('Gets named Jenkins instance', async () => { + const config = new JenkinsConfig([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + + expect(config.getInstanceConfig('other')).toEqual({ + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }); + }); +}); describe('DefaultJenkinsInfoProvider', () => { const mockCatalog: jest.Mocked = { diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index 50875cde99..1a992e73f1 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogClient } from '@backstage/catalog-client'; import { Entity, EntityName, stringifyEntityRef, } from '@backstage/catalog-model'; -import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; export interface JenkinsInfoProvider { @@ -40,145 +40,28 @@ export interface JenkinsInfo { jobFullName: string; // TODO: make this an array } +export interface JenkinsInstanceConfig { + name: string; + baseUrl: string; + username: string; + apiKey: string; +} + /** - * Use default config and annotations, build using fromConfig static function. - * - * This will fallback through various deprecated config and annotation schemes. + * Holds multiple Jenkins configurations. */ -export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { - static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; - static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; +export class JenkinsConfig { + constructor(public readonly instances: JenkinsInstanceConfig[]) {} - private constructor( - private readonly config: { - name: string; - baseUrl: string; - username: string; - apiKey: string; - }[], - private readonly catalog: CatalogClient, - ) {} - - static fromConfig(options: { - config: Config; - catalog: CatalogClient; - }): DefaultJenkinsInfoProvider { - return new DefaultJenkinsInfoProvider( - this.loadConfig(options.config), - options.catalog, - ); - } - - async getInstance(opt: { - entityRef: EntityName; - jobFullName?: string; - }): Promise { - // load entity - const entity = await this.catalog.getEntityByName(opt.entityRef); - if (!entity) { - throw new Error( - `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, - ); - } - - // lookup `[jenkinsName#]jobFullName` from entity annotation - const jenkinsAndJobName = - DefaultJenkinsInfoProvider.getEntityAnnotationValue(entity); - if (!jenkinsAndJobName) { - throw new Error( - `Couldn't find jenkins annotation (${ - DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION - }) on entity with name: ${stringifyEntityRef(opt.entityRef)}`, - ); - } - - let jobFullName; - let jenkinsName: string | undefined; - const splitIndex = jenkinsAndJobName.indexOf(':'); - if (splitIndex === -1) { - // no jenkinsName specified, use default - jobFullName = jenkinsAndJobName; - } else { - // There is a jenkinsName specified - jenkinsName = jenkinsAndJobName.substring(0, splitIndex); - jobFullName = jenkinsAndJobName.substring( - splitIndex + 1, - jenkinsAndJobName.length, - ); - } - - // lookup baseURL + creds from config - const instanceConfig = DefaultJenkinsInfoProvider.getInstanceConfig( - jenkinsName, - this.config, - ); - - const creds = Buffer.from( - `${instanceConfig.username}:${instanceConfig.apiKey}`, - 'binary', - ).toString('base64'); - - return { - baseUrl: instanceConfig.baseUrl, - headers: { - Authorization: `Basic ${creds}`, - }, - jobFullName, - }; - } - - private static getEntityAnnotationValue(entity: Entity) { - return ( - entity.metadata.annotations?.[ - DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION - ] || - entity.metadata.annotations?.[ - DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION - ] - ); - } - - private static getInstanceConfig( - jenkinsName: string | undefined, - config: { - name: string; - baseUrl: string; - username: string; - apiKey: string; - }[], - ): { name: string; baseUrl: string; username: string; apiKey: string } { + /** + * Read all Jenkins instance configurations. + * @param config - Root configuration + * @returns A JenkinsConfig that contains all configured Jenkins instances. + */ + static fromConfig(config: Config): JenkinsConfig { const DEFAULT_JENKINS_NAME = 'default'; - if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) { - // no name provided, use default - const instanceConfig = config.find(c => c.name === DEFAULT_JENKINS_NAME); - - if (!instanceConfig) { - throw new Error( - `Couldn't find a default jenkins instance in the config. Either configure an instance with name ${DEFAULT_JENKINS_NAME} or add a prefix to your annotation value.`, - ); - } - - return instanceConfig; - } - - // A name is provided, look it up. - const instanceConfig = config.find(c => c.name === jenkinsName); - - if (!instanceConfig) { - throw new Error( - `Couldn't find a jenkins instance in the config with name ${jenkinsName}`, - ); - } - return instanceConfig; - } - - private static loadConfig( - rootConfig: Config, - ): { name: string; baseUrl: string; username: string; apiKey: string }[] { - const DEFAULT_JENKINS_NAME = 'default'; - - const jenkinsConfig = rootConfig.getConfig('jenkins'); + const jenkinsConfig = config.getConfig('jenkins'); // load all named instance config const namedInstanceConfig = @@ -223,9 +106,138 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { apiKey: string; }[]; - return [...namedInstanceConfig, ...unnamedInstanceConfig]; + return new JenkinsConfig([ + ...namedInstanceConfig, + ...unnamedInstanceConfig, + ]); } - return namedInstanceConfig; + return new JenkinsConfig(namedInstanceConfig); + } + + /** + * Gets a Jenkins instance configuration by name, or the default one if no + * name is provided. + * @param jenkinsName - Optional name of the Jenkins instance. + * @returns The requested Jenkins instance. + */ + getInstanceConfig(jenkinsName?: string): JenkinsInstanceConfig { + const DEFAULT_JENKINS_NAME = 'default'; + + if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) { + // no name provided, use default + const instanceConfig = this.instances.find( + c => c.name === DEFAULT_JENKINS_NAME, + ); + + if (!instanceConfig) { + throw new Error( + `Couldn't find a default jenkins instance in the config. Either configure an instance with name ${DEFAULT_JENKINS_NAME} or add a prefix to your annotation value.`, + ); + } + + return instanceConfig; + } + + // A name is provided, look it up. + const instanceConfig = this.instances.find(c => c.name === jenkinsName); + + if (!instanceConfig) { + throw new Error( + `Couldn't find a jenkins instance in the config with name ${jenkinsName}`, + ); + } + return instanceConfig; + } +} + +/** + * Use default config and annotations, build using fromConfig static function. + * + * This will fallback through various deprecated config and annotation schemes. + */ +export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { + static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; + static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; + + private constructor( + private readonly config: JenkinsConfig, + private readonly catalog: CatalogClient, + ) {} + + static fromConfig(options: { + config: Config; + catalog: CatalogClient; + }): DefaultJenkinsInfoProvider { + return new DefaultJenkinsInfoProvider( + JenkinsConfig.fromConfig(options.config), + options.catalog, + ); + } + + async getInstance(opt: { + entityRef: EntityName; + jobFullName?: string; + }): Promise { + // load entity + const entity = await this.catalog.getEntityByName(opt.entityRef); + if (!entity) { + throw new Error( + `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, + ); + } + + // lookup `[jenkinsName#]jobFullName` from entity annotation + const jenkinsAndJobName = + DefaultJenkinsInfoProvider.getEntityAnnotationValue(entity); + if (!jenkinsAndJobName) { + throw new Error( + `Couldn't find jenkins annotation (${ + DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION + }) on entity with name: ${stringifyEntityRef(opt.entityRef)}`, + ); + } + + let jobFullName; + let jenkinsName: string | undefined; + const splitIndex = jenkinsAndJobName.indexOf(':'); + if (splitIndex === -1) { + // no jenkinsName specified, use default + jobFullName = jenkinsAndJobName; + } else { + // There is a jenkinsName specified + jenkinsName = jenkinsAndJobName.substring(0, splitIndex); + jobFullName = jenkinsAndJobName.substring( + splitIndex + 1, + jenkinsAndJobName.length, + ); + } + + // lookup baseURL + creds from config + const instanceConfig = this.config.getInstanceConfig(jenkinsName); + + const creds = Buffer.from( + `${instanceConfig.username}:${instanceConfig.apiKey}`, + 'binary', + ).toString('base64'); + + return { + baseUrl: instanceConfig.baseUrl, + headers: { + Authorization: `Basic ${creds}`, + }, + jobFullName, + }; + } + + private static getEntityAnnotationValue(entity: Entity) { + return ( + entity.metadata.annotations?.[ + DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION + ] || + entity.metadata.annotations?.[ + DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION + ] + ); } } diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 90352a7f95..aa363a5759 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-jenkins +## 0.5.4 + +### Patch Changes + +- d7d1f6fb9: Make `isJenkinsAvailable` recognise both the old and the new Jenkins annotation. +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + ## 0.5.3 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 99f5fa2f51..a7e6be999e 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.5.3", + "version": "0.5.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 607d980e4e..2ba5417163 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,9 +34,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index b86ce4b85b..cc002e1e22 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -32,11 +32,11 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/plugin-kubernetes-common": "^0.1.3", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@kubernetes/client-node": "^0.15.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -50,9 +50,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 177c5e2964..84e349cd8e 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -33,10 +33,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 8baf70cd69..5060b02064 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -42,9 +42,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/org/package.json b/plugins/org/package.json index c6be40fc51..59c86188a8 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,9 +35,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index dc918b2ae9..f4e32ea5a0 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -31,10 +31,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index ff6db11a5e..271dea513b 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -48,9 +48,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 7a10046fd0..c1ad3adf3e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -124,6 +124,7 @@ type BranchProtectionOptions = { owner: string; repoName: string; logger: Logger; + requireCodeOwnerReviews: boolean; defaultBranch?: string; }; @@ -132,6 +133,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ client, owner, logger, + requireCodeOwnerReviews, defaultBranch = 'master', }: BranchProtectionOptions): Promise => { const tryOnce = async () => { @@ -153,7 +155,10 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ required_status_checks: { strict: true, contexts: [] }, restrictions: null, enforce_admins: true, - required_pull_request_reviews: { required_approving_review_count: 1 }, + required_pull_request_reviews: { + required_approving_review_count: 1, + require_code_owner_reviews: requireCodeOwnerReviews, + }, }); } catch (e) { if ( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 5e948b52b1..4461d227e8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -22,7 +22,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../helpers'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; import { when } from 'jest-when'; describe('publish:github', () => { @@ -583,4 +586,61 @@ describe('publish:github', () => { 'https://github.com/html/url/blob/main', ); }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repository', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockGithubClient, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: true, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockGithubClient, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockGithubClient, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + }); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index f767d6d38c..5a358822af 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -49,6 +49,7 @@ export function createPublishGithubAction(options: { access?: string; defaultBranch?: string; sourcePath?: string; + requireCodeOwnerReviews?: boolean; repoVisibility: 'private' | 'internal' | 'public'; collaborators: Collaborator[]; topics?: string[]; @@ -75,6 +76,11 @@ export function createPublishGithubAction(options: { description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, type: 'string', }, + requireCodeOwnerReviews: { + title: + 'Require an approved review in PR including files with a designated Code Owner', + type: 'boolean', + }, repoVisibility: { title: 'Repository Visibility', type: 'string', @@ -138,6 +144,7 @@ export function createPublishGithubAction(options: { repoUrl, description, access, + requireCodeOwnerReviews = false, repoVisibility = 'private', defaultBranch = 'master', collaborators, @@ -283,6 +290,7 @@ export function createPublishGithubAction(options: { repoName: newRepo.name, logger: ctx.logger, defaultBranch, + requireCodeOwnerReviews, }); } catch (e) { ctx.logger.warn( diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index d97ce8e54c..88da5445ab 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-scaffolder +## 0.11.1 + +### Patch Changes + +- 5bab4fe2a: Previously when supplying custom scaffolder field extensions, it was necessary to also include the default ones if they were needed. Since the field extensions are keyed by name, there's no harm in leaving the default ones in place when adding custom ones - if templates don't refer to them they will be ignored, and if custom ones are introduced with the same name, the custom ones will take priority over the default ones. + + Users configuring custom field extensions can remove the default ones from the scaffolder route after this change, and they'll still be available: + + ```diff + }> + + - + - + - + - + + + + ``` + +- 24d0e1ea1: Set `id` in ``. +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/config@0.1.7 + - @backstage/core-components@0.3.2 + - @backstage/integration@0.6.1 + - @backstage/theme@0.2.10 + ## 0.11.0 ### Minor Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 4df2a0808c..b005610f04 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -66,6 +66,7 @@ export const EntityPicker: ({ uiSchema, rawErrors, formData, + idSchema, }: FieldProps) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -229,6 +230,8 @@ export const TextValuePicker: ({ rawErrors, formData, uiSchema: { 'ui:autofocus': autoFocus }, + idSchema, + placeholder, }: FieldProps) => JSX.Element; // (No @packageDocumentation comment for this package) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 84720e95a8..c52b4f04f5 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.11.0", + "version": "0.11.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,14 +32,14 @@ "dependencies": { "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/config": "^0.1.7", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.1", "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -63,9 +63,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 43c8dd6ebb..3d7bbcaf5f 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -32,7 +32,7 @@ import { useElementFilter } from '@backstage/core-plugin-api'; export const Router = () => { const outlet = useOutlet(); - const foundExtensions = useElementFilter(outlet, elements => + const customFieldExtensions = useElementFilter(outlet, elements => elements .selectByComponentData({ key: FIELD_EXTENSION_WRAPPER_KEY, @@ -42,9 +42,15 @@ export const Router = () => { }), ); - const fieldExtensions = foundExtensions.length - ? foundExtensions - : DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS; + const fieldExtensions = [ + ...customFieldExtensions, + ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( + ({ name }) => + !customFieldExtensions.some( + customFieldExtension => customFieldExtension.name === name, + ), + ), + ]; return ( diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 04693a94cd..a8ef524a13 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, formatEntityRefTitle, @@ -23,7 +24,6 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import { FieldProps } from '@rjsf/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core-plugin-api'; export const EntityPicker = ({ onChange, @@ -32,6 +32,7 @@ export const EntityPicker = ({ uiSchema, rawErrors, formData, + idSchema, }: FieldProps) => { const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[]; const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined; @@ -58,6 +59,7 @@ export const EntityPicker = ({ error={rawErrors?.length > 0 && !formData} > ) => ( **Important**: The search plugin requires at least Postgres 11! +> **Important**: The search plugin requires at least Postgres 12! ## Getting started diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 3b7fe0b28f..4bf7fdf445 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,7 +19,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.9", + "@backstage/backend-common": "^0.8.10", "@backstage/search-common": "^0.1.2", "@backstage/plugin-search-backend-node": "^0.4.0", "lodash": "^4.17.15", @@ -27,7 +27,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.5", - "@backstage/cli": "^0.7.8" + "@backstage/cli": "^0.7.9" }, "files": [ "dist", diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index ecaa3e2f0f..77f15746a6 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -34,18 +34,18 @@ export class DatabaseDocumentStore implements DatabaseStore { try { const majorVersion = await queryPostgresMajorVersion(knex); - if (majorVersion < 11) { + if (majorVersion < 12) { // We are using some features (like generated columns) that aren't // available in older postgres versions. throw new Error( - `The PgSearchEngine requires at least postgres version 11 (but is running on ${majorVersion})`, + `The PgSearchEngine requires at least postgres version 12 (but is running on ${majorVersion})`, ); } } catch { // Actually both mysql and sqlite have a full text search, too. We could // implement them separately or add them here. throw new Error( - 'The PgSearchEngine is only supported when using a postgres database (>=11.x)', + 'The PgSearchEngine is only supported when using a postgres database (>=12.x)', ); } @@ -59,7 +59,7 @@ export class DatabaseDocumentStore implements DatabaseStore { try { const majorVersion = await queryPostgresMajorVersion(knex); - return majorVersion >= 11; + return majorVersion >= 12; } catch { return false; } diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 637a1f02db..8352ff4fa3 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend +## 0.2.4 + +### Patch Changes + +- 64baedea5: Improve search query logging message +- Updated dependencies + - @backstage/backend-common@0.8.10 + ## 0.2.3 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 982c5a9ae7..1f9b3c1541 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,7 +19,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.6", + "@backstage/backend-common": "^0.8.10", "@backstage/search-common": "^0.1.2", "@backstage/plugin-search-backend-node": "^0.4.0", "@types/express": "^4.17.6", @@ -29,7 +29,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.4", + "@backstage/cli": "^0.7.9", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 27b8acf9cb..3a0a43522e 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -38,7 +38,7 @@ export async function createRouter({ ) => { const { term, filters = {}, pageCursor = '' } = req.query; logger.info( - `Search request received: ${term}, ${JSON.stringify( + `Search request received: term="${term}", filters=${JSON.stringify( filters, )}, ${pageCursor}`, ); diff --git a/plugins/search/package.json b/plugins/search/package.json index a535278e93..88ed91b06c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -31,12 +31,12 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/search-common": "^0.1.3", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -49,9 +49,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 9f5656f1e2..51e4080fc8 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sentry +## 0.3.19 + +### Patch Changes + +- 0e7595d19: Do not show [No Type] when issue metadata includes title instead of type +- d8e4afc62: Render the statsFor property in sentry issues table +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.3 + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + ## 0.3.18 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index a660a15cd4..d773ead18a 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.18", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx index 98833aaeba..ff1960aa4f 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx @@ -44,4 +44,24 @@ describe('Sentry error cell component', () => { 'http://example.com', ); }); + it('should render the title if type is not present', async () => { + const testIssue = { + ...mockIssue, + title: 'Exception: Could not load credentials from any providers', + count: '1', + metadata: {}, + userCount: 2, + permalink: 'http://example.com', + }; + const cell = render( + + + , + ); + const errorType = await cell.findByText('Exception: Could not load cr...'); + expect(errorType.closest('a')).toHaveAttribute( + 'href', + 'http://example.com', + ); + }); }); diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index 57f9cddc05..d2da63f01f 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -44,13 +44,18 @@ const useStyles = makeStyles(theme => ({ export const ErrorCell = ({ sentryIssue }: { sentryIssue: SentryIssue }) => { const classes = useStyles(); + let issueType = '[No Type]'; + if (sentryIssue.metadata.type) { + issueType = sentryIssue.metadata.type; + } else if (sentryIssue.title) { + issueType = sentryIssue.title; + } + return (
- {sentryIssue.metadata.type - ? stripText(sentryIssue.metadata.type, 28) - : '[No type]'} + {stripText(issueType, 28)} { expect(await table.findByText('101')).toBeInTheDocument(); expect(await table.findByText('202')).toBeInTheDocument(); }); + it('should render statsFor in table subtitle', async () => { + const issues: SentryIssue[] = [ + { + ...mockIssue, + metadata: { + type: 'Exception', + value: 'exception was thrown', + }, + count: '101', + userCount: 202, + }, + ]; + const table = await render( + + + , + ); + expect(await table.findByText('Last 24h')).toBeInTheDocument(); + }); }); diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index 67cd354d93..637f2c4355 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -59,14 +59,19 @@ const columns: TableColumn[] = [ type SentryIssuesTableProps = { sentryIssues: SentryIssue[]; + statsFor?: '24h' | '12h'; }; -const SentryIssuesTable = ({ sentryIssues }: SentryIssuesTableProps) => { +const SentryIssuesTable = ({ + sentryIssues, + statsFor, +}: SentryIssuesTableProps) => { return ( ); diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx index 01a2b8be1b..46198e4b93 100644 --- a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -81,5 +81,5 @@ export const SentryIssuesWidget = ({ ); } - return ; + return ; }; diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 4ac197f589..63e9553e5b 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,9 +36,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 13d375dfae..86587f6a1c 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -48,9 +48,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index f2eff3b739..730eb5dc42 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -31,10 +31,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -46,9 +46,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 55e5385a85..43c5bb590d 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -30,9 +30,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -44,9 +44,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index ddb223be89..ddb4799b57 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -33,14 +33,14 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.6.0", "@backstage/integration-react": "^0.1.7", "@backstage/plugin-catalog": "^0.6.11", "@backstage/plugin-catalog-react": "^0.4.2", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -48,20 +48,20 @@ "@types/react": "*", "dompurify": "^2.2.9", "event-source-polyfill": "^1.0.25", + "git-url-parse": "~11.4.4", "lodash": "^4.17.21", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4", "react-text-truncate": "^0.16.0", - "git-url-parse": "~11.4.4" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index bc27aed097..8fdc95f9c7 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-todo-backend +## 0.1.10 + +### Patch Changes + +- 0b34dc7a3: Ignore images and files that are larger than 200KB. +- Updated dependencies + - @backstage/backend-common@0.8.10 + - @backstage/config@0.1.7 + - @backstage/integration@0.6.1 + ## 0.1.9 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 0d59f2b073..91ec21d32d 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo-backend", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,12 +24,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.9", + "@backstage/backend-common": "^0.8.10", "@backstage/catalog-client": "^0.3.16", "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", + "@backstage/config": "^0.1.7", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.9", "@types/supertest": "^2.0.8", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts index a3fc607404..9675a285e0 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts @@ -74,7 +74,13 @@ describe('TodoScmReader', () => { ], }; - await expect(todoReader.readTodos({ url })).resolves.toEqual(expected); + // These two reads should only result in a single call to readTree + await expect( + Promise.all([ + todoReader.readTodos({ url }), + todoReader.readTodos({ url }), + ]), + ).resolves.toEqual([expected, expected]); expect(reader.readTree).toHaveBeenCalledTimes(1); expect(reader.readTree).toHaveBeenCalledWith( diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 256279596f..8442983637 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -27,6 +27,19 @@ import { } from './types'; import { Config } from '@backstage/config'; import { createTodoParser } from './createTodoParser'; +import path from 'path'; + +const excludedExtensions = [ + '.png', + '.svg', + '.jpg', + '.jpeg', + '.gif', + '.raw', + '.lock', + '.ico', +]; +const MAX_FILE_SIZE = 200000; type Options = { logger: Logger; @@ -47,6 +60,7 @@ export class TodoScmReader implements TodoReader { private readonly integrations: ScmIntegrations; private readonly cache = new Map(); + private readonly inFlightReads = new Map>(); static fromConfig(config: Config, options: Omit) { return new TodoScmReader({ @@ -63,16 +77,26 @@ export class TodoScmReader implements TodoReader { } async readTodos({ url }: ReadTodosOptions): Promise { + const inFlightRead = this.inFlightReads.get(url); + if (inFlightRead) { + return inFlightRead.then(read => read.result); + } + const cacheItem = this.cache.get(url); - try { - const newCacheItem = await this.doReadTodos({ url }, cacheItem?.etag); - this.cache.set(url, newCacheItem); - return newCacheItem.result; - } catch (error) { + const newRead = this.doReadTodos({ url }, cacheItem?.etag).catch(error => { if (cacheItem && error.name === 'NotModifiedError') { - return cacheItem.result; + return cacheItem; } throw error; + }); + + this.inFlightReads.set(url, newRead); + try { + const newCacheItem = await newRead; + this.cache.set(url, newCacheItem); + return newCacheItem.result; + } finally { + this.inFlightReads.delete(url); } } @@ -82,8 +106,16 @@ export class TodoScmReader implements TodoReader { ): Promise { const tree = await this.reader.readTree(url, { etag, - filter(path) { - return !path.startsWith('.') && !path.includes('/.'); + filter(filePath, info) { + const extname = path.extname(filePath); + if (info && info.size > MAX_FILE_SIZE) { + return false; + } + return ( + !filePath.startsWith('.') && + !filePath.includes('/.') && + !excludedExtensions.includes(extname) + ); }, }); diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 6cfe7790db..163130b28a 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -27,11 +27,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.1", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -40,9 +40,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index a55390ab86..9dbe093f98 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", "@backstage/core-plugin-api": "^0.1.5", - "@backstage/dev-utils": "^0.2.6", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index d7e7e0e355..53df08e936 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -30,9 +30,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -42,9 +42,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index b0123dcdb1..75ebf9954a 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-xcmetrics +## 0.2.3 + +### Patch Changes + +- eed15251e: Enable browsing detailed build information such as host configuration, errors, warnings, metadata and a timeline for all targets +- 8ee9aa066: Internal refactoring +- Updated dependencies + - @backstage/core-components@0.3.2 + - @backstage/theme@0.2.10 + ## 0.2.2 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 83e57acf70..fd208aeffc 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-xcmetrics", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.3.2", "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/theme": "^0.2.9", + "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,9 +34,9 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.9", + "@backstage/dev-utils": "^0.2.7", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/yarn.lock b/yarn.lock index d14fac46fa..6a30ec6789 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2193,14 +2193,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.14.6" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" - integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.14.8": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.14.8" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== @@ -2312,96 +2305,6 @@ react-use "^17.2.4" zen-observable "^0.8.15" -"@backstage/core-components@^0.1.3", "@backstage/core-components@^0.1.6": - version "0.1.6" - resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.1.6.tgz#a66d9af3be61e76ed60d1026dec376b2d97e0a8e" - integrity sha512-SdBORocyrMa38B2Y3F4Ju45UUm42hLSums+CV4LlHVHKuINGbZg7JZ/WI7tDGks27MXgtU7mLlMlV7tJMeJgKA== - dependencies: - "@backstage/config" "^0.1.5" - "@backstage/core-plugin-api" "^0.1.2" - "@backstage/errors" "^0.1.1" - "@backstage/theme" "^0.2.8" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@testing-library/react-hooks" "^3.4.2" - "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - "@types/react-text-truncate" "^0.14.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - immer "^9.0.1" - lodash "^4.17.15" - material-table "^1.69.1" - pluralize "^8.0.0" - prop-types "^15.7.2" - qs "^6.9.4" - rc-progress "^3.0.0" - react "^16.12.0" - react-dom "^16.12.0" - react-helmet "6.1.0" - react-hook-form "^6.15.4" - react-markdown "^5.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-sparklines "^1.7.0" - react-syntax-highlighter "^15.4.3" - react-text-truncate "^0.16.0" - react-use "^17.2.4" - remark-gfm "^1.0.0" - zen-observable "^0.8.15" - -"@backstage/core-components@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.2.0.tgz#d8c4f06b4b08d60292729b092ed743357802360f" - integrity sha512-pQipnJwZb7usojknLmY9vrMo4aSAo/r3ueGkdk1hPLRRP+8YI9XliMop2l558bHiLAqj7AjASD8ZGAfp8fpYog== - dependencies: - "@backstage/config" "^0.1.5" - "@backstage/core-plugin-api" "^0.1.4" - "@backstage/errors" "^0.1.1" - "@backstage/theme" "^0.2.9" - "@material-table/core" "^3.1.0" - "@material-ui/core" "^4.12.2" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@testing-library/react-hooks" "^3.4.2" - "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - "@types/react-text-truncate" "^0.14.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - immer "^9.0.1" - lodash "^4.17.15" - pluralize "^8.0.0" - prop-types "^15.7.2" - qs "^6.9.4" - rc-progress "^3.0.0" - react "^16.12.0" - react-dom "^16.12.0" - react-helmet "6.1.0" - react-hook-form "^6.15.4" - react-markdown "^5.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-sparklines "^1.7.0" - react-syntax-highlighter "^15.4.3" - react-text-truncate "^0.16.0" - react-use "^17.2.4" - remark-gfm "^1.0.0" - zen-observable "^0.8.15" - "@backstage/core@*": version "0.7.14" resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.14.tgz#863844fe40bb6a29bcc2d297e42055633b0e886f" @@ -2448,40 +2351,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/integration@^0.5.8": - version "0.5.9" - resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.5.9.tgz#9eeb2f570794b196a8e769d8628b68937863a0fc" - integrity sha512-SfYcIHMurasJ5AlNzvq/4+8VLx5ONZ1vZqSG9zX/jvASCyrQY+Ifumaj13XBBuykTv61eHaqsLcEFtd0zgBtpQ== - dependencies: - "@backstage/config" "^0.1.6" - "@octokit/auth-app" "^3.4.0" - "@octokit/rest" "^18.5.3" - cross-fetch "^3.0.6" - git-url-parse "~11.4.4" - luxon "^1.25.0" - -"@backstage/plugin-catalog-react@^0.3.0": - version "0.3.1" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.3.1.tgz#d30a063a4ceb4d446310a687d19c987f55824fdb" - integrity sha512-vKNTW3K6cp/G6f/pjcUe8895s3Z13MnXX6aCkOBcxfLnTJ8eNAJj092rbpkCn33YlCARsHrXUqgVJSqB4AnwtA== - dependencies: - "@backstage/catalog-client" "^0.3.17" - "@backstage/catalog-model" "^0.9.0" - "@backstage/core-app-api" "^0.1.5" - "@backstage/core-components" "^0.1.6" - "@backstage/core-plugin-api" "^0.1.3" - "@backstage/integration" "^0.5.8" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@types/react" "^16.9" - lodash "^4.17.15" - qs "^6.9.4" - react "^16.13.1" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^17.2.4" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3080,14 +2949,15 @@ stream-events "^1.0.1" xdg-basedir "^4.0.0" -"@graphiql/toolkit@^0.1.0": - version "0.1.1" - resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.1.1.tgz#a7da3ba460ceae27bcdc8f03831ca4f88f90f3d7" - integrity sha512-cvsuaPkOA6/TZOdqEdvzqr7i+or2STTpSsteyDkrUXrwftRnH9ZfiUwnHPyf0AC2cKMwpP/Dny/UTS6CLC8ZNQ== +"@graphiql/toolkit@^0.2.0": + version "0.2.2" + resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.2.2.tgz#193d570afcf686c9ee61c92054c1782b9f3c1255" + integrity sha512-kDgYhqnS4p4LqSo1KvLd3tbX8Hhdj0ZrgQuGsosjjEnahiPYmmylxUL1p9lj6348OsypcTlCncGpEjeb9S3TiQ== dependencies: - "@n1ru4l/push-pull-async-iterable-iterator" "^2.0.1" - graphql-ws "^4.1.0" - meros "^1.1.2" + "@n1ru4l/push-pull-async-iterable-iterator" "^2.1.4" + graphql-ws "^4.3.2" + meros "^1.1.4" + optionalDependencies: subscriptions-transport-ws "^0.9.18" "@graphql-codegen/cli@^1.21.3": @@ -4513,10 +4383,10 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.2": - version "4.12.2" - resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.2.tgz#59a8b19f16b8c218d912f37f5ae70473c3c82c73" - integrity sha512-Q1npB8V73IC+eV2X6as+g71MpEGQwqKHUI2iujY62npk35V8nMx/bUXAHjv5kKG1BZ8s8XUWoG6s/VkjYPjjQA== +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": + version "4.12.3" + resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" + integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/styles" "^4.11.4" @@ -4747,10 +4617,10 @@ strict-event-emitter "^0.2.0" xmldom "^0.6.0" -"@n1ru4l/push-pull-async-iterable-iterator@^2.0.1": - version "2.1.2" - resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-2.1.2.tgz#e486bf86c4c29e78601694a26f31c2dec0c08d9b" - integrity sha512-KwZGeX2XK7Xj9ksWwei5923QnqIGoEuLlh3O46OW9vc8hQxjzmMTKCgJMVZ5ne5xaWFQYDT2dMpbUhq6hEOhxA== +"@n1ru4l/push-pull-async-iterable-iterator@^2.1.4": + version "2.1.4" + resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-2.1.4.tgz#a90225474352f9f159bff979905f707b9c6bcf04" + integrity sha512-qLIvoOUJ+zritv+BlzcBMePKNjKQzH9Rb2i9W98YXxf/M62Lye8qH0peyiU8yJ1tL0kfulWi31BoK10E6BKJeA== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -5161,18 +5031,18 @@ resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-3.0.0.tgz#69ece2cb549f0e860b5f89898db90edcc95b15ba" integrity sha512-T2B8QnrDQphbFNxDz7baAa0zTd5TXJmO9soHBPTKKdniRbMEOQ19AJBbZkA3ED2XZa/xrUY/6XjERQLpNACddw== -"@roadiehq/backstage-plugin-buildkite@^1.0.6": - version "1.0.6" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-1.0.6.tgz#f0ca863d206437d8b14e059a0d04fa37553de5f7" - integrity sha512-poC1yaDnH2Gteim8U3SdvVT1jkk2MtMv7YlCq0QwK08RTKUc5PFO6MaDaz95cJ+lndSjYWYn/yii8UmuqrJMUw== +"@roadiehq/backstage-plugin-buildkite@^1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-1.0.8.tgz#c377ae194682426a957366e85263749ff75b8db1" + integrity sha512-v3OOQj5Ksvs/8SZcNBgq6bzmyJvMIzdGsQrwW8uaLgOX076T0nmK8c3Ojz3w1opymEl3s20sLxLUXjhd7V/nIA== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.1.3" + "@backstage/core-components" "^0.3.0" "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.3.0" + "@backstage/plugin-catalog-react" "^0.4.0" "@backstage/theme" "^0.2.6" - "@material-ui/core" "^4.11.3" + "@material-ui/core" "^4.12.1" "@material-ui/icons" "^4.11.2" "@material-ui/lab" "4.0.0-alpha.57" history "^5.0.0" @@ -5184,14 +5054,14 @@ react-router-dom "6.0.0-beta.0" react-use "^17.2.4" -"@roadiehq/backstage-plugin-github-insights@^1.1.20": - version "1.1.22" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.1.22.tgz#8d9a02d69ee55bfbf0c2ff84bf0579b372e6e8b1" - integrity sha512-KGLkJsVPKeF2hlBFNP0934mJ1kyB8Awx8bCKrZfoVw2mdBjkZqq5sB81WooMUBLpQ8QZJfCw+nvIQq22LA6cOg== +"@roadiehq/backstage-plugin-github-insights@^1.1.23": + version "1.1.23" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.1.23.tgz#bcf7424df5a659df39d5aa75f5fffd8e02c9437e" + integrity sha512-3HL9nEtlaE+gFmV0xkrtZGbWHbS26z+NHzEk2KIyV8oqReQ+57PRLTVcXCjFA+djkKfP2hpu5s7zp72aVb0v+w== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.2.0" + "@backstage/core-components" "^0.3.0" "@backstage/core-plugin-api" "^0.1.3" "@backstage/plugin-catalog-react" "^0.4.0" "@backstage/theme" "^0.2.7" @@ -5207,16 +5077,16 @@ react-router "^6.0.0-beta.0" react-use "^17.2.4" -"@roadiehq/backstage-plugin-github-pull-requests@^1.0.10": - version "1.0.10" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.0.10.tgz#8c921393d25a4d498c196fcab7d6138c5e2998ba" - integrity sha512-8mIhWpxIDZBUrfK72+Pc6g8e/nNo+IA0wbDqWbGO53gw8J2dWSFIU06IRpatN672pzkqrrfzt1qhQujzQslRkg== +"@roadiehq/backstage-plugin-github-pull-requests@^1.0.13": + version "1.0.13" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.0.13.tgz#18673d5b906f03fe9b34e388f85e699f51ce89f0" + integrity sha512-k9z2uas1SiCLC93A6kUu8smeKnBA6GbSdY+VhhMgojHmGkhS669HxIbCyt9xyf1S6/G7o6/4MibxHBvHfszI4Q== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.1.3" + "@backstage/core-components" "^0.3.0" "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.3.0" + "@backstage/plugin-catalog-react" "^0.4.0" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@octokit/rest" "^18.5.3" @@ -5230,17 +5100,17 @@ react-router "6.0.0-beta.0" react-use "^17.2.4" -"@roadiehq/backstage-plugin-travis-ci@^1.0.8": - version "1.0.8" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-1.0.8.tgz#33c51ed676191190ba3e2d00e5e9f75b6612b767" - integrity sha512-9iLUnjpF4aCyaD1+0B8OBI3k0nSAzWWqnOSM/4NUwoFDuqhKLlMeN+/HM9L38pp6x1wcwks8sMZh8GufFYl2nw== +"@roadiehq/backstage-plugin-travis-ci@^1.0.11": + version "1.0.11" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-1.0.11.tgz#427c14a93a9f1f62ab42c5f149ab1beba1178fd2" + integrity sha512-EAWIyprpjm6tkHX3AFqZFFf6B74soAbwwO0RJhTfGIPTa/t8H7g5AR5tUF283rmuA2KupYutT5ywXY2Bqcjfyw== dependencies: "@backstage/catalog-model" "^0.9.0" - "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.1.3" - "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.3.0" - "@backstage/theme" "^0.2.6" + "@backstage/core-app-api" "^0.1.6" + "@backstage/core-components" "^0.3.0" + "@backstage/core-plugin-api" "^0.1.4" + "@backstage/plugin-catalog-react" "^0.4.0" + "@backstage/theme" "^0.2.9" "@material-ui/core" "^4.11.3" "@material-ui/icons" "^4.11.2" "@material-ui/lab" "4.0.0-alpha.57" @@ -6562,9 +6432,9 @@ "@types/d3-path" "^1" "@types/d3-shape@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.1.tgz#8cebf5f3d56dd81674e1822279db58a38848a250" - integrity sha512-HnpwE2zl45cOaXLLo0zR4OfRQM9u9sI/ESb0w41PrrsQfkibj8pVIS8VYMSem2Wf5RuWxcgy3/8Kw2/FcIqAEQ== + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" + integrity sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw== dependencies: "@types/d3-path" "*" @@ -9452,6 +9322,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -12432,15 +12307,10 @@ domhandler@^4.0.0: dependencies: domelementtype "^2.1.0" -dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.8: - version "2.2.8" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.2.8.tgz#ce88e395f6d00b6dc53f80d6b2a6fdf5446873c6" - integrity sha512-9H0UL59EkDLgY3dUFjLV6IEUaHm5qp3mxSqWw7Yyx4Zhk2Jn2cmLe+CNPP3xy13zl8Bqg+0NehQzkdMoVhGRww== - -dompurify@^2.2.9: - version "2.3.0" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2" - integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw== +dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.8, dompurify@^2.2.9: + version "2.3.1" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz#a47059ca21fd1212d3c8f71fdea6943b8bfbdf6a" + integrity sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw== domutils@1.5.1: version "1.5.1" @@ -12535,7 +12405,7 @@ downshift@^6.0.15: prop-types "^15.7.2" react-is "^17.0.2" -dset@^3.0.0: +dset@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/dset/-/dset-3.1.0.tgz#23feb6df93816ea452566308b1374d6e869b0d7b" integrity sha512-7xTQ5DzyE59Nn+7ZgXDXjKAGSGmXZHqttMVVz1r4QNfmGpyj+cm2YtI3II0c/+4zS4a9yq2mBhgdeq2QnpcYlw== @@ -13486,6 +13356,14 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" +express-prom-bundle@^6.3.6: + version "6.3.6" + resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.3.6.tgz#c8da1c1024edfcc54953c365991aca57ffd0cfda" + integrity sha512-IRsTRCEKCVCHEriQlZ1FuutjEFc89KASsveXh+1HcGEnuZKiAC4LugxrsGEIdySqYvqOYSr2SWHJ6L8/BK2SHA== + dependencies: + on-finished "^2.3.0" + url-value-parser "^2.0.0" + express-promise-router@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-4.1.0.tgz#79160e145c27610ba411bceb0552a36f11dbab4f" @@ -14900,15 +14778,15 @@ grapheme-splitter@^1.0.4: integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== graphiql@^1.0.0-alpha.10: - version "1.4.0" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.0.tgz#8b17c988720a9da5ea3ebf7ff9d86939b462655e" - integrity sha512-E/Xzfu3YnifdINrK6dKHD1G0qnWkYYK2gHQ//vApUadMb4I4lZQ399ZKt5nqM5kzrATf5FDUTuSpnkaeSoARkQ== + version "1.4.2" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.2.tgz#a1dc1a4d8d35f60c90d6d8a9eb62a99756e9fd9b" + integrity sha512-TQDuuU/ZqTWV1yQDpVEiKskg0IYA+Wck37DYrrFzLlpgZWRbWiyab1PyHKiRep7J540CgScBg6C/gGCymKyO3g== dependencies: - "@graphiql/toolkit" "^0.1.0" + "@graphiql/toolkit" "^0.2.0" codemirror "^5.54.0" codemirror-graphql "^1.0.0" copy-to-clipboard "^3.2.0" - dset "^3.0.0" + dset "^3.1.0" entities "^2.0.0" graphql-language-service "^3.1.2" markdown-it "^10.0.0" @@ -15037,7 +14915,12 @@ graphql-type-json@^0.3.2: resolved "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115" integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg== -graphql-ws@^4.1.0, graphql-ws@^4.4.1: +graphql-ws@^4.3.2: + version "4.9.0" + resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz#5cfd8bb490b35e86583d8322f5d5d099c26e365c" + integrity sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag== + +graphql-ws@^4.4.1: version "4.7.0" resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.7.0.tgz#b323fbf35a3736eed85dac24c0054d6d10c93e62" integrity sha512-Md8SsmC9ZlsogFPd3Ot8HbIAAqsHh8Xoq7j4AmcIat1Bh6k91tjVyQvA0Au1/BolXSYq+RDvib6rATU2Hcf1Xw== @@ -18728,7 +18611,7 @@ luxon@2.0.2, luxon@^2.0.2: resolved "https://registry.npmjs.org/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== -luxon@^1.25.0, luxon@^1.27.0: +luxon@^1.27.0: version "1.28.0" resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== @@ -19190,7 +19073,7 @@ merge@^2.1.0: resolved "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== -meros@1.1.4, meros@^1.1.2: +meros@1.1.4, meros@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948" integrity sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ== @@ -19399,7 +19282,7 @@ minimatch@0.3: lru-cache "2" sigmund "~1.0.0" -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -20457,7 +20340,7 @@ oidc-token-hash@^5.0.1: resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6" integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ== -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -22095,6 +21978,13 @@ progress@^2.0.0: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@^13.2.0: + version "13.2.0" + resolved "https://registry.npmjs.org/prom-client/-/prom-client-13.2.0.tgz#99d13357912dd400f8911b77df19f7b328a93e92" + integrity sha512-wGr5mlNNdRNzEhRYXgboUU2LxHWIojxscJKmtG3R8f4/KiWqyYgXTLHs0+Ted7tG3zFT7pgHJbtomzZ1L0ARaQ== + dependencies: + tdigest "^0.1.1" + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -22691,12 +22581,7 @@ react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== - -react-is@^17.0.2: +"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -25680,6 +25565,13 @@ tarn@^3.0.1: resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw== +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + teeny-request@^3.11.3: version "3.11.3" resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz#335c629f7645e5d6599362df2f3230c4cbc23a55" @@ -26858,6 +26750,11 @@ url-parse@^1.4.3, url-parse@^1.5.1: querystringify "^2.1.1" requires-port "^1.0.0" +url-value-parser@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.0.3.tgz#cd4b8d6754e458d65e8125260c09718d926e6e21" + integrity sha512-FjIX+Q9lYmDM9uYIGdMYfQW0uLbWVwN2NrL2ayAI7BTOvEwzH+VoDdNquwB9h4dFAx+u6mb0ONLa3sHD5DvyvA== + url@0.10.3: version "0.10.3" resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"