From d558f41d3a0a75b71d0be3879feeee55a294c668 Mon Sep 17 00:00:00 2001 From: Ankit Luthra Date: Thu, 29 Sep 2022 14:32:50 +0530 Subject: [PATCH 1/7] feat: enhance catalog-plugin table with label column Signed-off-by: Ankit Luthra --- .changeset/fifty-berries-learn.md | 40 ++++++++++++++++++ plugins/catalog/api-report.md | 9 ++++ .../CatalogTable/CatalogTable.test.tsx | 38 +++++++++++++++++ .../src/components/CatalogTable/columns.tsx | 41 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 .changeset/fifty-berries-learn.md diff --git a/.changeset/fifty-berries-learn.md b/.changeset/fifty-berries-learn.md new file mode 100644 index 0000000000..eb95d1febd --- /dev/null +++ b/.changeset/fifty-berries-learn.md @@ -0,0 +1,40 @@ +--- +'@backstage/plugin-catalog': patch +--- + +--- + +This change would allow consumers of plugin catalog to make use of labels just like tags from metadata. + +The intent of change is to show customised columns based on selected label with which entity is associated. +For example: In example below category and visibility are type of labels associated with API entity. +YAML for API entity + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: sample-api + description: API for sample + links: + - url: http://localhost:8080/swagger-ui.html + title: Swagger UI + tags: + - http + labels: + category: legacy + visibility: protected +``` + +Consumers can customise columns to include label column and show in api-docs list + +```typescript +const columns = [ + CatalogTable.columns.createNameColumn({ defaultKind: 'API' }), + CatalogTable.columns.createLabelColumn('category', { title: 'Category' }), + CatalogTable.columns.createLabelColumn('visibility', { + title: 'Visibility', + defaultValue: 'public', + }), +]; +``` diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 377b62a1e6..71006b53b5 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -148,6 +148,15 @@ export const CatalogTable: { } | undefined, ): TableColumn; + createLabelColumn( + key: string, + options?: + | { + title?: string | undefined; + defaultValue?: string | undefined; + } + | undefined, + ): TableColumn; }>; }; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index bd78cecaf0..bb2f302a37 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -331,4 +331,42 @@ describe('CatalogTable component', () => { expect(getByText('Should be rendered')).toBeInTheDocument(); }); + + it('should render the label column with customised title and value as specified', async () => { + const columns = [ + CatalogTable.columns.createNameColumn({ defaultKind: 'API' }), + CatalogTable.columns.createLabelColumn('category', { title: 'Category' }), + ]; + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'APIWithLabel', + labels: { category: 'generic' }, + }, + }; + const expectedColumns = ['Name', 'Category', 'Actions']; + + const { getAllByRole, getByText } = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + expect(columnHeaderLabels).toEqual(expectedColumns); + + const labelCellValue = getByText('generic'); + expect(labelCellValue).toBeInTheDocument(); + }); }); diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 19d160c0e2..68491f2e07 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -25,6 +25,31 @@ import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; import { JsonArray } from '@backstage/types'; +/** + * Renders label if exists in entities or default + * @param labels - Key value pairs of labels specified in entity metadata + * @param key - specified label key to be extracted from all labels + * @param defaultValue - shows default in case label key does not exist + * @returns Chip style component when label value exists undefined otherwise. + */ +const renderLabel = ( + labels: Record | undefined, + key: string, + defaultValue?: string, +) => { + const specifiedLabelValue = (labels && labels[key]) || defaultValue; + return ( + specifiedLabelValue && ( + + ) + ); +}; + // The columnFactories symbol is not directly exported, but through the // CatalogTable.columns field. /** @public */ @@ -162,4 +187,20 @@ export const columnFactories = Object.freeze({ searchable: true, }; }, + createLabelColumn( + key: string, + options?: { title?: string; defaultValue?: string }, + ): TableColumn { + return { + title: options?.title || 'Label', + field: 'entity.metadata.labels', + cellStyle: { + padding: '0px 16px 0px 20px', + }, + render: ({ entity }: { entity: Entity }) => ( + <>{renderLabel(entity.metadata?.labels, key, options?.defaultValue)} + ), + width: 'auto', + }; + }, }); From 273009ba390933c83487ca6597ae7fc96ebaf003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 29 Sep 2022 19:33:40 +0200 Subject: [PATCH 2/7] One more integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-backend/src/integration.test.ts | 144 +++++++++++++++++- .../DefaultCatalogProcessingEngine.ts | 4 +- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-backend/src/integration.test.ts b/plugins/catalog-backend/src/integration.test.ts index 67b63428db..dcfe9a1489 100644 --- a/plugins/catalog-backend/src/integration.test.ts +++ b/plugins/catalog-backend/src/integration.test.ts @@ -34,7 +34,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { DefaultCatalogRulesEnforcer } from './ingestion/CatalogRules'; import { Stitcher } from './stitching/Stitcher'; import { DefaultEntitiesCatalog } from './service/DefaultEntitiesCatalog'; -import { DefaultCatalogProcessingEngine } from './processing/DefaultCatalogProcessingEngine'; +import { + DefaultCatalogProcessingEngine, + ProgressTracker, +} from './processing/DefaultCatalogProcessingEngine'; import { createHash } from 'crypto'; import { DefaultRefreshService } from './service/DefaultRefreshService'; import { connectEntityProviders } from './processing/connectEntityProviders'; @@ -69,10 +72,6 @@ class TestProvider implements EntityProvider { } } -type ProgressTracker = NonNullable< - ConstructorParameters[7] ->; - class ProxyProgressTracker implements ProgressTracker { #inner: ProgressTracker; @@ -540,4 +539,139 @@ describe('Catalog Backend Integration', () => { await expect(harness.getOutputEntities()).resolves.toEqual(outputEntities); }); + + // NOTE(freben): This test documents existing behavior, but it would be more correct to mark the cycle as orphans + it('leaves behind orphaned cycles without orphan markers', async () => { + function mkEntity(name: string) { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name, + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + }; + } + + const harness = await TestHarness.create({ + async processEntity( + entity: Entity, + location: LocationSpec, + emit: CatalogProcessorEmit, + ) { + if (entity.spec?.noEmit) { + return entity; + } + switch (entity.metadata.name) { + case 'a': + emit(processingResult.entity(location, mkEntity('b'))); + break; + case 'b': + emit(processingResult.entity(location, mkEntity('c'))); + break; + case 'c': + emit(processingResult.entity(location, mkEntity('d'))); + break; + case 'd': + emit(processingResult.entity(location, mkEntity('b'))); + break; + default: + } + return entity; + }, + }); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'a', + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + }, + ]); + + await expect(harness.getOutputEntities()).resolves.toEqual({}); + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/a': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'a' }), + }), + 'component:default/b': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'b' }), + }), + 'component:default/c': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'c' }), + }), + 'component:default/d': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'd' }), + }), + }); + // NOTE(freben): Avoid .toHaveProperty here, since it treats dots as path separators + expect( + (await harness.getOutputEntities())['component:default/b'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/c'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/d'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'a', + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + spec: { noEmit: true }, + }, + ]); + + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/a': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'a' }), + }), + 'component:default/b': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'b' }), + }), + 'component:default/c': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'c' }), + }), + 'component:default/d': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'd' }), + }), + }); + // TODO(freben): Ideally these should be orphaned now + expect( + (await harness.getOutputEntities())['component:default/b'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/c'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/d'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + }); }); diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 4693ffc559..2cc0e2dc40 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -31,6 +31,8 @@ import { startTaskPipeline } from './TaskPipeline'; const CACHE_TTL = 5; +export type ProgressTracker = ReturnType; + export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { private stopFunc?: () => void; @@ -45,7 +47,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { unprocessedEntity: Entity; errors: Error[]; }) => Promise | void, - private readonly tracker = progressTracker(), + private readonly tracker: ProgressTracker = progressTracker(), ) {} async start() { From c74b8e2807a78524c334ae0ac7875b8ecdddca23 Mon Sep 17 00:00:00 2001 From: Ankit Luthra Date: Fri, 30 Sep 2022 18:13:53 +0530 Subject: [PATCH 3/7] chore: rephrase changeset and inline render logic for feat: enhance catalog-plugin with label column Signed-off-by: Ankit Luthra --- .changeset/fifty-berries-learn.md | 9 ++-- .../src/components/CatalogTable/columns.tsx | 46 ++++++++----------- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/.changeset/fifty-berries-learn.md b/.changeset/fifty-berries-learn.md index eb95d1febd..a05d5f6078 100644 --- a/.changeset/fifty-berries-learn.md +++ b/.changeset/fifty-berries-learn.md @@ -2,13 +2,10 @@ '@backstage/plugin-catalog': patch --- ---- +Added new column `Label` to `CatalogTable.columns`, this new column allows you make use of labels from metadata. +For example: category and visibility are type of labels associated with API entity illustrated below. -This change would allow consumers of plugin catalog to make use of labels just like tags from metadata. - -The intent of change is to show customised columns based on selected label with which entity is associated. -For example: In example below category and visibility are type of labels associated with API entity. -YAML for API entity +YAML code snippet for API entity ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 68491f2e07..7f3cd5ef82 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -25,31 +25,6 @@ import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; import { JsonArray } from '@backstage/types'; -/** - * Renders label if exists in entities or default - * @param labels - Key value pairs of labels specified in entity metadata - * @param key - specified label key to be extracted from all labels - * @param defaultValue - shows default in case label key does not exist - * @returns Chip style component when label value exists undefined otherwise. - */ -const renderLabel = ( - labels: Record | undefined, - key: string, - defaultValue?: string, -) => { - const specifiedLabelValue = (labels && labels[key]) || defaultValue; - return ( - specifiedLabelValue && ( - - ) - ); -}; - // The columnFactories symbol is not directly exported, but through the // CatalogTable.columns field. /** @public */ @@ -197,9 +172,24 @@ export const columnFactories = Object.freeze({ cellStyle: { padding: '0px 16px 0px 20px', }, - render: ({ entity }: { entity: Entity }) => ( - <>{renderLabel(entity.metadata?.labels, key, options?.defaultValue)} - ), + render: ({ entity }: { entity: Entity }) => { + const labels: Record | undefined = + entity.metadata?.labels; + const specifiedLabelValue = + (labels && labels[key]) || options?.defaultValue; + return ( + <> + {specifiedLabelValue && ( + + )} + + ); + }, width: 'auto', }; }, From b36d4a5189066dd28b4a234b366d3b4eed54795a Mon Sep 17 00:00:00 2001 From: Pascal Wallenius Date: Fri, 30 Sep 2022 11:59:23 +0200 Subject: [PATCH 4/7] add link to questions tagged 'backstage' in Stack Overflow Signed-off-by: Pascal Wallenius --- docs/overview/support.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/overview/support.md b/docs/overview/support.md index 5122a1c9ff..662a37c6ae 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -6,6 +6,7 @@ description: Support and Community Details and Links - [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project. +- [Stack Overflow](https://stackoverflow.com/questions/tagged/backstage) - Browse or ask questions on Stack Overflow. - [Good First Issues](https://github.com/backstage/backstage/contribute) - Start here if you want to contribute. - [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the From 94dbaf364590ce6c9b804ee1fdbf13fb65ffb951 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 1 Oct 2022 20:33:06 -0700 Subject: [PATCH 5/7] Document host attribute for github catalog provider. This was added in https://github.com/backstage/backstage/pull/13400 but isn't documented yet. Signed-off-by: Bradley Grainger --- docs/integrations/github/discovery.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 6966753d68..6acb87ac37 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -96,6 +96,10 @@ catalog: topic: include: ['backstage-include'] # optional array of strings exclude: ['experiments'] # optional array of strings + enterpriseProviderId: + host: ghe.example.net + organization: 'backstage' # string + catalogPath: '/catalog-info.yaml' # string ``` This provider supports multiple organizations via unique provider IDs. @@ -125,6 +129,8 @@ This provider supports multiple organizations via unique provider IDs. - **organization**: Name of your organization account/workspace. If you want to add multiple organizations, you need to add one provider config each. +- **host** _(optional)_: + The hostname of your GitHub Enterprise instance. It must match a host defined in [integrations.github](locations.md). ## GitHub API Rate Limits From a948b6af07d45b304eb39af6e81582a5f965e848 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 3 Oct 2022 10:14:05 +0200 Subject: [PATCH 6/7] ci: skip publishing upgrade-helper diff for next releases Signed-off-by: Vincenzo Scamporlino --- .github/workflows/sync_release-manifest.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 8cf6991aaf..c6d00ecd28 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -53,6 +53,10 @@ jobs: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} # TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while script: | + const releaseVersion = require('./backstage/package.json').version; + if(releaseVersion.includes('next')) { + return; + } console.log('Dispatching upgrade helper sync'); await github.rest.actions.createWorkflowDispatch({ owner: 'backstage', @@ -61,6 +65,6 @@ jobs: ref: 'master', inputs: { version: require('./backstage/packages/create-app/package.json').version, - releaseVersion: require('./backstage/package.json').version + releaseVersion }, }); From 3f3ac6222624952bbafce0647610261ae96ed400 Mon Sep 17 00:00:00 2001 From: Ankit Luthra Date: Mon, 3 Oct 2022 14:39:19 +0530 Subject: [PATCH 7/7] chore: change upgrade version as minor in changeset for feat: enhance catalog-plugin with label column Signed-off-by: Ankit Luthra --- .changeset/fifty-berries-learn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fifty-berries-learn.md b/.changeset/fifty-berries-learn.md index a05d5f6078..647246a482 100644 --- a/.changeset/fifty-berries-learn.md +++ b/.changeset/fifty-berries-learn.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog': minor --- Added new column `Label` to `CatalogTable.columns`, this new column allows you make use of labels from metadata.