UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

474 lines (473 loc) 18 kB
/* * Copyright 2025 The Kubernetes 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 _ from 'lodash'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { isElectron } from '../../helpers/isElectron'; import { useSelectedClusters } from '../../lib/k8s'; import CRD from '../../lib/k8s/crd'; import { createRouteURL } from '../../lib/router/createRouteURL'; import { useTypedSelector } from '../../redux/hooks'; import { DefaultSidebars } from '.'; /** Iterates over every entry in the list, including children */ const forEachEntry = (items, cb) => { items.forEach(it => { cb(it); if (it.subList) { forEachEntry(it.subList, cb); } }); }; const sortSidebarItems = (items) => { const homeItems = items.filter(({ name }) => name === 'home'); const otherItems = items .filter(({ name }) => name !== 'home') .sort((a, b) => { const aLabel = ((a.label ?? a.name) + '').toLowerCase(); const bLabel = ((b.label ?? b.name) + '').toLowerCase(); return aLabel.localeCompare(bLabel); }); return [...homeItems, ...otherItems].map(item => ({ ...item, subList: item.subList ? sortSidebarItems(item.subList) : undefined, })); }; export const useSidebarItems = (sidebarName = DefaultSidebars.IN_CLUSTER) => { const clusters = useTypedSelector(state => state.config.clusters) ?? {}; const settings = useTypedSelector(state => state.config.settings); const customSidebarEntries = useTypedSelector(state => state.sidebar.entries); const customSidebarFilters = useTypedSelector(state => state.sidebar.filters); const shouldShowHomeItem = isElectron() || Object.keys(clusters).length !== 1; const selectedClusters = useSelectedClusters(); const { t } = useTranslation(); const [crds, error] = CRD.useList(); if (error !== null) { console.log(error); } const crdsSidebarEntries = useMemo(() => { const crdsSidebarEntries = []; if (sidebarName !== DefaultSidebars.IN_CLUSTER) { return crdsSidebarEntries; } if (crds === null) { return crdsSidebarEntries; } const entriesGroup = new Map(); crds.forEach(item => { const group = item.jsonData.spec.group; if (!entriesGroup.has(group)) { entriesGroup.set(group, { name: `group-${group}`, label: group, isCR: true, subList: [ { name: item.jsonData.metadata.name, label: item.jsonData.spec.names.kind, isCR: true, }, ], }); } else { const entryGroup = entriesGroup.get(group); if (entryGroup?.subList) { const crdName = item.jsonData.metadata.name; if (!entryGroup.subList.some(subItem => subItem.name === crdName)) { entryGroup.subList.push({ name: crdName, label: item.jsonData.spec.names.kind, isCR: true, }); } } } }); entriesGroup.forEach(item => { crdsSidebarEntries.push(item); }); return sortSidebarItems(crdsSidebarEntries); }, [sidebarName, crds]); const sidebars = useMemo(() => { const homeItems = [ { 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 = [ { name: 'home', icon: 'mdi:home', label: t('translation|Home'), url: '/', hide: !shouldShowHomeItem, }, { name: 'cluster', label: selectedClusters.length ? t('Clusters') : t('glossary|Cluster'), subtitle: selectedClusters.join('\n') || undefined, icon: 'mdi:hexagon-multiple-outline', subList: [ { name: 'namespaces', label: t('glossary|Namespaces'), }, { name: 'nodes', label: t('glossary|Nodes'), }, { name: 'advancedSearch', label: t('Advanced Search (Beta)'), }, ], }, { name: 'map', icon: 'mdi:map', label: t('glossary|Map'), }, { 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: 'endpointslices', label: t('glossary|Endpoint Slices'), }, { name: 'ingresses', label: t('glossary|Ingresses'), }, { name: 'ingressclasses', label: t('glossary|Ingress Classes'), }, { name: 'portforwards', label: t('glossary|Port Forwarding'), hide: !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: 'referencegrants', label: t('glossary|Reference Grants'), }, { name: 'backendtlspolicies', label: t('glossary|BackendTLSPolicies'), }, { name: 'backendtrafficpolicies', label: t('glossary|BackendTrafficPolicies'), }, ], }, { 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'), }, ], }, ]; if (crdsSidebarEntries.length !== 0) { const sublist = [ { name: 'crs', label: t('translation|Instances'), divider: true, }, ]; crdsSidebarEntries.forEach(item => { sublist.push(item); }); inClusterItems.push({ name: 'crds', label: t('glossary|Custom Resources'), icon: 'mdi:puzzle', subList: sublist, }); } else { inClusterItems.push({ name: 'crds', label: t('glossary|Custom Resources'), icon: 'mdi:puzzle', subList: [ { name: 'crs', label: t('translation|Instances'), }, ], }); } // List of sidebars, they act as roots for the sidebar tree const sidebarsList = [ { name: DefaultSidebars.HOME, subList: homeItems, label: '' }, { name: DefaultSidebars.IN_CLUSTER, subList: inClusterItems, label: '' }, ]; // Create a copy of all the custom entries so we don't accidentaly mutate them const customEntries = _.cloneDeep(Object.values(customSidebarEntries)); // Lookup map of every sidebar entry const entryLookup = new Map(); // Put all the entries in the map forEachEntry(sidebarsList, item => entryLookup.set(item.name, item)); forEachEntry(customEntries, item => entryLookup.set(item.name, item)); // Place all custom entries in the tree customEntries.forEach(item => { if (item.parent) { const parentEntry = entryLookup.get(item.parent); if (!parentEntry) { return; } parentEntry.subList ?? (parentEntry.subList = []); parentEntry?.subList?.push(item); } else { const sidebar = item.sidebar ?? DefaultSidebars.IN_CLUSTER; let sidebarEntry = entryLookup.get(sidebar); // Create the sidebar entry if it doesn't exist if (!sidebarEntry) { sidebarEntry = { name: sidebar, subList: [], label: '' }; sidebarsList.push(sidebarEntry); entryLookup.set(sidebar, sidebarEntry); } sidebarEntry.subList?.push(item); } }); const sidebars = Object.fromEntries(sidebarsList.map(item => [item.name, item.subList])); // Filter in-cluster sidebar if (customSidebarFilters.length > 0) { const filterSublist = (item, filter) => { 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(','), selectedClusters.join(','), crdsSidebarEntries, t, ]); const unsortedItems = sidebars[sidebarName === '' ? DefaultSidebars.IN_CLUSTER : sidebarName] ?? []; const sortedItems = useMemo(() => { // Make a deep copy so that we always start from the original (unsorted) order. const itemsCopy = _.cloneDeep(unsortedItems); return settings?.sidebarSortAlphabetically ? sortSidebarItems(itemsCopy) : itemsCopy; }, [unsortedItems, settings.sidebarSortAlphabetically]); return sortedItems; };