diff --git a/frontend/src/components/Sidebar/NavigationTabs.tsx b/frontend/src/components/Sidebar/NavigationTabs.tsx
index b6a8fdd3dc5..f69da3841a8 100644
--- a/frontend/src/components/Sidebar/NavigationTabs.tsx
+++ b/frontend/src/components/Sidebar/NavigationTabs.tsx
@@ -9,7 +9,7 @@ import { getCluster, getClusterPrefixedPath } from '../../lib/util';
import { useTypedSelector } from '../../redux/reducers/reducers';
import Tabs from '../common/Tabs';
import { SidebarItemProps } from '../Sidebar';
-import prepareRoutes from './prepareRoutes';
+import { useSidebarItems } from './useSidebarItems';
function searchNameInSubList(sublist: SidebarItemProps['subList'], name: string): boolean {
if (!sublist) {
@@ -54,7 +54,7 @@ export default function NavigationTabs() {
}
let defaultIndex = null;
- const listItems = prepareRoutes(t, sidebar.selected.sidebar || '');
+ const listItems = useSidebarItems(sidebar.selected.sidebar ?? undefined);
let navigationItem = listItems.find(item => item.name === sidebar.selected.item);
if (!navigationItem) {
const parent = findParentOfSubList(listItems, sidebar.selected.item);
diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx
index afd02395091..d565379da6c 100644
--- a/frontend/src/components/Sidebar/Sidebar.tsx
+++ b/frontend/src/components/Sidebar/Sidebar.tsx
@@ -10,15 +10,14 @@ import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import helpers from '../../helpers';
-import { useCluster } from '../../lib/k8s';
import { createRouteURL } from '../../lib/router';
import { useTypedSelector } from '../../redux/reducers/reducers';
import { ActionButton } from '../common';
import CreateButton from '../common/Resource/CreateButton';
import NavigationTabs from './NavigationTabs';
-import prepareRoutes from './prepareRoutes';
import SidebarItem, { SidebarItemProps } from './SidebarItem';
import { DefaultSidebars, setSidebarSelected, setWhetherSidebarOpen } from './sidebarSlice';
+import { useSidebarItems } from './useSidebarItems';
import VersionButton from './VersionButton';
export const drawerWidth = 240;
@@ -177,8 +176,6 @@ function updateItemSelected(
}
export default function Sidebar() {
- const { t, i18n } = useTranslation(['glossary', 'translation']);
-
const sidebar = useTypedSelector(state => state.sidebar);
const {
isOpen,
@@ -188,24 +185,10 @@ export default function Sidebar() {
isTemporary: isTemporaryDrawer,
} = useSidebarInfo();
const isNarrowOnly = isNarrow && !canExpand;
- const arePluginsLoaded = useTypedSelector(state => state.plugins.loaded);
const namespaces = useTypedSelector(state => state.filter.namespaces);
const dispatch = useDispatch();
- const cluster = useCluster();
- const items = React.useMemo(() => {
- // If the sidebar is null, then it means it should not be visible.
- if (sidebar.selected.sidebar === null) {
- return [];
- }
- return prepareRoutes(t, sidebar.selected.sidebar || '');
- }, [
- cluster,
- sidebar.selected.sidebar,
- sidebar.entries,
- sidebar.filters,
- i18n.language,
- arePluginsLoaded,
- ]);
+
+ const items = useSidebarItems(sidebar?.selected?.sidebar ?? undefined);
const search = namespaces.size !== 0 ? `?namespace=${[...namespaces].join('+')}` : '';
diff --git a/frontend/src/components/Sidebar/prepareRoutes.ts b/frontend/src/components/Sidebar/prepareRoutes.ts
deleted file mode 100644
index 6c2737aad85..00000000000
--- a/frontend/src/components/Sidebar/prepareRoutes.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import _ from 'lodash';
-import helpers from '../../helpers';
-import { createRouteURL } from '../../lib/router';
-import { getCluster } from '../../lib/util';
-import store from '../../redux/stores/store';
-import { DefaultSidebars, SidebarItemProps } from '../Sidebar';
-
-// @todo: We should convert this to a hook so we can update it automatically when
-// needed.
-function prepareRoutes(
- t: (arg: string) => string,
- sidebarToReturn: string = DefaultSidebars.IN_CLUSTER
-) {
- // We do not show the home view if there is only one cluster and we cannot
- // add new clusters, as it is redundant.
- const clusters = store.getState()?.config?.clusters || {};
- const showHome = helpers.isElectron() || Object.keys(clusters).length !== 1;
- const defaultClusterURL = createRouteURL('cluster', { cluster: Object.keys(clusters)[0] });
-
- const homeItems: SidebarItemProps[] = [
- {
- name: 'home',
- icon: showHome ? 'mdi:home' : 'mdi:hexagon-multiple-outline',
- label: showHome ? t('translation|Home') : t('glossary|Cluster'),
- url: showHome ? '/' : defaultClusterURL,
- divider: !showHome,
- },
- {
- name: 'notifications',
- icon: 'mdi:bell',
- label: t('translation|Notifications'),
- url: '/notifications',
- },
- {
- name: 'settings',
- icon: 'mdi:cog',
- label: t('translation|Settings'),
- url: '/settings/general',
- subList: [
- {
- name: 'settingsGeneral',
- label: t('translation|General'),
- url: '/settings/general',
- },
- {
- name: 'plugins',
- label: t('translation|Plugins'),
- url: '/settings/plugins',
- },
- {
- name: 'settingsCluster',
- label: t('glossary|Cluster'),
- url: '/settings/cluster',
- },
- ],
- },
- ];
- const inClusterItems: SidebarItemProps[] = [
- {
- name: 'home',
- icon: 'mdi:home',
- label: t('translation|Home'),
- url: '/',
- divider: true,
- hide: !showHome,
- },
- {
- name: 'cluster',
- label: t('glossary|Cluster'),
- subtitle: getCluster() || undefined,
- icon: 'mdi:hexagon-multiple-outline',
- subList: [
- {
- name: 'namespaces',
- label: t('glossary|Namespaces'),
- },
- {
- name: 'nodes',
- label: t('glossary|Nodes'),
- },
- ],
- },
- {
- name: 'workloads',
- label: t('glossary|Workloads'),
- icon: 'mdi:circle-slice-2',
- subList: [
- {
- name: 'Pods',
- label: t('glossary|Pods'),
- },
- {
- name: 'Deployments',
- label: t('glossary|Deployments'),
- },
- {
- name: 'StatefulSets',
- label: t('glossary|Stateful Sets'),
- },
- {
- name: 'DaemonSets',
- label: t('glossary|Daemon Sets'),
- },
- {
- name: 'ReplicaSets',
- label: t('glossary|Replica Sets'),
- },
- {
- name: 'Jobs',
- label: t('glossary|Jobs'),
- },
- {
- name: 'CronJobs',
- label: t('glossary|CronJobs'),
- },
- ],
- },
- {
- name: 'storage',
- label: t('glossary|Storage'),
- icon: 'mdi:database',
- subList: [
- {
- name: 'persistentVolumeClaims',
- label: t('glossary|Persistent Volume Claims'),
- },
- {
- name: 'persistentVolumes',
- label: t('glossary|Persistent Volumes'),
- },
- {
- name: 'storageClasses',
- label: t('glossary|Storage Classes'),
- },
- ],
- },
- {
- name: 'network',
- label: t('glossary|Network'),
- icon: 'mdi:folder-network-outline',
- subList: [
- {
- name: 'services',
- label: t('glossary|Services'),
- },
- {
- name: 'endpoints',
- label: t('glossary|Endpoints'),
- },
- {
- name: 'ingresses',
- label: t('glossary|Ingresses'),
- },
- {
- name: 'ingressclasses',
- label: t('glossary|Ingress Classes'),
- },
- {
- name: 'portforwards',
- label: t('glossary|Port Forwarding'),
- hide: !helpers.isElectron(),
- },
- {
- name: 'NetworkPolicies',
- label: t('glossary|Network Policies'),
- },
- ],
- },
- {
- name: 'gatewayapi',
- label: t('glossary|Gateway (beta)'),
- icon: 'mdi:lan-connect',
- subList: [
- {
- name: 'gateways',
- label: t('glossary|Gateways'),
- },
- {
- name: 'gatewayclasses',
- label: t('glossary|Gateway Classes'),
- },
- {
- name: 'httproutes',
- label: t('glossary|HTTP Routes'),
- },
- {
- name: 'grpcroutes',
- label: t('glossary|GRPC Routes'),
- },
- ],
- },
- {
- name: 'security',
- label: t('glossary|Security'),
- icon: 'mdi:lock',
- subList: [
- {
- name: 'serviceAccounts',
- label: t('glossary|Service Accounts'),
- },
- {
- name: 'roles',
- label: t('glossary|Roles'),
- },
- {
- name: 'roleBindings',
- label: t('glossary|Role Bindings'),
- },
- ],
- },
- {
- name: 'config',
- label: t('glossary|Configuration'),
- icon: 'mdi:format-list-checks',
- subList: [
- {
- name: 'configMaps',
- label: t('glossary|Config Maps'),
- },
- {
- name: 'secrets',
- label: t('glossary|Secrets'),
- },
- {
- name: 'horizontalPodAutoscalers',
- label: t('glossary|HPAs'),
- },
- {
- name: 'verticalPodAutoscalers',
- label: t('glossary|VPAs'),
- },
- {
- name: 'podDisruptionBudgets',
- label: t('glossary|Pod Disruption Budgets'),
- },
- {
- name: 'resourceQuotas',
- label: t('glossary|Resource Quotas'),
- },
- {
- name: 'limitRanges',
- label: t('glossary|Limit Ranges'),
- },
- {
- name: 'priorityClasses',
- label: t('glossary|Priority Classes'),
- },
- {
- name: 'runtimeClasses',
- label: t('glossary|Runtime Classes'),
- },
- {
- name: 'leases',
- label: t('glossary|Leases'),
- },
- {
- name: 'mutatingWebhookConfigurations',
- label: t('glossary|Mutating Webhook Configurations'),
- },
- {
- name: 'validatingWebhookConfigurations',
- label: t('glossary|Validating Webhook Configurations'),
- },
- ],
- },
- {
- name: 'crds',
- label: t('glossary|Custom Resources'),
- icon: 'mdi:puzzle',
- subList: [
- {
- name: 'crs',
- label: t('translation|Instances'),
- },
- ],
- },
- {
- name: 'map',
- icon: 'mdi:map',
- label: t('glossary|Map (beta)'),
- },
- ];
-
- const sidebars: { [key: string]: SidebarItemProps[] } = {
- [DefaultSidebars.IN_CLUSTER]: _.cloneDeep(inClusterItems),
- [DefaultSidebars.HOME]: _.cloneDeep(homeItems),
- };
-
- const items = store.getState().sidebar.entries;
- const filters = store.getState().sidebar.filters;
-
- for (const i of Object.values(items)) {
- const item = _.cloneDeep(i);
- // For back-compatibility reasons, the default sidebar is the in-cluster one.
- const desiredSidebar = item.sidebar || DefaultSidebars.IN_CLUSTER;
- let itemsSidebar = sidebars[desiredSidebar];
- if (!itemsSidebar) {
- itemsSidebar = [];
- sidebars[desiredSidebar] = itemsSidebar;
- }
-
- const parent = item.parent ? itemsSidebar.find(({ name }) => name === item.parent) : null;
- let placement = itemsSidebar;
- if (parent) {
- if (!parent['subList']) {
- parent['subList'] = [];
- }
-
- placement = parent['subList'];
- }
-
- placement.push(item);
- }
-
- // Filter the routes, if we have any filters.
- // @todo: We need to deprecate this and implement a list processor.
- const filteredRoutes = [];
- const defaultRoutes: SidebarItemProps[] = sidebars[DefaultSidebars.IN_CLUSTER];
- for (const route of defaultRoutes) {
- const routeFiltered =
- !route.hide && filters.length > 0 && filters.filter(f => f(route)).length !== filters.length;
- if (routeFiltered) {
- continue;
- }
-
- const newSubList = route.subList?.filter(
- subRoute =>
- !subRoute.hide &&
- !(filters.length > 0 && filters.filter(f => f(subRoute)).length !== filters.length)
- );
- route.subList = newSubList;
-
- filteredRoutes.push(route);
- }
-
- if (!sidebarToReturn || sidebarToReturn === DefaultSidebars.IN_CLUSTER) {
- return filteredRoutes;
- }
-
- return sidebars[sidebarToReturn];
-}
-
-export default prepareRoutes;
diff --git a/frontend/src/components/Sidebar/useSidebarItems.test.tsx b/frontend/src/components/Sidebar/useSidebarItems.test.tsx
new file mode 100644
index 00000000000..9167da9c8ac
--- /dev/null
+++ b/frontend/src/components/Sidebar/useSidebarItems.test.tsx
@@ -0,0 +1,115 @@
+import { configureStore } from '@reduxjs/toolkit';
+import { renderHook } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import reducers from '../../redux/reducers/reducers';
+import { DefaultSidebars, SidebarEntry } from './sidebarSlice';
+import { useSidebarItems } from './useSidebarItems';
+
+describe('useSidebarItems', () => {
+ const mockStore = (
+ customSidebarEntries: { [name: string]: SidebarEntry },
+ customSidebarFilters: ((entry: SidebarEntry) => SidebarEntry | null)[]
+ ) => {
+ return configureStore({
+ reducer: reducers,
+ preloadedState: {
+ sidebar: {
+ entries: customSidebarEntries,
+ filters: customSidebarFilters,
+ selected: { item: null, sidebar: DefaultSidebars.IN_CLUSTER },
+ isVisible: true,
+ },
+ },
+ });
+ };
+
+ const wrapper =
+ (store: any) =>
+ ({ children }: any) =>
+ {children};
+
+ it('should include customSidebarEntries', () => {
+ const customEntries = {
+ custom1: {
+ name: 'custom1',
+ label: 'Custom 1',
+ url: '/custom1',
+ },
+ custom2: {
+ name: 'custom2',
+ label: 'Custom 2',
+ url: '/custom2',
+ parent: 'custom1',
+ },
+ outoforder: {
+ name: 'outoforder',
+ label: 'outoforder',
+ url: '/outoforder',
+ parent: 'custom3',
+ },
+ custom3: {
+ name: 'custom3',
+ label: 'Custom 3',
+ url: '/custom3',
+ parent: 'custom2',
+ },
+ };
+
+ const store = mockStore(customEntries, []);
+ const { result } = renderHook(() => useSidebarItems(), {
+ wrapper: wrapper(store),
+ });
+
+ expect(result.current.find(it => it.name === 'custom1')).toMatchInlineSnapshot(`
+ {
+ "label": "Custom 1",
+ "name": "custom1",
+ "subList": [
+ {
+ "label": "Custom 2",
+ "name": "custom2",
+ "parent": "custom1",
+ "subList": [
+ {
+ "label": "Custom 3",
+ "name": "custom3",
+ "parent": "custom2",
+ "subList": [
+ {
+ "label": "outoforder",
+ "name": "outoforder",
+ "parent": "custom3",
+ "url": "/outoforder",
+ },
+ ],
+ "url": "/custom3",
+ },
+ ],
+ "url": "/custom2",
+ },
+ ],
+ "url": "/custom1",
+ }
+ `);
+ });
+
+ it('should apply customSidebarFilters', () => {
+ const customEntries = {
+ custom1: { name: 'custom1', label: 'Custom 1', url: '/custom1' },
+ custom2: { name: 'custom2', label: 'Custom 2', url: '/custom2' },
+ };
+ const customFilters = [(entry: SidebarEntry) => (entry.name === 'custom2' ? null : entry)];
+
+ const store = mockStore(customEntries, customFilters);
+ const { result } = renderHook(() => useSidebarItems(), {
+ wrapper: wrapper(store),
+ });
+
+ expect(result.current).toEqual(
+ expect.arrayContaining([{ name: 'custom1', label: 'Custom 1', url: '/custom1' }])
+ );
+ expect(result.current).not.toEqual(
+ expect.arrayContaining([{ name: 'custom2', label: 'Custom 2', url: '/custom2' }])
+ );
+ });
+});
diff --git a/frontend/src/components/Sidebar/useSidebarItems.ts b/frontend/src/components/Sidebar/useSidebarItems.ts
new file mode 100644
index 00000000000..73b345cbc5e
--- /dev/null
+++ b/frontend/src/components/Sidebar/useSidebarItems.ts
@@ -0,0 +1,345 @@
+import _ from 'lodash';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import helpers from '../../helpers';
+import { createRouteURL } from '../../lib/router';
+import { getCluster } from '../../lib/util';
+import { useTypedSelector } from '../../redux/reducers/reducers';
+import { DefaultSidebars, SidebarEntry, SidebarItemProps } from '.';
+
+export const useSidebarItems = (sidebarName: string = DefaultSidebars.IN_CLUSTER) => {
+ const clusters = useTypedSelector(state => state.config.clusters) ?? {};
+ const customSidebarEntries = useTypedSelector(state => state.sidebar.entries);
+ const customSidebarFilters = useTypedSelector(state => state.sidebar.filters);
+ const shouldShowHomeItem = helpers.isElectron() || Object.keys(clusters).length !== 1;
+ const { t } = useTranslation();
+
+ const sidebars = useMemo(() => {
+ const homeItems: SidebarItemProps[] = [
+ {
+ name: 'home',
+ icon: shouldShowHomeItem ? 'mdi:home' : 'mdi:hexagon-multiple-outline',
+ label: shouldShowHomeItem ? t('translation|Home') : t('glossary|Cluster'),
+ url: shouldShowHomeItem
+ ? '/'
+ : createRouteURL('cluster', { cluster: Object.keys(clusters)[0] }),
+ divider: !shouldShowHomeItem,
+ },
+ {
+ name: 'notifications',
+ icon: 'mdi:bell',
+ label: t('translation|Notifications'),
+ url: '/notifications',
+ },
+ {
+ name: 'settings',
+ icon: 'mdi:cog',
+ label: t('translation|Settings'),
+ url: '/settings/general',
+ subList: [
+ {
+ name: 'settingsGeneral',
+ label: t('translation|General'),
+ url: '/settings/general',
+ },
+ {
+ name: 'plugins',
+ label: t('translation|Plugins'),
+ url: '/settings/plugins',
+ },
+ {
+ name: 'settingsCluster',
+ label: t('glossary|Cluster'),
+ url: '/settings/cluster',
+ },
+ ],
+ },
+ ];
+ const inClusterItems: SidebarItemProps[] = [
+ {
+ name: 'home',
+ icon: 'mdi:home',
+ label: t('translation|Home'),
+ url: '/',
+ divider: true,
+ hide: !shouldShowHomeItem,
+ },
+ {
+ name: 'cluster',
+ label: t('glossary|Cluster'),
+ subtitle: getCluster() || undefined,
+ icon: 'mdi:hexagon-multiple-outline',
+ subList: [
+ {
+ name: 'namespaces',
+ label: t('glossary|Namespaces'),
+ },
+ {
+ name: 'nodes',
+ label: t('glossary|Nodes'),
+ },
+ ],
+ },
+ {
+ name: 'workloads',
+ label: t('glossary|Workloads'),
+ icon: 'mdi:circle-slice-2',
+ subList: [
+ {
+ name: 'Pods',
+ label: t('glossary|Pods'),
+ },
+ {
+ name: 'Deployments',
+ label: t('glossary|Deployments'),
+ },
+ {
+ name: 'StatefulSets',
+ label: t('glossary|Stateful Sets'),
+ },
+ {
+ name: 'DaemonSets',
+ label: t('glossary|Daemon Sets'),
+ },
+ {
+ name: 'ReplicaSets',
+ label: t('glossary|Replica Sets'),
+ },
+ {
+ name: 'Jobs',
+ label: t('glossary|Jobs'),
+ },
+ {
+ name: 'CronJobs',
+ label: t('glossary|CronJobs'),
+ },
+ ],
+ },
+ {
+ name: 'storage',
+ label: t('glossary|Storage'),
+ icon: 'mdi:database',
+ subList: [
+ {
+ name: 'persistentVolumeClaims',
+ label: t('glossary|Persistent Volume Claims'),
+ },
+ {
+ name: 'persistentVolumes',
+ label: t('glossary|Persistent Volumes'),
+ },
+ {
+ name: 'storageClasses',
+ label: t('glossary|Storage Classes'),
+ },
+ ],
+ },
+ {
+ name: 'network',
+ label: t('glossary|Network'),
+ icon: 'mdi:folder-network-outline',
+ subList: [
+ {
+ name: 'services',
+ label: t('glossary|Services'),
+ },
+ {
+ name: 'endpoints',
+ label: t('glossary|Endpoints'),
+ },
+ {
+ name: 'ingresses',
+ label: t('glossary|Ingresses'),
+ },
+ {
+ name: 'ingressclasses',
+ label: t('glossary|Ingress Classes'),
+ },
+ {
+ name: 'portforwards',
+ label: t('glossary|Port Forwarding'),
+ hide: !helpers.isElectron(),
+ },
+ {
+ name: 'NetworkPolicies',
+ label: t('glossary|Network Policies'),
+ },
+ ],
+ },
+ {
+ name: 'gatewayapi',
+ label: t('glossary|Gateway (beta)'),
+ icon: 'mdi:lan-connect',
+ subList: [
+ {
+ name: 'gateways',
+ label: t('glossary|Gateways'),
+ },
+ {
+ name: 'gatewayclasses',
+ label: t('glossary|Gateway Classes'),
+ },
+ {
+ name: 'httproutes',
+ label: t('glossary|HTTP Routes'),
+ },
+ {
+ name: 'grpcroutes',
+ label: t('glossary|GRPC Routes'),
+ },
+ ],
+ },
+ {
+ name: 'security',
+ label: t('glossary|Security'),
+ icon: 'mdi:lock',
+ subList: [
+ {
+ name: 'serviceAccounts',
+ label: t('glossary|Service Accounts'),
+ },
+ {
+ name: 'roles',
+ label: t('glossary|Roles'),
+ },
+ {
+ name: 'roleBindings',
+ label: t('glossary|Role Bindings'),
+ },
+ ],
+ },
+ {
+ name: 'config',
+ label: t('glossary|Configuration'),
+ icon: 'mdi:format-list-checks',
+ subList: [
+ {
+ name: 'configMaps',
+ label: t('glossary|Config Maps'),
+ },
+ {
+ name: 'secrets',
+ label: t('glossary|Secrets'),
+ },
+ {
+ name: 'horizontalPodAutoscalers',
+ label: t('glossary|HPAs'),
+ },
+ {
+ name: 'verticalPodAutoscalers',
+ label: t('glossary|VPAs'),
+ },
+ {
+ name: 'podDisruptionBudgets',
+ label: t('glossary|Pod Disruption Budgets'),
+ },
+ {
+ name: 'resourceQuotas',
+ label: t('glossary|Resource Quotas'),
+ },
+ {
+ name: 'limitRanges',
+ label: t('glossary|Limit Ranges'),
+ },
+ {
+ name: 'priorityClasses',
+ label: t('glossary|Priority Classes'),
+ },
+ {
+ name: 'runtimeClasses',
+ label: t('glossary|Runtime Classes'),
+ },
+ {
+ name: 'leases',
+ label: t('glossary|Leases'),
+ },
+ {
+ name: 'mutatingWebhookConfigurations',
+ label: t('glossary|Mutating Webhook Configurations'),
+ },
+ {
+ name: 'validatingWebhookConfigurations',
+ label: t('glossary|Validating Webhook Configurations'),
+ },
+ ],
+ },
+ {
+ name: 'crds',
+ label: t('glossary|Custom Resources'),
+ icon: 'mdi:puzzle',
+ subList: [
+ {
+ name: 'crs',
+ label: t('translation|Instances'),
+ },
+ ],
+ },
+ {
+ name: 'map',
+ icon: 'mdi:map',
+ label: t('glossary|Map (beta)'),
+ },
+ ];
+
+ const sidebars: Record = {
+ [DefaultSidebars.HOME]: homeItems,
+ [DefaultSidebars.IN_CLUSTER]: inClusterItems,
+ };
+
+ // Set of all the entries that need to be added
+ const entriesToAdd = new Set(_.cloneDeep(Object.values(customSidebarEntries)));
+
+ // Takes entry from the set and places it in the appropriate sidebar or a parent item
+ // Recursively looks for child items
+ const placeSidebarEntry = (entry: SidebarItemProps, parentEntry?: SidebarItemProps) => {
+ // Skip entries with parents, they're handled in a separate loop
+ if (entry.parent && !parentEntry) return;
+
+ // Add entry to the sidebar or parent item
+ if (parentEntry) {
+ parentEntry.subList ??= [];
+ parentEntry.subList.push(entry);
+ } else {
+ const entrySidebarName = entry.sidebar ?? DefaultSidebars.IN_CLUSTER;
+ sidebars[entrySidebarName] ??= [];
+ sidebars[entrySidebarName].push(entry);
+ }
+ entriesToAdd.delete(entry);
+
+ // Find and place all child entries
+ entriesToAdd.forEach(maybeChildEntry => {
+ if (maybeChildEntry.parent === entry.name) {
+ placeSidebarEntry(maybeChildEntry, entry);
+ }
+ });
+ };
+
+ entriesToAdd.forEach(entry => placeSidebarEntry(entry));
+
+ if (entriesToAdd.size > 0) {
+ console.error(`Couldn't find where to put some sidebar entries`, entriesToAdd.values());
+ }
+
+ // Filter in-cluster sidebar
+ if (customSidebarFilters.length > 0) {
+ const filterSublist = (item: SidebarItemProps, filter: any) => {
+ if (item.subList) {
+ item.subList = item.subList.filter(it => filter(it));
+ item.subList = item.subList.map(it => filterSublist(it, filter));
+ }
+
+ return item;
+ };
+
+ customSidebarFilters.forEach(customFilter => {
+ sidebars[DefaultSidebars.IN_CLUSTER] = sidebars[DefaultSidebars.IN_CLUSTER]
+ .filter(it => customFilter(it))
+ .map(it => filterSublist(it, customFilter));
+ });
+ }
+
+ return sidebars;
+ }, [customSidebarEntries, shouldShowHomeItem, Object.keys(clusters).join(',')]);
+
+ return sidebars[sidebarName === '' ? DefaultSidebars.IN_CLUSTER : sidebarName] ?? [];
+};