Merge pull request #33004 from backstage/rugvip/fix-entity-tab-group-ordering

catalog: fix entity page tab group ordering
This commit is contained in:
Patrik Oldsberg
2026-02-25 16:55:04 +01:00
committed by GitHub
4 changed files with 114 additions and 15 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Fixed entity page tab groups not respecting the ordering from the `groups` configuration.
+1
View File
@@ -0,0 +1 @@
Fixes entity page tab groups not respecting the ordering from the groups configuration.
@@ -16,8 +16,10 @@
import { screen } from '@testing-library/react';
import { useSelectedSubRoute } from './EntityTabs';
import { EntityTabsList } from './EntityTabsList';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { render } from '@testing-library/react';
import { renderInTestApp } from '@backstage/frontend-test-utils';
function TestSubRouteHook(props: {
subRoutes: Array<{
@@ -37,6 +39,83 @@ function TestSubRouteHook(props: {
);
}
describe('EntityTabsList', () => {
it('should render groups in the order defined by groupDefinitions', () => {
const tabs = [
{ id: '/cicd', label: 'CI/CD', path: 'cicd', group: 'cicd' },
{
id: '/overview',
label: 'Overview',
path: 'overview',
group: 'overview',
},
{
id: '/techdocs',
label: 'TechDocs',
path: 'techdocs',
group: 'techdocs',
},
];
const groupDefinitions = {
overview: { title: 'Overview' },
techdocs: { title: 'TechDocs' },
cicd: { title: 'CI/CD' },
};
renderInTestApp(
<EntityTabsList
tabs={tabs}
groupDefinitions={groupDefinitions}
selectedIndex={0}
/>,
);
const tabElements = screen.getAllByRole('tab');
expect(tabElements).toHaveLength(3);
expect(tabElements[0]).toHaveTextContent('Overview');
expect(tabElements[1]).toHaveTextContent('TechDocs');
expect(tabElements[2]).toHaveTextContent('CI/CD');
});
it('should place ungrouped tabs after defined groups', () => {
const tabs = [
{ id: '/standalone', label: 'Standalone', path: 'standalone' },
{
id: '/overview',
label: 'Overview',
path: 'overview',
group: 'overview',
},
{
id: '/techdocs',
label: 'TechDocs',
path: 'techdocs',
group: 'techdocs',
},
];
const groupDefinitions = {
overview: { title: 'Overview' },
techdocs: { title: 'TechDocs' },
};
renderInTestApp(
<EntityTabsList
tabs={tabs}
groupDefinitions={groupDefinitions}
selectedIndex={0}
/>,
);
const tabElements = screen.getAllByRole('tab');
expect(tabElements).toHaveLength(3);
expect(tabElements[0]).toHaveTextContent('Overview');
expect(tabElements[1]).toHaveTextContent('TechDocs');
expect(tabElements[2]).toHaveTextContent('Standalone');
});
});
describe('EntityTabs', () => {
const subRoutes = [
{
@@ -88,20 +88,34 @@ export function EntityTabsList(props: EntityTabsListProps) {
const { tabs: items, selectedIndex = 0, showIcons, groupDefinitions } = props;
const groups = useMemo(
() =>
items.reduce((result, tab) => {
const group = tab.group ? groupDefinitions[tab.group] : undefined;
const groupOrId = group && tab.group ? tab.group : tab.id;
result[groupOrId] = result[groupOrId] ?? {
group,
items: [],
};
result[groupOrId].items.push(tab);
return result;
}, {} as Record<string, TabGroup>),
[items, groupDefinitions],
);
const groups = useMemo(() => {
const byKey = items.reduce((result, tab) => {
const group = tab.group ? groupDefinitions[tab.group] : undefined;
const groupOrId = group && tab.group ? tab.group : tab.id;
result[groupOrId] = result[groupOrId] ?? {
group,
items: [],
};
result[groupOrId].items.push(tab);
return result;
}, {} as Record<string, TabGroup>);
const groupOrder = Object.keys(groupDefinitions);
return Object.entries(byKey).sort(([a], [b]) => {
const ai = groupOrder.indexOf(a);
const bi = groupOrder.indexOf(b);
if (ai !== -1 && bi !== -1) {
return ai - bi;
}
if (ai !== -1) {
return -1;
}
if (bi !== -1) {
return 1;
}
return 0;
});
}, [items, groupDefinitions]);
const selectedItem = items[selectedIndex];
return (
@@ -115,7 +129,7 @@ export function EntityTabsList(props: EntityTabsListProps) {
aria-label={t('entityTabs.tabsAriaLabel')}
value={selectedItem?.group ?? selectedItem?.id}
>
{Object.entries(groups).map(([id, tabGroup]) => (
{groups.map(([id, tabGroup]) => (
<EntityTabsGroup
data-testid={`header-tab-${id}`}
className={styles.defaultTab}