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; 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); } 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 new file mode 100644 index 0000000000..6299c0bdba --- /dev/null +++ b/plugins/catalog-react/src/routes.test.ts @@ -0,0 +1,103 @@ +/* + * 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, + getCompoundEntityRef, +} 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], + ['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) => { + const actualRouteParams = entityRouteParams(entityOrRef); + 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 013bf7b5be..34b9437691 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'; @@ -37,16 +42,54 @@ 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(entity: Entity) { +export function entityRouteParams( + entityOrRef: Entity | CompoundEntityRef | string, + options?: EntityRouteParamsOptions, +) { + 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; + + const { encodeParams = false } = options || {}; + if (encodeParams) { + kind = encodeURIComponent(kind); + namespace = encodeURIComponent(namespace); + name = encodeURIComponent(name); + } + 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; }