UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

242 lines (241 loc) 8.95 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { createElement as _createElement } from "react"; /* * 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 { throttle } from 'lodash'; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { deduplicateGraphEdges, deduplicateGraphElements, } from '../graph/graphModel'; const Context = createContext(undefined); export const useSources = () => useContext(Context); /** * Returns a flat list of all the sources */ export function getFlatSources(sources, result = []) { for (const source of sources) { if ('sources' in source) { getFlatSources(source.sources, result); } else { result.push(source); } } return result; } /** * Create Edges from object's ownerReferences */ export const kubeOwnersEdges = (obj) => { return (obj.metadata.ownerReferences?.map(owner => ({ id: `${obj.metadata.uid}-${owner.uid}`, source: obj.metadata.uid, target: owner.uid, })) ?? []); }; /** * Create reverse Edges from object's ownerReferences */ export const kubeOwnersEdgesReversed = (obj) => { return (obj.metadata.ownerReferences?.map(owner => ({ id: `${owner.uid}-${obj.metadata.uid}`, type: 'kubeRelation', source: owner.uid, target: obj.metadata.uid, })) ?? []); }; /** * Create an object from any Kube object */ export const makeKubeObjectNode = (obj) => { const crd = obj.constructor?.customResourceDefinition; if (crd && typeof crd.getMainAPIGroup === 'function') { const [group, , plural] = crd.getMainAPIGroup(); return { id: obj.metadata.uid, kubeObject: obj, customResourceDefinition: plural + '.' + group, }; } return { id: obj.metadata.uid, kubeObject: obj, }; }; /** * Make an edge connecting two Kube objects */ export const makeKubeToKubeEdge = (from, to) => ({ id: `${from.metadata.uid}-${to.metadata.uid}`, source: from.metadata.uid, target: to.metadata.uid, }); /** * Since we can't use hooks in a loop, we need to create a component for each source * that will load the data and pass it to the parent component. */ const SourceLoader = memo(({ useHook, onData, id, }) => { const data = useHook(); useEffect(() => { onData(id, data); // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, data]); return null; }); export default function useThrottledMemo(factory, deps, throttleMs) { const [state, setState] = useState(factory); // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedSetState = useCallback(throttle(setState, throttleMs), []); useEffect(() => { debouncedSetState(factory()); // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); return state; } /** * Loads data from all the sources */ export function GraphSourceManager({ sources, children, relations }) { const [sourceData, setSourceData] = useState(new Map()); const [selectedSources, setSelectedSources] = useState(() => { const _selectedSources = new Set(); const step = (source) => { if (source.isEnabledByDefault ?? true) { _selectedSources.add(source.id); if ('sources' in source) { source.sources.forEach(step); } } }; sources.map(step); return _selectedSources; }); const toggleSelection = useCallback((source) => { setSelectedSources(selection => { const isSelected = (source) => 'sources' in source ? source.sources.every(s => isSelected(s)) : selection.has(source.id); const deselectAll = (source) => { if ('sources' in source) { source.sources.forEach(deselectAll); } else { selection.delete(source.id); } }; const selectAll = (source) => { if ('sources' in source) { source.sources.forEach(s => selectAll(s)); } else { selection.add(source.id); } }; if (!('sources' in source)) { // not a group, just toggle the selection if (selection.has(source.id)) { selection.delete(source.id); } else { selection.add(source.id); } } else { // if all children are selected, deselect them if (source.sources.every(isSelected)) { source.sources.forEach(deselectAll); selection.delete(source.id); } else { source.sources.forEach(selectAll); } } return new Set(selection); }); }, [setSelectedSources]); const onData = useCallback((id, data) => { setSourceData(map => new Map(map).set(id, data)); }, [setSourceData]); const components = useMemo(() => { const allSources = getFlatSources(sources); return allSources .filter(it => selectedSources.has(it.id)) .filter(it => 'useData' in it) .map(source => { return { props: { useHook: source.useData, onData: onData, key: source.id, id: source.id, }, }; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [sources, selectedSources]); const contextValue = useThrottledMemo(() => { let nodes = []; let edges = []; const enabledRelations = relations.filter(relation => { if (relation.toSource) { return selectedSources.has(relation.fromSource) && selectedSources.has(relation.toSource); } return selectedSources.has(relation.fromSource); }); const nodesPerSource = new Map(); const selectedSourceIds = getFlatSources(sources) .filter(source => selectedSources.has(source.id)) .map(source => source.id); selectedSourceIds.forEach(id => { const data = sourceData.get(id); const sourceGraph = deduplicateGraphElements(data?.nodes ?? [], data?.edges ?? []); if (data?.nodes) { nodes = nodes.concat(sourceGraph.nodes); nodesPerSource.set(id, sourceGraph.nodes); } if (data?.edges) { edges = edges.concat(sourceGraph.edges); } }); const sourceGraph = deduplicateGraphElements(nodes, edges); nodes = sourceGraph.nodes; edges = sourceGraph.edges; // Create edges based on Relations enabledRelations.forEach(relation => { const fromNodes = nodesPerSource.get(relation.fromSource) ?? []; const toNodes = relation.toSource ? nodesPerSource.get(relation.toSource) ?? [] : nodes; fromNodes.forEach(from => { toNodes.forEach(to => { if (relation.predicate(from, to)) { edges.push({ id: from.id + '-' + to.id, source: from.id, target: to.id, }); } }); }); }); const isLoading = sourceData.size === 0 || selectedSources?.values()?.some?.(source => sourceData.get(source) === null); return { nodes, edges: deduplicateGraphEdges(edges), toggleSelection, setSelectedSources, selectedSources, sourceData, isLoading, }; }, [sources, selectedSources, sourceData, setSelectedSources, relations], 1000); return (_jsxs(_Fragment, { children: [components.map(it => (_createElement(SourceLoader, { ...it.props, key: it.props.key }))), _jsx(Context.Provider, { value: contextValue, children: children })] })); }