From c3a91d510327d97bc3630e6f8f935000bb768337 Mon Sep 17 00:00:00 2001 From: Lucifergene <47265560+Lucifergene@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:09:19 +0530 Subject: [PATCH 001/115] Add support for PostgreSQL 17 and update related test configurations Signed-off-by: Lucifergene <47265560+Lucifergene@users.noreply.github.com> --- .changeset/long-apples-jog.md | 5 +++++ .github/workflows/ci.yml | 12 ++++++------ .github/workflows/deploy_packages.yml | 12 ++++++------ .github/workflows/verify_e2e-linux.yml | 2 +- .../building-plugins-and-modules/02-testing.md | 4 ++-- docs/overview/versioning-policy.md | 4 ++-- .../scheduler/lib/PluginTaskSchedulerImpl.test.ts | 2 +- .../scheduler/lib/PluginTaskSchedulerJanitor.test.ts | 4 ++-- packages/backend-defaults/src/setupTests.ts | 2 +- packages/backend-test-utils/report.api.md | 1 + packages/backend-test-utils/src/database/types.ts | 8 ++++++++ plugins/app-backend/src/setupTests.ts | 2 +- plugins/auth-backend/src/setupTests.ts | 2 +- .../IncrementalIngestionDatabaseManager.test.ts | 2 +- .../src/module/WrapperProviders.test.ts | 2 +- plugins/catalog-backend/src/setupTests.ts | 2 +- .../tests/performance/getEntitiesPerformance.test.ts | 2 +- .../getProcessableEntitiesPerformance.test.ts | 2 +- .../tests/performance/providerDeltaMutations.test.ts | 2 +- .../tests/performance/stitchingPerformance.test.ts | 2 +- .../events-backend/src/service/EventsPlugin.test.ts | 2 +- .../src/service/hub/DatabaseEventBusStore.test.ts | 2 +- .../src/database/util.test.ts | 2 +- 23 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 .changeset/long-apples-jog.md diff --git a/.changeset/long-apples-jog.md b/.changeset/long-apples-jog.md new file mode 100644 index 0000000000..4f57576cc4 --- /dev/null +++ b/.changeset/long-apples-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added support for PostgreSQL version 17 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8bf57cc95..6a03d81e96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,8 +157,8 @@ jobs: name: Test ${{ matrix.node-version }} services: - postgres16: - image: postgres:16 + postgres17: + image: postgres:17 env: POSTGRES_PASSWORD: postgres options: >- @@ -168,8 +168,8 @@ jobs: --health-retries 5 ports: - 5432/tcp - postgres12: - image: postgres:12 + postgres13: + image: postgres:13 env: POSTGRES_PASSWORD: postgres options: >- @@ -249,8 +249,8 @@ jobs: run: yarn backstage-cli repo test --maxWorkers=3 --workerIdleMemoryLimit=1300M --since origin/master --successCache --successCacheDir .cache/backstage-cli env: BACKSTAGE_TEST_DISABLE_DOCKER: 1 - BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres16.ports[5432] }} - BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres12.ports[5432] }} + BACKSTAGE_TEST_DATABASE_POSTGRES17_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres17.ports[5432] }} + BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }} diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index d1d7d3fd7c..3c67b2c387 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -16,8 +16,8 @@ jobs: node-version: [20.x, 22.x] services: - postgres16: - image: postgres:16 + postgres17: + image: postgres:17 env: POSTGRES_PASSWORD: postgres options: >- @@ -27,8 +27,8 @@ jobs: --health-retries 5 ports: - 5432/tcp - postgres12: - image: postgres:12 + postgres13: + image: postgres:13 env: POSTGRES_PASSWORD: postgres options: >- @@ -118,8 +118,8 @@ jobs: yarn backstage-cli repo test --maxWorkers=3 --workerIdleMemoryLimit=1300M --coverage --successCache --successCacheDir .cache/backstage-cli env: BACKSTAGE_TEST_DISABLE_DOCKER: 1 - BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres16.ports[5432] }} - BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres12.ports[5432] }} + BACKSTAGE_TEST_DATABASE_POSTGRES17_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres17.ports[5432] }} + BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }} diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 30887980a4..a32d7bc26e 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -21,7 +21,7 @@ jobs: services: postgres: - image: postgres:12 + image: postgres:13 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 1ec100cff3..439fed597a 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -190,7 +190,7 @@ describe('MyDatabaseClass', () => { // "physical" databases to test against is much costlier than creating the // "logical" databases within them that the individual tests use. const databases = TestDatabases.create({ - ids: ['POSTGRES_16', 'POSTGRES_12', 'SQLITE_3', 'MYSQL_8'], + ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3', 'MYSQL_8'], }); // Just an example of how to conveniently bundle up the setup code @@ -236,8 +236,8 @@ your CI environment is able to supply databases natively, the `TestDatabases` support custom connection strings through the use of environment variables that it'll take into account when present. +- `BACKSTAGE_TEST_DATABASE_POSTGRES17_CONNECTION_STRING` - `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING` -- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING` - `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING` ## Testing Service Factories diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index f5e2e222f8..12103f255d 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -106,7 +106,7 @@ be part of the `1.12` Backstage release. The following versioning policy applies to all packages: - Breaking changes are noted in the changelog, and documentation is updated. -- Breaking changes are prefixed with `**BREAKING**: ` in the changelog. +- Breaking changes are prefixed with `**BREAKING**:` in the changelog. - All public exports are considered stable and will have an entry in the changelog - Breaking changes are recommended to document a clear upgrade path in the @@ -195,4 +195,4 @@ The Backstage project recommends and supports using PostgreSQL for persistent st The PostgreSQL [versioning policy](https://www.postgresql.org/support/versioning/) is to release a new major version every year with new features which is then supported for 5 years after its initial release. -Our policy mirrors the PostgreSQL versioning policy - we will support the last 5 major versions. We will also test the newest and oldest versions in that range. For example, if the range we support is currently 12 to 16, then we would only test 12 and 16 explicitly. +Our policy mirrors the PostgreSQL versioning policy - we will support the last 5 major versions. We will also test the newest and oldest versions in that range. For example, if the range we support is currently 13 to 17, then we would only test 13 and 17 explicitly. diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index 1ce1401829..712ed63a2f 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -33,7 +33,7 @@ jest.setTimeout(60_000); describe('PluginTaskManagerImpl', () => { const addShutdownHook = jest.fn(); const databases = TestDatabases.create({ - ids: ['POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); beforeAll(async () => { diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts index 388f882afc..b2c6af24cb 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts @@ -39,8 +39,8 @@ describe('PluginTaskSchedulerJanitor', () => { const databases = TestDatabases.create({ ids: [ /* 'MYSQL_8' not supported yet */ - 'POSTGRES_16', - 'POSTGRES_12', + 'POSTGRES_17', + 'POSTGRES_13', 'SQLITE_3', 'MYSQL_8', ], diff --git a/packages/backend-defaults/src/setupTests.ts b/packages/backend-defaults/src/setupTests.ts index 76619a2542..c9745b280d 100644 --- a/packages/backend-defaults/src/setupTests.ts +++ b/packages/backend-defaults/src/setupTests.ts @@ -21,5 +21,5 @@ import { Settings } from 'luxon'; Settings.throwOnInvalid = true; TestDatabases.setDefaults({ - ids: ['MYSQL_8', 'POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index a79bd9d03c..879d7dd28f 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -459,6 +459,7 @@ export class TestCaches { // @public export type TestDatabaseId = + | 'POSTGRES_17' | 'POSTGRES_16' | 'POSTGRES_15' | 'POSTGRES_14' diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index 39a8af065c..e1ef61a93b 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -28,6 +28,7 @@ export interface Engine { * @public */ export type TestDatabaseId = + | 'POSTGRES_17' | 'POSTGRES_16' | 'POSTGRES_15' | 'POSTGRES_14' @@ -47,6 +48,13 @@ export type TestDatabaseProperties = { export const allDatabases: Record = Object.freeze({ + POSTGRES_17: { + name: 'Postgres 17.x', + driver: 'pg', + dockerImageName: getDockerImageForName('postgres:17'), + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_POSTGRES17_CONNECTION_STRING', + }, POSTGRES_16: { name: 'Postgres 16.x', driver: 'pg', diff --git a/plugins/app-backend/src/setupTests.ts b/plugins/app-backend/src/setupTests.ts index 4ed508a382..ea10a94839 100644 --- a/plugins/app-backend/src/setupTests.ts +++ b/plugins/app-backend/src/setupTests.ts @@ -17,5 +17,5 @@ import { TestDatabases } from '@backstage/backend-test-utils'; TestDatabases.setDefaults({ - ids: ['MYSQL_8', 'POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); diff --git a/plugins/auth-backend/src/setupTests.ts b/plugins/auth-backend/src/setupTests.ts index f7c56ef27d..5af5f05360 100644 --- a/plugins/auth-backend/src/setupTests.ts +++ b/plugins/auth-backend/src/setupTests.ts @@ -19,5 +19,5 @@ import { TestDatabases } from '@backstage/backend-test-utils'; export {}; TestDatabases.setDefaults({ - ids: ['MYSQL_8', 'POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.test.ts index a8862cfc2a..bed216b5de 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.test.ts @@ -24,7 +24,7 @@ jest.setTimeout(60_000); describe('IncrementalIngestionDatabaseManager', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); it.each(databases.eachSupportedId())( diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts index 95b3f203c4..877e32acfb 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts @@ -25,7 +25,7 @@ jest.setTimeout(60_000); describe('WrapperProviders', () => { const applyDatabaseMigrations = jest.fn(); const databases = TestDatabases.create({ - ids: ['POSTGRES_16', 'POSTGRES_12', 'SQLITE_3', 'MYSQL_8'], + ids: ['POSTGRES_17', 'POSTGRES_13', 'SQLITE_3', 'MYSQL_8'], }); const config = new ConfigReader({}); const logger = mockServices.logger.mock(); diff --git a/plugins/catalog-backend/src/setupTests.ts b/plugins/catalog-backend/src/setupTests.ts index f7c56ef27d..5af5f05360 100644 --- a/plugins/catalog-backend/src/setupTests.ts +++ b/plugins/catalog-backend/src/setupTests.ts @@ -19,5 +19,5 @@ import { TestDatabases } from '@backstage/backend-test-utils'; export {}; TestDatabases.setDefaults({ - ids: ['MYSQL_8', 'POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); diff --git a/plugins/catalog-backend/src/tests/performance/getEntitiesPerformance.test.ts b/plugins/catalog-backend/src/tests/performance/getEntitiesPerformance.test.ts index 9d1bb8d80a..aa76bace54 100644 --- a/plugins/catalog-backend/src/tests/performance/getEntitiesPerformance.test.ts +++ b/plugins/catalog-backend/src/tests/performance/getEntitiesPerformance.test.ts @@ -40,7 +40,7 @@ import { describePerformanceTest, performanceTraceEnabled } from './lib/env'; jest.setTimeout(600_000); const databases = TestDatabases.create({ - ids: [/* 'MYSQL_8', */ 'POSTGRES_16', /* 'POSTGRES_12',*/ 'SQLITE_3'], + ids: [/* 'MYSQL_8', */ 'POSTGRES_17', /* 'POSTGRES_13',*/ 'SQLITE_3'], disableDocker: false, }); diff --git a/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts b/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts index 8218ae82cc..e2ed160b42 100644 --- a/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts +++ b/plugins/catalog-backend/src/tests/performance/getProcessableEntitiesPerformance.test.ts @@ -25,7 +25,7 @@ import { describePerformanceTest, performanceTraceEnabled } from './lib/env'; jest.setTimeout(600_000); const databases = TestDatabases.create({ - ids: [/* 'MYSQL_8', */ 'POSTGRES_16', /* 'POSTGRES_12',*/ 'SQLITE_3'], + ids: [/* 'MYSQL_8', */ 'POSTGRES_17', /* 'POSTGRES_13',*/ 'SQLITE_3'], disableDocker: false, }); diff --git a/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts b/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts index 6e95c2535a..377843643b 100644 --- a/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts +++ b/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts @@ -26,7 +26,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node'; jest.setTimeout(600_000); const databases = TestDatabases.create({ - ids: [/* 'MYSQL_8', */ 'POSTGRES_16', /* 'POSTGRES_12',*/ 'SQLITE_3'], + ids: [/* 'MYSQL_8', */ 'POSTGRES_17', /* 'POSTGRES_13',*/ 'SQLITE_3'], disableDocker: false, }); diff --git a/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts b/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts index a4d843649e..d0bf7c2cd8 100644 --- a/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts +++ b/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts @@ -143,7 +143,7 @@ class Tracker { describePerformanceTest('stitchingPerformance', () => { const databases = TestDatabases.create({ - ids: [/* 'MYSQL_8', */ 'POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + ids: [/* 'MYSQL_8', */ 'POSTGRES_17', 'POSTGRES_13', 'SQLITE_3'], }); it.each(databases.eachSupportedId())( diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index 35813d7b8e..12a0f63002 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -141,7 +141,7 @@ describe('eventsPlugin', () => { } const databases = TestDatabases.create({ - ids: ['SQLITE_3', 'MYSQL_8', 'POSTGRES_9', 'POSTGRES_13', 'POSTGRES_16'], + ids: ['SQLITE_3', 'MYSQL_8', 'POSTGRES_13', 'POSTGRES_17'], }); async function mockKnexFactory(databaseId: TestDatabaseId) { diff --git a/plugins/events-backend/src/service/hub/DatabaseEventBusStore.test.ts b/plugins/events-backend/src/service/hub/DatabaseEventBusStore.test.ts index ade462a920..959341cbc0 100644 --- a/plugins/events-backend/src/service/hub/DatabaseEventBusStore.test.ts +++ b/plugins/events-backend/src/service/hub/DatabaseEventBusStore.test.ts @@ -24,7 +24,7 @@ import { DatabaseEventBusStore } from './DatabaseEventBusStore'; const logger = mockServices.logger.mock(); const databases = TestDatabases.create({ - ids: ['POSTGRES_9', 'POSTGRES_13', 'POSTGRES_16'], + ids: ['POSTGRES_13', 'POSTGRES_17'], }); const maybeDescribe = diff --git a/plugins/search-backend-module-pg/src/database/util.test.ts b/plugins/search-backend-module-pg/src/database/util.test.ts index 38d997c836..45f0aea2c9 100644 --- a/plugins/search-backend-module-pg/src/database/util.test.ts +++ b/plugins/search-backend-module-pg/src/database/util.test.ts @@ -38,7 +38,7 @@ describe('util', () => { describe('supported', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_16', 'POSTGRES_12'], + ids: ['POSTGRES_17', 'POSTGRES_13'], }); if (databases.eachSupportedId().length < 1) { From 2d11521f8cb40757735e6dd317015840799949d7 Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Mon, 10 Feb 2025 13:02:31 -0500 Subject: [PATCH 002/115] fix home starred entities display Signed-off-by: Stephen Glass --- .changeset/cyan-needles-hear.md | 5 +++++ .../src/homePageComponents/StarredEntities/Content.tsx | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/cyan-needles-hear.md diff --git a/.changeset/cyan-needles-hear.md b/.changeset/cyan-needles-hear.md new file mode 100644 index 0000000000..0decabf45b --- /dev/null +++ b/.changeset/cyan-needles-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +The starred entities component uses the entity title or display name if it exists diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx index bc5e3e2923..ce1810a9d0 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx @@ -73,7 +73,14 @@ export const Content = ({ return ( await catalogApi.getEntitiesByRefs({ entityRefs: [...starredEntities], - fields: ['kind', 'metadata.namespace', 'metadata.name', 'spec.type'], + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'spec.type', + 'metadata.title', + 'spec.profile.displayName', + ], }) ).items.filter((e): e is Entity => !!e); }, [catalogApi, starredEntities]); From fa47e818da1496adbba145c6d36df8a54493dc79 Mon Sep 17 00:00:00 2001 From: Sayyid Yofa Date: Wed, 12 Feb 2025 21:47:30 +0700 Subject: [PATCH 003/115] update readme plugin oauth2, oidc, and okta to refer to github.com Signed-off-by: Sayyid Yofa --- plugins/auth-backend-module-oauth2-provider/README.md | 2 +- plugins/auth-backend-module-oidc-provider/README.md | 2 +- plugins/auth-backend-module-okta-provider/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend-module-oauth2-provider/README.md b/plugins/auth-backend-module-oauth2-provider/README.md index 6c23d24ff3..1a6937515e 100644 --- a/plugins/auth-backend-module-oauth2-provider/README.md +++ b/plugins/auth-backend-module-oauth2-provider/README.md @@ -4,5 +4,5 @@ This module provides an oauth2 auth provider implementation for `@backstage/plug ## Links -- [Repository](https://oauth2.com/backstage/backstage/tree/master/plugins/auth-backend-module-oauth2-provider) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-oauth2-provider) - [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-oidc-provider/README.md b/plugins/auth-backend-module-oidc-provider/README.md index e586f32fd7..1cbe3525cd 100644 --- a/plugins/auth-backend-module-oidc-provider/README.md +++ b/plugins/auth-backend-module-oidc-provider/README.md @@ -4,5 +4,5 @@ This module provides an Oidc auth provider implementation for `@backstage/plugin ## Links -- [Repository](https://oidc.com/backstage/backstage/tree/master/plugins/auth-backend-module-oidc-provider) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-oidc-provider) - [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-okta-provider/README.md b/plugins/auth-backend-module-okta-provider/README.md index 3a320a9b1a..51aa13c47e 100644 --- a/plugins/auth-backend-module-okta-provider/README.md +++ b/plugins/auth-backend-module-okta-provider/README.md @@ -26,5 +26,5 @@ export const okta = createAuthProviderIntegration({ ## Links -- [Repository](https://okta.com/backstage/backstage/tree/master/plugins/auth-backend-module-okta-provider) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-okta-provider) - [Backstage Project Homepage](https://backstage.io) From ce15e30edf194fe91611debdb461040a65c11689 Mon Sep 17 00:00:00 2001 From: Sayyid Yofa Date: Wed, 12 Feb 2025 21:48:50 +0700 Subject: [PATCH 004/115] add changeset Signed-off-by: Sayyid Yofa --- .changeset/small-onions-live.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/small-onions-live.md diff --git a/.changeset/small-onions-live.md b/.changeset/small-onions-live.md new file mode 100644 index 0000000000..68d691e590 --- /dev/null +++ b/.changeset/small-onions-live.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-auth-backend-module-oauth2-provider': patch +'@backstage/plugin-auth-backend-module-oidc-provider': patch +'@backstage/plugin-auth-backend-module-okta-provider': patch +--- + +update readme plugin oauth2, oidc, and okta to refer to github.com From b7b34da413a5307e88dafcd4804a0d5a33e870dc Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 12 Feb 2025 21:09:22 +0100 Subject: [PATCH 005/115] Update .changeset/small-onions-live.md Signed-off-by: Vincenzo Scamporlino --- .changeset/small-onions-live.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/small-onions-live.md b/.changeset/small-onions-live.md index 68d691e590..fc2ed28fe9 100644 --- a/.changeset/small-onions-live.md +++ b/.changeset/small-onions-live.md @@ -4,4 +4,4 @@ '@backstage/plugin-auth-backend-module-okta-provider': patch --- -update readme plugin oauth2, oidc, and okta to refer to github.com +Fixed repository url in `README.md` From 8515a1aea79a0ed6ff8df514082f58df5a7c3b4d Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Fri, 14 Feb 2025 11:34:13 -0500 Subject: [PATCH 006/115] check for expected fields Signed-off-by: Stephen Glass --- .../StarredEntities/Content.test.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx index ad26482961..b2522cda96 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -50,7 +50,24 @@ describe('StarredEntitiesContent', () => { mockedApi.toggleStarred('component:default/mock-starred-entity-2'); mockedApi.toggleStarred('component:default/mock-starred-entity-3'); - const mockCatalogApi = catalogApiMock({ entities }); + const mockCatalogApi = catalogApiMock.mock({ + getEntitiesByRefs: jest.fn().mockImplementation(async ({ fields }) => { + const expectedFields = [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'spec.type', + 'metadata.title', + 'spec.profile.displayName', + ]; + expectedFields.forEach(field => { + expect(fields).toContain(field); + }); + return { + items: entities, + }; + }), + }); const { getByText, queryByText } = await renderInTestApp( Date: Tue, 18 Feb 2025 11:15:44 +0100 Subject: [PATCH 007/115] fix(core-components): SimpleStepper back button works with activeStep set higher than 0 Signed-off-by: Lex Felix <112618115+lexfelixpost@users.noreply.github.com> --- .changeset/beige-chairs-suffer.md | 5 ++++ .../SimpleStepper/SimpleStepper.test.tsx | 28 +++++++++++++++++++ .../SimpleStepper/SimpleStepper.tsx | 13 ++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .changeset/beige-chairs-suffer.md diff --git a/.changeset/beige-chairs-suffer.md b/.changeset/beige-chairs-suffer.md new file mode 100644 index 0000000000..8989921373 --- /dev/null +++ b/.changeset/beige-chairs-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +SimpleStepper back button now works with activeStep property set higher than 0 diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx index 73baff8db2..e4093f07d2 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx @@ -173,4 +173,32 @@ describe('Stepper', () => { fireEvent.click(getTextInSlide(rendered, 2)('Back') as Node); expect(rendered.getByText('step1')).toBeInTheDocument(); }); + + it('Handles onBack action properly when activeStep is higher than 0', async () => { + const rendered = await renderInTestApp( + + +
step0
+
+ +
step1
+
+ +
step2
+
+
, + ); + + fireEvent.click(getTextInSlide(rendered, 2)('Back') as Node); + expect(rendered.getByText('step1')).toBeInTheDocument(); + + fireEvent.click(getTextInSlide(rendered, 1)('Back') as Node); + expect(rendered.getByText('step0')).toBeInTheDocument(); + + fireEvent.click(getTextInSlide(rendered, 0)('Next') as Node); + expect(rendered.getByText('step1')).toBeInTheDocument(); + + fireEvent.click(getTextInSlide(rendered, 1)('Next') as Node); + expect(rendered.getByText('step2')).toBeInTheDocument(); + }); }); diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx index 4a1a4ab919..b0b6c1afd0 100644 --- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx +++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx @@ -32,6 +32,7 @@ type InternalState = { }; const noop = () => {}; + export const VerticalStepperContext = React.createContext({ stepperLength: 0, stepIndex: 0, @@ -50,7 +51,17 @@ export interface StepperProps { export function SimpleStepper(props: PropsWithChildren) { const { children, elevated, onStepChange, activeStep = 0 } = props; const [stepIndex, setStepIndex] = useState(activeStep); - const [stepHistory, setStepHistory] = useState([0]); + /* + Recreates the stepHistory array based on the activeStep + to make sure the handleBack function of the Footer works when activeStep is higher than 0 + */ + const inOrderRecreatedStepHistory = Array.from( + { length: activeStep + 1 }, + (_, i) => i, + ); + const [stepHistory, setStepHistory] = useState( + inOrderRecreatedStepHistory, + ); useEffect(() => { setStepIndex(activeStep); From b95aa77ce2f9349007d88d96c5a6943561e7051e Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 12 Feb 2025 22:25:37 -0300 Subject: [PATCH 008/115] feat(events-backend,events-node): add a parser option to events by content type Signed-off-by: Rogerio Angeliski --- .changeset/ten-impalas-hope.md | 6 ++ plugins/events-backend/README.md | 42 +++++++++ plugins/events-backend/report.api.md | 4 + .../src/service/EventsPlugin.ts | 20 ++++- .../HttpPostIngressEventPublisher.test.ts | 87 ++++++++++++++++++- .../http/HttpPostIngressEventPublisher.ts | 69 +++++---------- .../HttpApplicationJsonBodyParser.ts | 45 ++++++++++ .../src/service/http/body-parser/index.ts | 22 +++++ .../events-backend/src/service/http/errors.ts | 38 ++++++++ plugins/events-node/package.json | 2 + plugins/events-node/report-alpha.api.md | 3 + plugins/events-node/report.api.md | 22 +++++ .../src/api/http/HttpBodyParserOptions.ts | 24 +++++ .../api/http/body-parser/HttpBodyParser.ts | 33 +++++++ .../src/api/http/body-parser/index.ts | 16 ++++ plugins/events-node/src/api/http/index.ts | 2 + plugins/events-node/src/extensions.ts | 3 + yarn.lock | 2 + 18 files changed, 393 insertions(+), 47 deletions(-) create mode 100644 .changeset/ten-impalas-hope.md create mode 100644 plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts create mode 100644 plugins/events-backend/src/service/http/body-parser/index.ts create mode 100644 plugins/events-backend/src/service/http/errors.ts create mode 100644 plugins/events-node/src/api/http/HttpBodyParserOptions.ts create mode 100644 plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts create mode 100644 plugins/events-node/src/api/http/body-parser/index.ts diff --git a/.changeset/ten-impalas-hope.md b/.changeset/ten-impalas-hope.md new file mode 100644 index 0000000000..b770b15c6b --- /dev/null +++ b/.changeset/ten-impalas-hope.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-events-backend': minor +'@backstage/plugin-events-node': minor +--- + +add `addHttpPostBodyParser` to events extension to allow body parse customization. This feature will enhance flexibility in handling HTTP POST requests in event-related operations. diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md index 7f88452ff6..219b60f213 100644 --- a/plugins/events-backend/README.md +++ b/plugins/events-backend/README.md @@ -140,6 +140,48 @@ export const eventsModuleYourFeature = createBackendModule({ }); ``` +### Request Body Parse + +We need to parse the request body before we can validate it. We have some default parsers but you can provide your own when necessary. + +```ts +import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; + +// [...] + +export const eventsModuleYourFeature = createBackendModule({ + pluginId: 'events', + moduleId: 'your-feature', + register(env) { + // [...] + env.registerInit({ + deps: { + // [...] + events: eventsExtensionPoint, + // [...] + }, + async init({ /* ... */ events /*, ... */ }) { + // [...] + events.addHttpPostBodyParser({ + contentType: 'application/x-www-form-urlencoded', + parser: async (req, _topic) => { + return { + bodyParsed: req.body.toString('utf-8'), + bodyBuffer: req.body, + encoding: 'utf-8', + }; + }, + }); + }, + }); + }, +}); +``` + +We have the following default parsers: + +- `application/json` + #### Legacy Backend System ```ts diff --git a/plugins/events-backend/report.api.md b/plugins/events-backend/report.api.md index fb9f5fb237..286bc8317d 100644 --- a/plugins/events-backend/report.api.md +++ b/plugins/events-backend/report.api.md @@ -11,6 +11,7 @@ import { EventPublisher } from '@backstage/plugin-events-node'; import { EventsService } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import express from 'express'; +import { HttpBodyParser } from '@backstage/plugin-events-node'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -58,6 +59,9 @@ export class HttpPostIngressEventPublisher { ingresses?: { [topic: string]: Omit; }; + bodyParsers?: { + [contentType: string]: HttpBodyParser; + }; logger: LoggerService; }): HttpPostIngressEventPublisher; } diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index a837e69c32..1b43f0c527 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -24,6 +24,7 @@ import { } from '@backstage/plugin-events-node/alpha'; import { eventsServiceRef, + HttpBodyParserOptions, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; import Router from 'express-promise-router'; @@ -31,7 +32,8 @@ import { HttpPostIngressEventPublisher } from './http'; import { createEventBusRouter } from './hub'; class EventsExtensionPointImpl implements EventsExtensionPoint { - #httpPostIngresses: HttpPostIngressOptions[] = []; + readonly #httpPostIngresses: HttpPostIngressOptions[] = []; + readonly #httpBodyParsers: HttpBodyParserOptions[] = []; setEventBroker(_: any): void { throw new Error( @@ -55,9 +57,17 @@ class EventsExtensionPointImpl implements EventsExtensionPoint { this.#httpPostIngresses.push(options); } + addHttpPostBodyParser(options: HttpBodyParserOptions): void { + this.#httpBodyParsers.push(options); + } + get httpPostIngresses() { return this.#httpPostIngresses; } + + get httpBodyParsers() { + return this.#httpBodyParsers; + } } /** @@ -99,10 +109,18 @@ export const eventsPlugin = createBackendPlugin({ ]), ); + const bodyParsers = Object.fromEntries( + extensionPoint.httpBodyParsers.map(option => [ + option.contentType, + option.parser, + ]), + ); + const http = HttpPostIngressEventPublisher.fromConfig({ config, events, ingresses, + bodyParsers, logger, }); const eventsRouter = Router(); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 16f36d6b0a..84e2ed104c 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -22,6 +22,7 @@ import request from 'supertest'; import { HttpPostIngressEventPublisher } from './HttpPostIngressEventPublisher'; import { mockServices } from '@backstage/backend-test-utils'; import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; +import { HttpBodyParser } from '@backstage/plugin-events-node'; const middleware = MiddlewareFactory.create({ logger: mockServices.logger.mock(), @@ -207,7 +208,8 @@ describe('HttpPostIngressEventPublisher', () => { expect(response.body).toEqual( expect.objectContaining({ error: { - message: 'Unsupported media type: text/plain', + message: + 'Unsupported media type: text/plain. You need to provide a custom body parser for this media type using events extension.', name: 'UnsupportedMediaTypeError', statusCode: 415, }, @@ -217,6 +219,89 @@ describe('HttpPostIngressEventPublisher', () => { ); }); + it('with a text/plain body parser implementation', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + const events = new TestEventsService(); + + const bodyParser: HttpBodyParser = async (req, _topic) => { + return { + bodyParsed: req.body.toString('utf-8'), + bodyBuffer: req.body, + encoding: 'utf-8', + }; + }; + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + events, + bodyParsers: { + 'text/plain': bodyParser, + }, + logger, + }); + publisher.bind(router); + router.use(middleware.error()); + + const response = await request(app) + .post('/http/testA') + .type('text/plain') + .timeout(1000) + .send('Textual information'); + expect(response.status).toBe(202); + }); + + it('with a custom application/json body parser implementation', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + const events = new TestEventsService(); + const customParse = jest.fn(); + + const bodyParser: HttpBodyParser = async (req, _topic) => { + customParse(); + return { + bodyParsed: JSON.parse(req.body.toString('utf-8')), + bodyBuffer: req.body, + encoding: 'utf-8', + }; + }; + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + events, + bodyParsers: { + 'application/json': bodyParser, + }, + logger, + }); + publisher.bind(router); + router.use(middleware.error()); + + const response = await request(app) + .post('/http/testA') + .type('application/json') + .timeout(1000) + .send(JSON.stringify({ testA: 'data' })); + expect(response.status).toBe(202); + expect(customParse).toHaveBeenCalled(); + }); + it('with validator', async () => { const config = new ConfigReader({ events: { diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index 806247e870..4ddcb2a291 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -16,35 +16,18 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; -import { CustomErrorBase } from '@backstage/errors'; import { EventsService, + HttpBodyParser, HttpPostIngressOptions, RequestValidator, } from '@backstage/plugin-events-node'; import contentType from 'content-type'; import express from 'express'; import Router from 'express-promise-router'; +import { defaultHttpBodyParsers } from './body-parser'; import { RequestValidationContextImpl } from './validation'; - -class UnsupportedCharsetError extends CustomErrorBase { - name = 'UnsupportedCharsetError' as const; - statusCode = 415 as const; - - constructor(charset: string) { - super(`Unsupported charset: ${charset}`); - } -} - -class UnsupportedMediaTypeError extends CustomErrorBase { - name = 'UnsupportedMediaTypeError' as const; - statusCode = 415 as const; - - constructor(mediaType?: string) { - super(`Unsupported media type: ${mediaType ?? 'unknown'}`); - } -} - +import { UnsupportedMediaTypeError } from './errors'; /** * Publishes events received from their origin (e.g., webhook events from an SCM system) * via HTTP POST endpoint and passes the request body as event payload to the registered subscribers. @@ -57,6 +40,7 @@ export class HttpPostIngressEventPublisher { config: Config; events: EventsService; ingresses?: { [topic: string]: Omit }; + bodyParsers?: { [contentType: string]: HttpBodyParser }; logger: LoggerService; }): HttpPostIngressEventPublisher { const topics = @@ -71,7 +55,14 @@ export class HttpPostIngressEventPublisher { } }); - return new HttpPostIngressEventPublisher(env.events, env.logger, ingresses); + const parsers = { ...defaultHttpBodyParsers, ...env.bodyParsers }; + + return new HttpPostIngressEventPublisher( + env.events, + env.logger, + ingresses, + parsers, + ); } private constructor( @@ -80,6 +71,9 @@ export class HttpPostIngressEventPublisher { private readonly ingresses: { [topic: string]: Omit; }, + private readonly bodyParsers: { + [contentType: string]: HttpBodyParser; + }, ) {} bind(router: express.Router): void { @@ -108,32 +102,17 @@ export class HttpPostIngressEventPublisher { const logger = this.logger; router.post(path, async (request, response) => { - const requestBody = request.body; - if (!Buffer.isBuffer(requestBody)) { - throw new Error( - `Failed to retrieve raw body from incoming event for topic ${topic}; not a buffer: ${typeof requestBody}`, - ); + const requestContentType = contentType.parse(request); + const bodyParser = this.bodyParsers[requestContentType.type]; + + if (!bodyParser) { + throw new UnsupportedMediaTypeError(requestContentType.type); } - const bodyBuffer: Buffer = requestBody; - const parsedContentType = contentType.parse(request); - if ( - !parsedContentType.type || - parsedContentType.type !== 'application/json' - ) { - throw new UnsupportedMediaTypeError(parsedContentType.type); - } - - const encoding = parsedContentType.parameters.charset ?? 'utf-8'; - if (!Buffer.isEncoding(encoding)) { - throw new UnsupportedCharsetError(encoding); - } - - const bodyString = bodyBuffer.toString(encoding); - const bodyParsed = - parsedContentType.type === 'application/json' - ? JSON.parse(bodyString) - : bodyString; + const { bodyParsed, bodyBuffer, encoding } = await bodyParser( + request, + topic, + ); if (validator) { const requestDetails = { diff --git a/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts b/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts new file mode 100644 index 0000000000..b253d1babf --- /dev/null +++ b/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 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 { HttpBodyParser } from '@backstage/plugin-events-node'; +import contentType from 'content-type'; +import { UnsupportedCharsetError } from '../errors'; + +export const HttpApplicationJsonBodyParser: HttpBodyParser = async ( + request, + topic, +) => { + const requestBody = request.body; + if (!Buffer.isBuffer(requestBody)) { + throw new Error( + `Failed to retrieve raw body from incoming event for topic ${topic}; not a buffer: ${typeof requestBody}`, + ); + } + + const bodyBuffer: Buffer = requestBody; + const parsedContentType = contentType.parse(request); + + const encoding = parsedContentType.parameters.charset ?? 'utf-8'; + if (!Buffer.isEncoding(encoding)) { + throw new UnsupportedCharsetError(encoding); + } + + const bodyString = bodyBuffer.toString(encoding); + const bodyParsed = + parsedContentType.type === 'application/json' + ? JSON.parse(bodyString) + : bodyString; + return { bodyParsed, bodyBuffer, encoding }; +}; diff --git a/plugins/events-backend/src/service/http/body-parser/index.ts b/plugins/events-backend/src/service/http/body-parser/index.ts new file mode 100644 index 0000000000..7fc17ef235 --- /dev/null +++ b/plugins/events-backend/src/service/http/body-parser/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2025 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 { HttpBodyParser } from '@backstage/plugin-events-node'; +import { HttpApplicationJsonBodyParser } from './HttpApplicationJsonBodyParser'; + +export const defaultHttpBodyParsers: { [contentType: string]: HttpBodyParser } = + { + 'application/json': HttpApplicationJsonBodyParser, + }; diff --git a/plugins/events-backend/src/service/http/errors.ts b/plugins/events-backend/src/service/http/errors.ts new file mode 100644 index 0000000000..b078f0d40e --- /dev/null +++ b/plugins/events-backend/src/service/http/errors.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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 { CustomErrorBase } from '@backstage/errors'; + +export class UnsupportedCharsetError extends CustomErrorBase { + name = 'UnsupportedCharsetError' as const; + statusCode = 415 as const; + + constructor(charset: string) { + super(`Unsupported charset: ${charset}`); + } +} + +export class UnsupportedMediaTypeError extends CustomErrorBase { + name = 'UnsupportedMediaTypeError' as const; + statusCode = 415 as const; + + constructor(mediaType?: string) { + super( + `Unsupported media type: ${ + mediaType ?? 'unknown' + }. You need to provide a custom body parser for this media type using events extension.`, + ); + } +} diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 3d859f49a0..068c0d55f9 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -55,7 +55,9 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", + "@types/express": "^4.17.6", "cross-fetch": "^4.0.0", + "express": "^4.17.1", "uri-template": "^2.0.0" }, "devDependencies": { diff --git a/plugins/events-node/report-alpha.api.md b/plugins/events-node/report-alpha.api.md index f61048ac94..ba8b7e9adf 100644 --- a/plugins/events-node/report-alpha.api.md +++ b/plugins/events-node/report-alpha.api.md @@ -7,10 +7,13 @@ import { EventBroker } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { HttpBodyParserOptions } from '@backstage/plugin-events-node'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; // @alpha (undocumented) export interface EventsExtensionPoint { + // (undocumented) + addHttpPostBodyParser(options: HttpBodyParserOptions): void; // (undocumented) addHttpPostIngress(options: HttpPostIngressOptions): void; // @deprecated (undocumented) diff --git a/plugins/events-node/report.api.md b/plugins/events-node/report.api.md index f53bb251ec..d68e321e8f 100644 --- a/plugins/events-node/report.api.md +++ b/plugins/events-node/report.api.md @@ -9,6 +9,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { Request as Request_2 } from 'express'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -111,6 +112,27 @@ export interface EventSubscriber { supportsEventTopics(): string[]; } +// @public (undocumented) +export type HttpBodyParsed = { + bodyParsed: any; + bodyBuffer: Buffer; + encoding: string; +}; + +// @public (undocumented) +export type HttpBodyParser = ( + request: Request_2, + topic: string, +) => Promise; + +// @public (undocumented) +export interface HttpBodyParserOptions { + // (undocumented) + contentType: string; + // (undocumented) + parser: HttpBodyParser; +} + // @public (undocumented) export interface HttpPostIngressOptions { // (undocumented) diff --git a/plugins/events-node/src/api/http/HttpBodyParserOptions.ts b/plugins/events-node/src/api/http/HttpBodyParserOptions.ts new file mode 100644 index 0000000000..cedaecf28e --- /dev/null +++ b/plugins/events-node/src/api/http/HttpBodyParserOptions.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2025 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 { HttpBodyParser } from './body-parser'; + +/** + * @public + */ +export interface HttpBodyParserOptions { + contentType: string; + parser: HttpBodyParser; +} diff --git a/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts b/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts new file mode 100644 index 0000000000..cfd65a6871 --- /dev/null +++ b/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2025 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 { Request } from 'express'; +/** + * @public + */ +export type HttpBodyParsed = { + bodyParsed: any; + bodyBuffer: Buffer; + encoding: string; +}; + +/** + * @public + */ +export type HttpBodyParser = ( + request: Request, + topic: string, +) => Promise; diff --git a/plugins/events-node/src/api/http/body-parser/index.ts b/plugins/events-node/src/api/http/body-parser/index.ts new file mode 100644 index 0000000000..e1376841ae --- /dev/null +++ b/plugins/events-node/src/api/http/body-parser/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './HttpBodyParser'; diff --git a/plugins/events-node/src/api/http/index.ts b/plugins/events-node/src/api/http/index.ts index 9206819a4a..c0908583ad 100644 --- a/plugins/events-node/src/api/http/index.ts +++ b/plugins/events-node/src/api/http/index.ts @@ -15,4 +15,6 @@ */ export type { HttpPostIngressOptions } from './HttpPostIngressOptions'; +export type { HttpBodyParserOptions } from './HttpBodyParserOptions'; export * from './validation'; +export * from './body-parser'; diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts index 90add52563..72be2db51f 100644 --- a/plugins/events-node/src/extensions.ts +++ b/plugins/events-node/src/extensions.ts @@ -19,6 +19,7 @@ import { EventBroker, EventPublisher, EventSubscriber, + HttpBodyParserOptions, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; @@ -46,6 +47,8 @@ export interface EventsExtensionPoint { ): void; addHttpPostIngress(options: HttpPostIngressOptions): void; + + addHttpPostBodyParser(options: HttpBodyParserOptions): void; } /** diff --git a/yarn.lock b/yarn.lock index 8ae7f225e3..8d7ec6b3bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6727,7 +6727,9 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" + "@types/express": ^4.17.6 cross-fetch: ^4.0.0 + express: ^4.17.1 msw: ^1.0.0 uri-template: ^2.0.0 languageName: unknown From c2ff31f64f864ab146f8ca7fd025561c3073cc89 Mon Sep 17 00:00:00 2001 From: Vivek Dwivedi <60487641+vivekvdwivedi@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:00:50 +0530 Subject: [PATCH 009/115] Create facets-cloud.yaml Signed-off-by: Vivek Dwivedi <60487641+vivekvdwivedi@users.noreply.github.com> --- microsite/data/plugins/facets-cloud.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 microsite/data/plugins/facets-cloud.yaml diff --git a/microsite/data/plugins/facets-cloud.yaml b/microsite/data/plugins/facets-cloud.yaml new file mode 100644 index 0000000000..a099aca19b --- /dev/null +++ b/microsite/data/plugins/facets-cloud.yaml @@ -0,0 +1,11 @@ +title: Facets.cloud Platform +author: Facets.cloud +authorUrl: 'https://facets.cloud' +category: Deployment +description: | + Show environments, releases and terraform outputs deployed by Facets.cloud Platform. + Plugin includes an Environment Overview, Release Details and Terraform Output ComponentCard. +documentation: https://github.com/Facets-cloud/facets-backstage-plugin +iconUrl: /img/facets-cloud-logo.png +npmPackageName: '@facets-cloud/backstage-plugin' +addedDate: '2025-02-25' From 6308517c7c9abf7ac0de72809b42fbbd09a820eb Mon Sep 17 00:00:00 2001 From: Vivek Dwivedi <60487641+vivekvdwivedi@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:03:42 +0530 Subject: [PATCH 010/115] Facets.cloud logo Signed-off-by: Vivek Dwivedi <60487641+vivekvdwivedi@users.noreply.github.com> --- microsite/static/img/facets-cloud-logo.png | Bin 0 -> 12087 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 microsite/static/img/facets-cloud-logo.png diff --git a/microsite/static/img/facets-cloud-logo.png b/microsite/static/img/facets-cloud-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fd9c992f52fb154fa7aa787be81062d3af90be98 GIT binary patch literal 12087 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 Date: Wed, 26 Feb 2025 20:16:47 -0300 Subject: [PATCH 011/115] Apply suggestions from code review Co-authored-by: Patrick Jungermann Signed-off-by: Rogerio Angeliski --- .changeset/ten-impalas-hope.md | 4 ++-- plugins/events-backend/src/service/http/errors.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/ten-impalas-hope.md b/.changeset/ten-impalas-hope.md index b770b15c6b..5bbdea7a11 100644 --- a/.changeset/ten-impalas-hope.md +++ b/.changeset/ten-impalas-hope.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-events-backend': minor -'@backstage/plugin-events-node': minor +'@backstage/plugin-events-backend': patch +'@backstage/plugin-events-node': patch --- add `addHttpPostBodyParser` to events extension to allow body parse customization. This feature will enhance flexibility in handling HTTP POST requests in event-related operations. diff --git a/plugins/events-backend/src/service/http/errors.ts b/plugins/events-backend/src/service/http/errors.ts index b078f0d40e..583f1a6b9b 100644 --- a/plugins/events-backend/src/service/http/errors.ts +++ b/plugins/events-backend/src/service/http/errors.ts @@ -32,7 +32,7 @@ export class UnsupportedMediaTypeError extends CustomErrorBase { super( `Unsupported media type: ${ mediaType ?? 'unknown' - }. You need to provide a custom body parser for this media type using events extension.`, + }. You need to provide a custom body parser for this media type using the EventsExtensionPoint.`, ); } } From cf75bca4f3ce4a4943ce346fc94d03555f54c4be Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Wed, 26 Feb 2025 20:18:14 -0300 Subject: [PATCH 012/115] feat(events-backend,events-node): improve parser interface Signed-off-by: Rogerio Angeliski --- .../HttpPostIngressEventPublisher.test.ts | 49 ++++++++++++++++++- .../http/HttpPostIngressEventPublisher.ts | 3 +- .../HttpApplicationJsonBodyParser.ts | 7 ++- plugins/events-node/package.json | 1 + plugins/events-node/report.api.md | 2 + .../api/http/body-parser/HttpBodyParser.ts | 2 + yarn.lock | 1 + 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 84e2ed104c..d0de341a6e 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -209,7 +209,7 @@ describe('HttpPostIngressEventPublisher', () => { expect.objectContaining({ error: { message: - 'Unsupported media type: text/plain. You need to provide a custom body parser for this media type using events extension.', + 'Unsupported media type: text/plain. You need to provide a custom body parser for this media type using the EventsExtensionPoint.', name: 'UnsupportedMediaTypeError', statusCode: 415, }, @@ -259,6 +259,47 @@ describe('HttpPostIngressEventPublisher', () => { expect(response.status).toBe(202); }); + it('with a invalid media type', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + const events = new TestEventsService(); + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + events, + logger, + }); + publisher.bind(router); + router.use(middleware.error()); + + const response = await request(app) + .post('/http/testA') + .type('not-valid-content/plain') + .timeout(1000) + .send('Textual information'); + expect(response.status).toBe(415); + expect(response.body).toEqual( + expect.objectContaining({ + error: { + message: + 'Unsupported media type: not-valid-content/plain. You need to provide a custom body parser for this media type using the EventsExtensionPoint.', + name: 'UnsupportedMediaTypeError', + statusCode: 415, + }, + request: { method: 'POST', url: '/http/testA' }, + response: { statusCode: 415 }, + }), + ); + }); + it('with a custom application/json body parser implementation', async () => { const config = new ConfigReader({ events: { @@ -273,7 +314,11 @@ describe('HttpPostIngressEventPublisher', () => { const events = new TestEventsService(); const customParse = jest.fn(); - const bodyParser: HttpBodyParser = async (req, _topic) => { + const bodyParser: HttpBodyParser = async ( + req, + _parsedMediaType, + _topic, + ) => { customParse(); return { bodyParsed: JSON.parse(req.body.toString('utf-8')), diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index 4ddcb2a291..01d07e64d6 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -103,7 +103,7 @@ export class HttpPostIngressEventPublisher { router.post(path, async (request, response) => { const requestContentType = contentType.parse(request); - const bodyParser = this.bodyParsers[requestContentType.type]; + const bodyParser = this.bodyParsers[requestContentType.type ?? '']; if (!bodyParser) { throw new UnsupportedMediaTypeError(requestContentType.type); @@ -111,6 +111,7 @@ export class HttpPostIngressEventPublisher { const { bodyParsed, bodyBuffer, encoding } = await bodyParser( request, + requestContentType, topic, ); diff --git a/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts b/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts index b253d1babf..fa716a5db0 100644 --- a/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts +++ b/plugins/events-backend/src/service/http/body-parser/HttpApplicationJsonBodyParser.ts @@ -14,11 +14,11 @@ * limitations under the License. */ import { HttpBodyParser } from '@backstage/plugin-events-node'; -import contentType from 'content-type'; import { UnsupportedCharsetError } from '../errors'; export const HttpApplicationJsonBodyParser: HttpBodyParser = async ( request, + parsedMediaType, topic, ) => { const requestBody = request.body; @@ -29,16 +29,15 @@ export const HttpApplicationJsonBodyParser: HttpBodyParser = async ( } const bodyBuffer: Buffer = requestBody; - const parsedContentType = contentType.parse(request); - const encoding = parsedContentType.parameters.charset ?? 'utf-8'; + const encoding = parsedMediaType.parameters.charset ?? 'utf-8'; if (!Buffer.isEncoding(encoding)) { throw new UnsupportedCharsetError(encoding); } const bodyString = bodyBuffer.toString(encoding); const bodyParsed = - parsedContentType.type === 'application/json' + parsedMediaType.type === 'application/json' ? JSON.parse(bodyString) : bodyString; return { bodyParsed, bodyBuffer, encoding }; diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 068c0d55f9..c98ae64499 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -56,6 +56,7 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", + "content-type": "^1.0.5", "cross-fetch": "^4.0.0", "express": "^4.17.1", "uri-template": "^2.0.0" diff --git a/plugins/events-node/report.api.md b/plugins/events-node/report.api.md index d68e321e8f..705083ce16 100644 --- a/plugins/events-node/report.api.md +++ b/plugins/events-node/report.api.md @@ -9,6 +9,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { ParsedMediaType } from 'content-type'; import { Request as Request_2 } from 'express'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; @@ -122,6 +123,7 @@ export type HttpBodyParsed = { // @public (undocumented) export type HttpBodyParser = ( request: Request_2, + parsedMediaType: ParsedMediaType, topic: string, ) => Promise; diff --git a/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts b/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts index cfd65a6871..c0b6a46b95 100644 --- a/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts +++ b/plugins/events-node/src/api/http/body-parser/HttpBodyParser.ts @@ -15,6 +15,7 @@ */ import { Request } from 'express'; +import { ParsedMediaType } from 'content-type'; /** * @public */ @@ -29,5 +30,6 @@ export type HttpBodyParsed = { */ export type HttpBodyParser = ( request: Request, + parsedMediaType: ParsedMediaType, topic: string, ) => Promise; diff --git a/yarn.lock b/yarn.lock index 8d7ec6b3bf..1b4880e8b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6728,6 +6728,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 + content-type: ^1.0.5 cross-fetch: ^4.0.0 express: ^4.17.1 msw: ^1.0.0 From cdd27c7e81f31cd8b52ded0d52379ed9ba1bd3a4 Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Thu, 27 Feb 2025 17:37:02 -0300 Subject: [PATCH 013/115] chore: add missing dep Signed-off-by: Rogerio Angeliski --- plugins/events-node/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index c98ae64499..2a7ff6c01c 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -55,6 +55,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", + "@types/content-type": "^1.1.8", "@types/express": "^4.17.6", "content-type": "^1.0.5", "cross-fetch": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 1b4880e8b1..264bfc572f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6727,6 +6727,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" + "@types/content-type": ^1.1.8 "@types/express": ^4.17.6 content-type: ^1.0.5 cross-fetch: ^4.0.0 From 0a0ced65da520d15e1c42bc62f052a8d8428c9b2 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Mon, 3 Mar 2025 16:23:41 +0100 Subject: [PATCH 014/115] feat(core-components): Avoid Layout Shift for DismissableBanner when using a storageApi with latency (e.g. user-settings-backend) Properly handle the `unknown` state of the storageApi. This may lead to some Layout Shift if the banner has not been dismissed, but once it has been dismissed, you won't have any. Signed-off-by: Gabriel Dugny --- .changeset/serious-chefs-provide.md | 7 ++++++ .../DismissableBanner/DismissableBanner.tsx | 23 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .changeset/serious-chefs-provide.md diff --git a/.changeset/serious-chefs-provide.md b/.changeset/serious-chefs-provide.md new file mode 100644 index 0000000000..fd84577330 --- /dev/null +++ b/.changeset/serious-chefs-provide.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +--- + +Avoid Layout Shift for DismissableBanner when using a storageApi with latency (e.g. user-settings-backend) + +Properly handle the `unknown` state of the storageApi. There's a trade-off: this may lead to some Layout Shift if the banner has not been dismissed, but once it has been dismissed, you won't have any. diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 8c18a1feee..e3e423d166 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -102,11 +102,14 @@ export const DismissableBanner = (props: Props) => { const classes = useStyles(); const storageApi = useApi(storageApiRef); const notificationsStore = storageApi.forBucket('notifications'); - const rawDismissedBanners = - notificationsStore.snapshot('dismissedBanners').value ?? []; + const dismissedBannersSnapshot = + notificationsStore.snapshot('dismissedBanners'); const [dismissedBanners, setDismissedBanners] = useState( - new Set(rawDismissedBanners), + new Set(dismissedBannersSnapshot.value ?? []), + ); + const [loadingSettings, setLoadingSettings] = useState( + dismissedBannersSnapshot.presence === 'unknown', ); const observedItems = useObservable( @@ -114,11 +117,21 @@ export const DismissableBanner = (props: Props) => { ); useEffect(() => { + if (observedItems?.presence === 'unknown' || observedItems === undefined) { + setLoadingSettings(true); + } + if (observedItems?.value) { const currentValue = observedItems?.value ?? []; setDismissedBanners(new Set(currentValue)); } - }, [observedItems?.value]); + if ( + observedItems?.presence === 'absent' || + observedItems?.presence === 'present' + ) { + setLoadingSettings(false); + } + }, [observedItems]); const handleClick = () => { notificationsStore.set('dismissedBanners', [...dismissedBanners, id]); @@ -131,7 +144,7 @@ export const DismissableBanner = (props: Props) => { ? { vertical: 'bottom', horizontal: 'center' } : { vertical: 'top', horizontal: 'center' } } - open={!dismissedBanners.has(id)} + open={!loadingSettings && !dismissedBanners.has(id)} classes={{ root: classNames(classes.root, !fixed && classes.topPosition), }} From b229f0d9de4bdcbe25517340096ed170dc6b9438 Mon Sep 17 00:00:00 2001 From: deepthi-28 Date: Thu, 6 Mar 2025 11:11:17 +0530 Subject: [PATCH 015/115] Proxying doc changed Signed-off-by: deepthi-28 --- docs/plugins/proxying.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index 0e57a9a72a..26329f8475 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -110,13 +110,13 @@ There are also additional settings: from the target. By default, the proxy will only forward safe HTTP request headers to the target. -Those are based on the headers that are considered safe for CORS and includes +These are based on the headers that are considered safe for CORS and include headers like `content-type` or `last-modified`, as well as all headers that are -set by the proxy. If the proxy should forward other headers like +set by the proxy. If the proxy should forward other headers, like `authorization`, this must be enabled by the `allowedHeaders` config, for example `allowedHeaders: ['Authorization']`. This should help to not accidentally forward confidential headers (`cookie`, `X-Auth-Request-User`) to -third-parties. +third parties. The same logic applies to headers that are sent from the target back to the frontend. From 9545af219b4ac1d32fe20afc658ae77f8060e017 Mon Sep 17 00:00:00 2001 From: Fatih Camgoz Date: Thu, 6 Mar 2025 23:29:03 -0500 Subject: [PATCH 016/115] Remove elements from multiple-select type filters in component using X icon on chip Signed-off-by: Fatih Camgoz --- .changeset/metal-crabs-agree.md | 5 +++++ packages/core-components/src/components/Select/Select.tsx | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/metal-crabs-agree.md diff --git a/.changeset/metal-crabs-agree.md b/.changeset/metal-crabs-agree.md new file mode 100644 index 0000000000..c5a2d1102d --- /dev/null +++ b/.changeset/metal-crabs-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Declared CancelIcon explicitly on Chip component inside Select.tsx to disable onMouseDown event by default that creates the flaw of re-opening select component when user tries to remove a selected filter. diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index 6b5174820b..557d66c40f 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -28,6 +28,7 @@ import { withStyles, } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; +import CancelIcon from '@material-ui/icons/Cancel'; import React, { useEffect, useState } from 'react'; import ClosedDropdown from './static/ClosedDropdown'; @@ -225,6 +226,11 @@ export function SelectComponent(props: SelectProps) { key={item?.value} label={item?.label} clickable + deleteIcon={ + event.stopPropagation()} + /> + } onDelete={handleDelete(selectedValue)} className={classes.chip} /> From b03c46fe052aedbc2ea10621f4edb71e86a17d96 Mon Sep 17 00:00:00 2001 From: Lex Felix <112618115+lexGPT@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:09:19 +0100 Subject: [PATCH 017/115] Update .changeset/beige-chairs-suffer.md Co-authored-by: Ben Lambert Signed-off-by: Lex Felix <112618115+lexGPT@users.noreply.github.com> --- .changeset/beige-chairs-suffer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/beige-chairs-suffer.md b/.changeset/beige-chairs-suffer.md index 8989921373..c642b70cbb 100644 --- a/.changeset/beige-chairs-suffer.md +++ b/.changeset/beige-chairs-suffer.md @@ -2,4 +2,4 @@ '@backstage/core-components': minor --- -SimpleStepper back button now works with activeStep property set higher than 0 +`SimpleStepper` back button now works with `activeStep` property set higher than 0 From 44a92b2b2c7f9a5c30940a5495b8765f8e784d6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:51:49 +0000 Subject: [PATCH 018/115] chore(deps): update github/codeql-action action to v3.28.11 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- .github/workflows/sync_snyk-monitor.yml | 2 +- .github/workflows/verify_codeql.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index bc0cff1182..4a5707087e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: sarif_file: results.sarif diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index c08e71cbcb..aec03d09ee 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -58,6 +58,6 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: sarif_file: snyk.sarif diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index ac4c3c7e81..d028be98d8 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -55,7 +55,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 From 3db64ba50f175d6aca36660dfcd1f167b7e9bea7 Mon Sep 17 00:00:00 2001 From: JounQin Date: Wed, 5 Mar 2025 16:27:07 +0800 Subject: [PATCH 019/115] feat(scaffolder): disable the submit button on creating close #29054 Signed-off-by: JounQin --- .changeset/real-turkeys-hunt.md | 6 +++ .../next/components/Stepper/Stepper.test.tsx | 18 +++++-- .../src/next/components/Stepper/Stepper.tsx | 16 ++++-- .../src/next/components/Workflow/Workflow.tsx | 2 +- .../TemplateWizardPage/TemplateWizardPage.tsx | 50 ++++++++++++------- 5 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 .changeset/real-turkeys-hunt.md diff --git a/.changeset/real-turkeys-hunt.md b/.changeset/real-turkeys-hunt.md new file mode 100644 index 0000000000..ba8fe44518 --- /dev/null +++ b/.changeset/real-turkeys-hunt.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-react': patch +--- + +Disable the submit button on creating diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index 5f3dfdfa1d..c1d24ac6c6 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -15,17 +15,16 @@ */ import { renderInTestApp } from '@backstage/test-utils'; import { JsonValue } from '@backstage/types'; +import type { RJSFValidationError } from '@rjsf/utils'; import { act, fireEvent, waitFor } from '@testing-library/react'; import React, { useEffect } from 'react'; +import { FieldExtensionComponentProps } from '../../../extensions'; import { LayoutTemplate } from '../../../layouts'; import { SecretsContextProvider } from '../../../secrets'; import { TemplateParameterSchema } from '../../../types'; import { Stepper } from './Stepper'; -import type { RJSFValidationError } from '@rjsf/utils'; -import { FieldExtensionComponentProps } from '../../../extensions'; - describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { const manifest: TemplateParameterSchema = { @@ -762,7 +761,10 @@ describe('Stepper', () => { ], }; - const onCreate = jest.fn(); + // `onCreate` must be async to mock the submit button disabled behavior + const onCreate = jest.fn( + () => new Promise(resolve => setTimeout(resolve, 0)), + ); const { getByRole } = await renderInTestApp( @@ -783,10 +785,18 @@ describe('Stepper', () => { fireEvent.click(getByRole('button', { name: 'Review' })); }); + fireEvent.click(getByRole('button', { name: 'Create' })); + + await waitFor(() => + expect(getByRole('button', { name: 'Create' })).toBeDisabled(), + ); + await act(async () => { fireEvent.click(getByRole('button', { name: 'Create' })); }); + expect(onCreate).toHaveBeenCalledTimes(1); + expect(onCreate).toHaveBeenCalledWith( expect.objectContaining({ thing: { repoOrg: 'backstage' }, diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 9ca37e284a..7508985a5c 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -115,7 +115,7 @@ export type StepperProps = { */ export const Stepper = (stepperProps: StepperProps) => { const { t } = useTranslationRef(scaffolderReactTranslationRef); - const { layouts = [], components = {}, ...props } = stepperProps; + const { layouts = [], components = {}, onCreate, ...props } = stepperProps; const { ReviewStateComponent = ReviewState, ReviewStepComponent, @@ -220,10 +220,17 @@ export const Stepper = (stepperProps: StepperProps) => { const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); - const handleCreate = useCallback(() => { - props.onCreate(stepsState); + const [isCreating, setIsCreating] = useState(false); + + const handleCreate = useCallback(async () => { + setIsCreating(true); analytics.captureEvent('click', `${createLabel}`); - }, [props, stepsState, analytics, createLabel]); + try { + await onCreate(stepsState); + } finally { + setIsCreating(false); + } + }, [analytics, createLabel, onCreate, stepsState]); return ( <> @@ -318,6 +325,7 @@ export const Stepper = (stepperProps: StepperProps) => { {backLabel}