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 => (