From 93533bdd7203ed680cd02a6dd9702dbb5040e745 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 4 Mar 2025 09:53:21 +0100 Subject: [PATCH] refactor: change groups sorting Signed-off-by: Camila Belo --- .changeset/shy-knives-matter.md | 48 ++++++++ packages/app-next/app-config.yaml | 12 +- plugins/catalog/report-alpha.api.md | 14 +-- plugins/catalog/src/alpha/pages.test.tsx | 144 ++++++++++++++--------- plugins/catalog/src/alpha/pages.tsx | 72 ++++++------ 5 files changed, 186 insertions(+), 104 deletions(-) create mode 100644 .changeset/shy-knives-matter.md diff --git a/.changeset/shy-knives-matter.md b/.changeset/shy-knives-matter.md new file mode 100644 index 0000000000..a2cbb104b6 --- /dev/null +++ b/.changeset/shy-knives-matter.md @@ -0,0 +1,48 @@ +--- +'@backstage/plugin-catalog': minor +--- + +The order in which group tabs appear on the entity page has been changed. + +### Before + +Previously, entity contents determined the order in which groups were rendered, so a group was rendered as soon as its first entity content was detected. + +### After + +Groups are now rendered first by default based on their order in the `app-config.yaml` file: + +```diff +app: + extensions: + - page:catalog/entity: ++ config: ++ groups: ++ # this will be the first tab of the default entity page ++ - deployment: ++ title: Deployment ++ # this will be the second tab of the default entiy page ++ - documentation: ++ title: Documentation +``` + +If you wish to place a normal tab before a group, you must add the tab to a group and place the group in the order you wish it to appear on the entity page (groups that contains only one tab are rendered as normal tabs). + +```diff +app: + extensions: + - page:catalog/entity: + config: + groups: ++ # Example placing the overview tab first ++ - overview: ++ title: Overview + - deployment: + title: Deployment + # this will be the second tab of the default entiy page + - documentation: + title: Documentation + - entity-content:catalog/overview: ++ config: ++ group: 'overview' +``` diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 45f11ae1e2..77adc83777 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -16,11 +16,16 @@ app: - page:catalog/entity: config: groups: + # placing a tab at the beginning + - overview: + title: Overview # example disabling a default group - - development: false + # - development: false # example overriding a default group title - documentation: - title: Docs + title: Docs 2 + - deployment: + title: Deployments # example adding a new group - custom: title: Custom @@ -53,8 +58,7 @@ app: # Entity page contents - entity-content:catalog/overview: config: - # associating with a custom group - group: custom + group: overview - entity-content:api-docs/definition - entity-content:api-docs/apis: config: diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 53f1f751ef..19b1260b2d 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -940,10 +940,9 @@ const _default: FrontendPlugin< groups: | Record< string, - | false - | { - title: string; - } + { + title: string; + } >[] | undefined; } & { @@ -953,10 +952,9 @@ const _default: FrontendPlugin< groups?: | Record< string, - | false - | { - title: string; - } + { + title: string; + } >[] | undefined; } & { diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index ac47843e1a..90340f3ebe 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -114,7 +114,6 @@ describe('Index page', () => { params: { defaultPath: '/overview', defaultTitle: 'Overview', - defaultGroup: 'documentation', loader: async () =>
Mock Overview content
, }, }); @@ -254,61 +253,6 @@ describe('Index page', () => { ); }); - it('Should disable a default group', async () => { - const tester = createExtensionTester( - Object.assign({ namespace: 'catalog' }, catalogEntityPage), - { - config: { - groups: [ - { - documentation: false, - }, - ], - }, - }, - ) - .add(techdocsEntityContent) - .add(apidocsEntityContent); - - await renderInTestApp( - - {tester.reactElement()} - , - { - config: { - app: { - title: 'Custom app', - }, - backend: { baseUrl: 'http://localhost:7000' }, - }, - mountedRoutes: { - '/catalog': convertLegacyRouteRef(rootRouteRef), - '/catalog/:namespace/:kind/:name': - convertLegacyRouteRef(entityRouteRef), - }, - }, - ); - - await waitFor(() => - expect( - screen.queryByRole('tab', { name: /Documentation/ }), - ).not.toBeInTheDocument(), - ); - - await waitFor(() => - expect(screen.getByRole('tab', { name: /TechDocs/ })).toBeInTheDocument(), - ); - - await waitFor(() => - expect(screen.getByRole('tab', { name: /ApiDocs/ })).toBeInTheDocument(), - ); - }); - it('Should disassociate a content with a default group', async () => { const tester = createExtensionTester( Object.assign({ namespace: 'catalog' }, catalogEntityPage), @@ -474,4 +418,92 @@ describe('Index page', () => { ).not.toBeInTheDocument(), ); }); + + it('Should render groups first', async () => { + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + ) + .add(techdocsEntityContent) + .add(apidocsEntityContent) + .add(overviewEntityContent); + + await renderInTestApp( + + {tester.reactElement()} + , + { + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }, + ); + + await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2)); + + expect(screen.getAllByRole('tab')[0]).toHaveTextContent('Documentation'); + expect(screen.getAllByRole('tab')[1]).toHaveTextContent('Overview'); + }); + + it('Should render groups on the correct order', async () => { + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + { + config: { + groups: [ + { overview: { title: 'Overview' } }, + { documentation: { title: 'Documentation' } }, + ], + }, + }, + ) + .add(techdocsEntityContent) + .add(apidocsEntityContent) + .add(overviewEntityContent, { + config: { + group: 'overview', + }, + }); + + await renderInTestApp( + + {tester.reactElement()} + , + { + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }, + ); + + await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2)); + + expect(screen.getAllByRole('tab')[0]).toHaveTextContent('Overview'); + expect(screen.getAllByRole('tab')[1]).toHaveTextContent('Documentation'); + }); }); diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 8fd73e1061..205426bd59 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -72,12 +72,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ schema: { groups: z => z - .array( - z.record( - z.string(), - z.literal(false).or(z.object({ title: z.string() })), - ), - ) + .array(z.record(z.string(), z.object({ title: z.string() }))) .optional(), }, }, @@ -88,48 +83,53 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ loader: async () => { const { EntityLayout } = await import('./components/EntityLayout'); - // config groups override default groups - const groups: Record = config.groups?.length - ? config.groups.reduce>((rest, group) => { - const [groupId, groupValue] = Object.entries(group)[0]; - return groupValue - ? { - ...rest, - [groupId]: groupValue.title, - } - : rest; - }, {}) - : defaultEntityContentGroups; + type Groups = Record< + string, + { title: string; items: Array<(typeof inputs.contents)[0]> } + >; - // the groups order is determined by the order of the contents - // a group will appear in the order of the first item that belongs to it - const tabs = inputs.contents.reduce< - Record> - >((rest, output) => { - const itemTitle = output.get(EntityContentBlueprint.dataRefs.title); - const groupId = output.get(EntityContentBlueprint.dataRefs.group); - const groupTitle = groupId && groups[groupId]; - // disabled or invalid groups are ignored - if (!groupTitle) { + let groups = Object.entries(defaultEntityContentGroups).reduce( + (rest, group) => { + const [groupId, groupValue] = group; return { ...rest, - [itemTitle]: [output], + [groupId]: { title: groupValue, items: [] }, }; + }, + {}, + ); + + // config groups override default groups + if (config.groups) { + groups = config.groups.reduce((rest, group) => { + const [groupId, groupValue] = Object.entries(group)[0]; + return { + ...rest, + [groupId]: { title: groupValue.title, items: [] }, + }; + }, {}); + } + + for (const output of inputs.contents) { + const itemId = output.node.spec.id; + const itemTitle = output.get(EntityContentBlueprint.dataRefs.title); + const itemGroup = output.get(EntityContentBlueprint.dataRefs.group); + const group = itemGroup && groups[itemGroup]; + if (!group) { + groups[itemId] = { title: itemTitle, items: [output] }; + continue; } - return { - ...rest, - [groupTitle]: [...(rest[groupTitle] ?? []), output], - }; - }, {}); + group.items.push(output); + } const Component = () => { return ( - {Object.entries(tabs).flatMap(([group, items]) => + {Object.values(groups).flatMap(({ title, items }) => items.map(output => (