Merge pull request #29445 from tklever/entity-route-params-refactor

refactor: consolidate entity route param extraction in catalog-react
This commit is contained in:
Fredrik Adelöw
2025-04-15 11:20:42 +02:00
committed by GitHub
6 changed files with 171 additions and 42 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': minor
---
`entityRouteParams` now also accepts entity refs, and can help with encoding the resulting parameters.
+9 -1
View File
@@ -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;
@@ -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);
}
+1
View File
@@ -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';
+103
View File
@@ -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',
});
});
});
+50 -7
View File
@@ -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;
}