From def696c310a0d916b549d0206d0b203f408638c1 Mon Sep 17 00:00:00 2001 From: Tim Klever Date: Tue, 1 Apr 2025 19:00:21 -0700 Subject: [PATCH 1/5] tests(entityRouteParams): initial test suite Signed-off-by: Tim Klever --- plugins/catalog-react/src/routes.test.ts | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 plugins/catalog-react/src/routes.test.ts diff --git a/plugins/catalog-react/src/routes.test.ts b/plugins/catalog-react/src/routes.test.ts new file mode 100644 index 0000000000..58b8707edd --- /dev/null +++ b/plugins/catalog-react/src/routes.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model'; +import { entityRouteParams } from './routes'; + +const entity: Entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test-Component', + }, +}; + +const expectedEntityRouteParams = { + kind: 'component', + name: 'Test-Component', + namespace: DEFAULT_NAMESPACE, +}; + +const namespacedEntity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test-Namespaced-Component', + namespace: 'Test-Namespace', + }, +}; + +const expectedNamespacedEntityRouteParams = { + kind: 'component', + name: 'Test-Namespaced-Component', + namespace: 'test-namespace', +}; + +describe('entityRouteParams', () => { + it.each([ + ['Entity', entity, expectedEntityRouteParams], + [ + 'namespaced Entity', + namespacedEntity, + expectedNamespacedEntityRouteParams, + ], + ])( + 'should return correct route params for %s', + (_type, entityOrRef, expectedRouteParams) => { + const actualRouteParams = entityRouteParams(entityOrRef); + expect(actualRouteParams).toEqual(expectedRouteParams); + }, + ); +}); From 1f1f8de2c15dbf8dd754bc0dbf0a5db8a1178db0 Mon Sep 17 00:00:00 2001 From: Tim Klever Date: Tue, 1 Apr 2025 19:15:30 -0700 Subject: [PATCH 2/5] feat: loosen `entityRouteParams` to accept references Signed-off-by: Tim Klever --- plugins/catalog-react/src/routes.test.ts | 18 ++++++++++- plugins/catalog-react/src/routes.ts | 41 ++++++++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-react/src/routes.test.ts b/plugins/catalog-react/src/routes.test.ts index 58b8707edd..2b172ae201 100644 --- a/plugins/catalog-react/src/routes.test.ts +++ b/plugins/catalog-react/src/routes.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + Entity, + getCompoundEntityRef, +} from '@backstage/catalog-model'; import { entityRouteParams } from './routes'; const entity: Entity = { @@ -49,11 +53,23 @@ const expectedNamespacedEntityRouteParams = { describe('entityRouteParams', () => { it.each([ ['Entity', entity, expectedEntityRouteParams], + ['ComponentRef', getCompoundEntityRef(entity), expectedEntityRouteParams], + ['string', 'component:Test-Component', expectedEntityRouteParams], [ 'namespaced Entity', namespacedEntity, expectedNamespacedEntityRouteParams, ], + [ + 'namespaced ComponentRef', + getCompoundEntityRef(namespacedEntity), + expectedNamespacedEntityRouteParams, + ], + [ + 'namespaced string', + 'component:Test-Namespace/Test-Namespaced-Component', + expectedNamespacedEntityRouteParams, + ], ])( 'should return correct route params for %s', (_type, entityOrRef, expectedRouteParams) => { diff --git a/plugins/catalog-react/src/routes.ts b/plugins/catalog-react/src/routes.ts index 013bf7b5be..13421fafef 100644 --- a/plugins/catalog-react/src/routes.ts +++ b/plugins/catalog-react/src/routes.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { + Entity, + DEFAULT_NAMESPACE, + CompoundEntityRef, + parseEntityRef, +} from '@backstage/catalog-model'; import { createRouteRef } from '@backstage/core-plugin-api'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; @@ -41,12 +46,34 @@ export const entityRouteRef = getOrCreateGlobalSingleton( * Utility function to get suitable route params for entityRoute, given an * @public */ -export function entityRouteParams(entity: Entity) { +export function entityRouteParams( + entityOrRef: Entity | CompoundEntityRef | string, +) { + let kind; + let namespace; + let name; + + if (typeof entityOrRef === 'string') { + const parsed = parseEntityRef(entityOrRef); + kind = parsed.kind; + namespace = parsed.namespace; + name = parsed.name; + } else if ('metadata' in entityOrRef) { + kind = entityOrRef.kind; + namespace = entityOrRef.metadata.namespace; + name = entityOrRef.metadata.name; + } else { + kind = entityOrRef.kind; + namespace = entityOrRef.namespace; + name = entityOrRef.name; + } + + kind = kind.toLocaleLowerCase('en-US'); + namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE; + return { - kind: entity.kind.toLocaleLowerCase('en-US'), - namespace: - entity.metadata.namespace?.toLocaleLowerCase('en-US') ?? - DEFAULT_NAMESPACE, - name: entity.metadata.name, + kind, + namespace, + name, } as const; } From 1f89bdf951706b883b9da2fe0d76943746d6c9ec Mon Sep 17 00:00:00 2001 From: Tim Klever Date: Tue, 1 Apr 2025 19:31:42 -0700 Subject: [PATCH 3/5] feat(entityRouteParams): optionally allow encoding of route params Signed-off-by: Tim Klever --- plugins/catalog-react/src/index.ts | 1 + plugins/catalog-react/src/routes.test.ts | 23 +++++++++++++++++++++++ plugins/catalog-react/src/routes.ts | 16 ++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 2ea1eb6f53..2b1664abe2 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -27,6 +27,7 @@ export * from './apis'; export * from './components'; export * from './hooks'; export * from './filters'; +export type { EntityRouteParamsOptions } from './routes'; export { entityRouteParams, entityRouteRef } from './routes'; export * from './types'; export * from './overridableComponents'; diff --git a/plugins/catalog-react/src/routes.test.ts b/plugins/catalog-react/src/routes.test.ts index 2b172ae201..6299c0bdba 100644 --- a/plugins/catalog-react/src/routes.test.ts +++ b/plugins/catalog-react/src/routes.test.ts @@ -77,4 +77,27 @@ describe('entityRouteParams', () => { expect(actualRouteParams).toEqual(expectedRouteParams); }, ); + + it('should not encode route params by default', () => { + const actualRouteParams = entityRouteParams( + 'Custom Entity:Test Namespace/Test Component', + ); + expect(actualRouteParams).toEqual({ + kind: 'custom entity', + name: 'Test Component', + namespace: 'test namespace', + }); + }); + + it('should encode route params', () => { + const actualRouteParams = entityRouteParams( + 'Custom Entity:Test Namespace/Test Component', + { encodeParams: true }, + ); + expect(actualRouteParams).toEqual({ + kind: 'custom%20entity', + name: 'Test%20Component', + namespace: 'test%20namespace', + }); + }); }); diff --git a/plugins/catalog-react/src/routes.ts b/plugins/catalog-react/src/routes.ts index 13421fafef..34b9437691 100644 --- a/plugins/catalog-react/src/routes.ts +++ b/plugins/catalog-react/src/routes.ts @@ -42,12 +42,21 @@ export const entityRouteRef = getOrCreateGlobalSingleton( }), ); +/** + * Configurable options for `entityRouteParams` + * @public + */ +export type EntityRouteParamsOptions = { + encodeParams?: boolean; +}; + /** * Utility function to get suitable route params for entityRoute, given an * @public */ export function entityRouteParams( entityOrRef: Entity | CompoundEntityRef | string, + options?: EntityRouteParamsOptions, ) { let kind; let namespace; @@ -71,6 +80,13 @@ export function entityRouteParams( kind = kind.toLocaleLowerCase('en-US'); namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE; + const { encodeParams = false } = options || {}; + if (encodeParams) { + kind = encodeURIComponent(kind); + namespace = encodeURIComponent(namespace); + name = encodeURIComponent(name); + } + return { kind, namespace, From 2fdc9422d308bcd4c7f871a301e26b552fa77904 Mon Sep 17 00:00:00 2001 From: Tim Klever Date: Tue, 1 Apr 2025 19:42:32 -0700 Subject: [PATCH 4/5] refactor: migrate `useEntityRoute` to use enhanced `entityRouteParams` Signed-off-by: Tim Klever --- .../EntityRefLink/EntityRefLink.tsx | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 8952e17f5a..40c738a66e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -14,16 +14,11 @@ * limitations under the License. */ -import { - CompoundEntityRef, - DEFAULT_NAMESPACE, - Entity, - parseEntityRef, -} from '@backstage/catalog-model'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { ReactNode, forwardRef } from 'react'; -import { entityRouteRef } from '../../routes'; +import { entityRouteParams, entityRouteRef } from '../../routes'; import { EntityDisplayName } from '../EntityDisplayName'; /** @@ -88,33 +83,7 @@ function useEntityRoute( ): string { const entityRoute = useRouteRef(entityRouteRef); - let kind; - let namespace; - let name; - - if (typeof entityRef === 'string') { - const parsed = parseEntityRef(entityRef); - kind = parsed.kind; - namespace = parsed.namespace; - name = parsed.name; - } else if ('metadata' in entityRef) { - kind = entityRef.kind; - namespace = entityRef.metadata.namespace; - name = entityRef.metadata.name; - } else { - kind = entityRef.kind; - namespace = entityRef.namespace; - name = entityRef.name; - } - - kind = kind.toLocaleLowerCase('en-US'); - namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE; - - const routeParams = { - kind: encodeURIComponent(kind), - namespace: encodeURIComponent(namespace), - name: encodeURIComponent(name), - }; + const routeParams = entityRouteParams(entityRef, { encodeParams: true }); return entityRoute(routeParams); } From 0f37fa80d2c270a412210049581d1996914882b3 Mon Sep 17 00:00:00 2001 From: Tim Klever Date: Tue, 1 Apr 2025 20:31:42 -0700 Subject: [PATCH 5/5] docs: add changeset and api-report for submission Signed-off-by: Tim Klever --- .changeset/dull-masks-occur.md | 5 +++++ plugins/catalog-react/report.api.md | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/dull-masks-occur.md diff --git a/.changeset/dull-masks-occur.md b/.changeset/dull-masks-occur.md new file mode 100644 index 0000000000..076f4dfef4 --- /dev/null +++ b/.changeset/dull-masks-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +`entityRouteParams` now also accepts entity refs, and can help with encoding the resulting parameters. diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 1e5735bd86..f7673e1bd8 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -524,12 +524,20 @@ export interface EntityRefPresentationSnapshot { } // @public -export function entityRouteParams(entity: Entity): { +export function entityRouteParams( + entityOrRef: Entity | CompoundEntityRef | string, + options?: EntityRouteParamsOptions, +): { readonly kind: string; readonly namespace: string; readonly name: string; }; +// @public +export type EntityRouteParamsOptions = { + encodeParams?: boolean; +}; + // @public export const entityRouteRef: RouteRef<{ name: string;