UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

138 lines (137 loc) 4.96 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 { createSlice } from '@reduxjs/toolkit'; import { JSONPath } from 'jsonpath-plus'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getSavedNamespaces, saveNamespaces } from '../lib/storage'; export const initialState = { namespaces: new Set(getSavedNamespaces()), }; /** * Filters a resource based on the filter state. * * @param item - The item to filter. * @param filter - The filter state. * @param matchCriteria - The JSONPath criteria to match. * * @returns True if the item matches the filter, false otherwise. */ export function filterResource(item, filter, search, matchCriteria) { let matches = true; if (item.metadata.namespace && filter.namespaces.size > 0) { matches = filter.namespaces.has(item.metadata.namespace); } if (!matches) { return false; } if (search) { const filterString = search.toLowerCase(); const usedMatchCriteria = [ item.metadata.uid.toLowerCase(), item.metadata.namespace ? item.metadata.namespace.toLowerCase() : '', item.metadata.name.toLowerCase(), ...Object.keys(item.metadata.labels || {}).map(item => item.toLowerCase()), ...Object.values(item.metadata.labels || {}).map(item => item.toLowerCase()), ]; matches = !!usedMatchCriteria.find(item => item.includes(filterString)); if (matches) { return true; } matches = filterGeneric(item, search, matchCriteria); } return matches; } /** * Filters a generic item based on the filter state. * * The item is considered to match if any of the matchCriteria (described as JSONPath) * matches the filter.search contents. Case matching is insensitive. * * @param item - The item to filter. * @param filter - The filter state. * @param matchCriteria - The JSONPath criteria to match. */ export function filterGeneric(item, search, matchCriteria) { if (!search) { return true; } const filterString = search.toLowerCase(); const usedMatchCriteria = []; // Use the custom matchCriteria if any (matchCriteria || []).forEach(jsonPath => { let values; try { values = JSONPath({ path: '$' + jsonPath, json: item }); } catch (err) { console.debug(`Failed to get value from JSONPath when filtering ${jsonPath} on item ${item}; skipping criteria`); return; } // Include matches values in the criteria values.forEach((value) => { if (typeof value === 'string' || typeof value === 'number') { // Don't use empty string, otherwise it'll match everything if (value !== '') { usedMatchCriteria.push(value.toString().toLowerCase()); } } else if (Array.isArray(value)) { value.forEach((elem) => { if (!!elem && typeof elem === 'string') { usedMatchCriteria.push(elem.toLowerCase()); } }); } }); }); return !!usedMatchCriteria.find(item => item.includes(filterString)); } const filterSlice = createSlice({ name: 'filter', initialState, reducers: { /** * Sets the namespace filter with an array of strings. */ setNamespaceFilter(state, action) { const namespaces = action.payload .map(ns => (typeof ns === 'string' ? ns.trim() : '')) .filter(Boolean); const namespacesSet = new Set(namespaces); state.namespaces = namespacesSet; saveNamespaces([...namespacesSet]); }, /** * Resets the filter state. */ resetFilter(state) { state.namespaces = new Set(); saveNamespaces([]); }, }, }); export const { setNamespaceFilter, resetFilter } = filterSlice.actions; export default filterSlice.reducer; /** * Get globally selected namespaces * * @returns An array of selected namespaces, empty means all namespaces are visible */ export const useNamespaces = () => { const namespacesSet = useSelector(({ filter }) => filter.namespaces); return useMemo(() => [...namespacesSet], [namespacesSet]); };