diff --git a/ui/src/stores/kubeExplorer/allExplorerStore.ts b/ui/src/stores/kubeExplorer/allExplorerStore.ts index e59342e..f85f323 100644 --- a/ui/src/stores/kubeExplorer/allExplorerStore.ts +++ b/ui/src/stores/kubeExplorer/allExplorerStore.ts @@ -13,7 +13,14 @@ import { ResourceWatcher } from "../../common/watchers"; import { useKubeDataStore } from "../kubeDataStore"; import { useKubeWatchStore } from "../kubeWatchStore"; -import { filterObjects, filterResources, filterResourceGroups, isApplicableObjectFilterExpr } from "./filter"; +import { + type Filter, + filterObjects, + filterResources, + filterResourceGroups, + isApplicableObjectFilterExpr, + parseFilterExpr, +} from "./filter"; import { useRelatedExplorerStore } from "./relatedExplorerStore"; interface TreeNode { @@ -46,6 +53,7 @@ export const useAllExplorerStore = defineStore({ } as Tree, selectors: {} as Record>, // { clusterName => { resource { selector }} filterExpr: null as string | null, + filters: [] as Filter[], }, localStorage, { mergeDefaults: true }, @@ -60,14 +68,14 @@ export const useAllExplorerStore = defineStore({ resourceGroups(): (ctx: KubeContext) => KubeResourceGroup[] { return (ctx) => { - return filterResourceGroups(useKubeDataStore().resourceGroups(ctx), this.filterExpr) + return filterResourceGroups(useKubeDataStore().resourceGroups(ctx), this._persistent.filters) .sort((a, b) => a.groupVersion.localeCompare(b.groupVersion)); }; }, resources(): (ctx: KubeContext, group: KubeResourceGroup) => KubeResource[] { return (ctx, group) => { - return filterResources(group.resources || [], this.filterExpr) + return filterResources(group.resources || [], this._persistent.filters) .sort((a, b) => a.name.localeCompare(b.name)); }; }, @@ -76,7 +84,7 @@ export const useAllExplorerStore = defineStore({ return (ctx, res) => { const w = this._watcher(ctx, res); return w - ? filterObjects(w.objects(), this.filterExpr).sort((a, b) => a.name.localeCompare(b.name)) + ? filterObjects(w.objects(), this._persistent.filters).sort((a, b) => a.name.localeCompare(b.name)) : []; }; }, @@ -111,7 +119,7 @@ export const useAllExplorerStore = defineStore({ }, isResourceOpen(state): (ctx: KubeContext, res: KubeResource) => boolean { - return (ctx, res) => (!!this.filterExpr && isApplicableObjectFilterExpr(res, this.filterExpr)) || + return (ctx, res) => isApplicableObjectFilterExpr(res, this.filterExpr, this._persistent.filters) || !!_getOrCreateResourceNode(state._persistent.tree, ctx.name, res.groupVersion, res.kind).open; }, @@ -241,10 +249,12 @@ export const useAllExplorerStore = defineStore({ setFilterExpr(f: string) { this._persistent.filterExpr = f.trim(); + this._persistent.filters = parseFilterExpr(this._persistent.filterExpr); }, clearFilterExpr() { this._persistent.filterExpr = null; + this._persistent.filters = []; }, _initWatcher(ctx: KubeContext, res: KubeResource) { diff --git a/ui/src/stores/kubeExplorer/filter.ts b/ui/src/stores/kubeExplorer/filter.ts index 7a585dd..d7161cb 100644 --- a/ui/src/stores/kubeExplorer/filter.ts +++ b/ui/src/stores/kubeExplorer/filter.ts @@ -1,82 +1,116 @@ import type { KubeObject, KubeResource, KubeResourceGroup } from "../../common/types"; -export function filterObjects(objects: KubeObject[], filterExpr: string | null): KubeObject[] { - const [resFilters, nameFilters] = _parseFilterExpr(filterExpr); - if (objects.length === 0 || resFilters.length === 0 || nameFilters.length === 0) { +export interface Filter { + resources: string[]; + names: string[]; +} + +export function parseFilterExpr(filterExpr: string | null): Filter[] { + const filters: Filter[] = []; + + for (const subExpr of (filterExpr || "").trim().split(";")) { + console.log("parsing filter expr", subExpr); + const [resFilterExpr, nameFilterExpr] = subExpr.trim().split(/\s+/); + + const resFilters = resFilterExpr.toLowerCase().split(",").filter((f) => !!f); + const nameFilters = (nameFilterExpr || "").split(",").filter((f) => !!f); + + if (resFilters.length > 0) { + filters.push({ + resources: resFilters, + names: nameFilters, + }); + } + } + + console.log("parsed filters", filters); + return filters; +} + +export function filterObjects(objects: KubeObject[], filters: Filter[]): KubeObject[] { + if (objects.length === 0 || filters.every((f) => f.resources.length === 0 || f.names.length === 0)) { + console.log("filtering objects - noop"); return objects; } - if (_matchingResources([objects[0].resource], resFilters).length === 0) { - return []; + console.log("filtering objects", objects[0].resource.groupVersion + "/" + objects[0].resource.kind); + + const matched = new Set(); + for (const f of filters) { + console.log("matching object resource filter", f); + if (_matchingResources([objects[0].resource], f.resources).length > 0) { + console.log("matched object resource filter", f.resources); + for (const obj of _matchingObjects(objects, f.names)) { + matched.add(obj.ident); + } + } } - return _matchingObjects(objects, nameFilters); + return objects.filter((obj) => !!matched.has(obj.ident)); } -export function filterResources(resources: KubeResource[], filterExpr: string | null): KubeResource[] { - if (!filterExpr) { +export function filterResources(resources: KubeResource[], filters: Filter[]): KubeResource[] { + console.log("filtering resources"); + if (filters.length === 0) { return resources; } - const [resFilters] = _parseFilterExpr(filterExpr); + const resFilters = filters.flatMap((f) => f.resources); + console.log("matched resources", _matchingResources(resources, resFilters)); return _matchingResources(resources, resFilters); } -export function filterResourceGroups(groups: KubeResourceGroup[], filterExpr: string | null): KubeResourceGroup[] { - if (!filterExpr) { +export function filterResourceGroups(groups: KubeResourceGroup[], filters: Filter[]): KubeResourceGroup[] { + if (filters.length === 0) { return groups; } - const [resFilters] = _parseFilterExpr(filterExpr); + const resFilters = filters.flatMap((f) => f.resources); return groups.filter((group) => { return _matchingResources(group.resources || [], resFilters).length > 0; }); } -export function isApplicableObjectFilterExpr(res: KubeResource, filterExpr: string | null): boolean { +export function isApplicableObjectFilterExpr(res: KubeResource, filterExpr: string | null, filters: Filter[]): boolean { if (!filterExpr) { - return true; - } - - const expr = filterExpr.split(/\s+/)[1]; - if (!expr) { return false; } - if (expr === "*") { - return true; - } - - const hasSlash = expr.indexOf("/") !== -1; - if (!res.namespaced && hasSlash) { - return false; - } - if (res.namespaced && !hasSlash) { - return false; - } - - const [ns, name] = expr.split("/"); - if (ns !== "*" && ns.length < 2) { - return false; - } + for (const f of filters) { + if (_matchingResources([res], f.resources).length === 0) { + continue; + } - if (hasSlash) { - return name === "*" || (name || "").length > 1; + console.log("matched resource filter", f.resources); + for (const expr of f.names) { + if (expr === "*") { + return true; + } + + const hasSlash = expr.indexOf("/") !== -1; + if (!res.namespaced && hasSlash) { + continue; + } + if (res.namespaced && !hasSlash) { + continue; + } + + const [ns, name] = expr.split("/"); + if (ns !== "*" && ns.length < 2) { + continue; + } + + if (hasSlash && (name === "*" || (name || "").length > 1)) { + return true; + } + + if (!hasSlash && (expr === "*" || (expr || "").length > 1)) { + return true; + } + } } - return true; -} - -function _parseFilterExpr(filterExpr: string | null): [string[], string[]] { - const [resFilterExpr, nameFilterExpr] = (filterExpr || "").trim().split(/\s+/); - - const resFilters = resFilterExpr.toLowerCase().split(","); - const nameFilters = (nameFilterExpr || "").split(","); - - return [ - resFilters.filter((f) => !!f), - nameFilters.filter((f) => !!f), - ]; + return false; } function _matchingObjects(objects: KubeObject[], filters: string[]): KubeObject[] { diff --git a/ui/src/stores/kubeExplorer/quickExplorerStore.ts b/ui/src/stores/kubeExplorer/quickExplorerStore.ts index 9bb1302..732becc 100644 --- a/ui/src/stores/kubeExplorer/quickExplorerStore.ts +++ b/ui/src/stores/kubeExplorer/quickExplorerStore.ts @@ -13,7 +13,13 @@ import { ResourceWatcher } from "../../common/watchers"; import { useKubeDataStore } from "../kubeDataStore"; import { useKubeWatchStore } from "../kubeWatchStore"; -import { filterObjects, filterResources, isApplicableObjectFilterExpr } from "./filter"; +import { + type Filter, + filterObjects, + filterResources, + isApplicableObjectFilterExpr, + parseFilterExpr, +} from "./filter"; import { useRelatedExplorerStore } from "./relatedExplorerStore"; interface TreeNode { @@ -92,6 +98,7 @@ export const useQuickExplorerStore = defineStore({ } as Tree, selectors: {} as Record>, // { clusterName => { resource { selector }} filterExpr: null as string | null, + filters: [] as Filter[], }, localStorage, { mergeDefaults: true }, @@ -113,14 +120,14 @@ export const useQuickExplorerStore = defineStore({ groupVersion: quickGroup, resources: groups.flatMap((group) => (group.resources || []).filter((res) => gvkToQuickGroup[res.groupVersion + "/" + res.kind as keyof typeof gvkToQuickGroup] === quickGroup)), })) - .filter((group) => filterResources(group.resources, this.filterExpr).length > 0) + .filter((group) => filterResources(group.resources, this._persistent.filters).length > 0) .sort((a, b) => quickGroupRank[b.groupVersion as keyof typeof quickGroupRank] - quickGroupRank[a.groupVersion as keyof typeof quickGroupRank]); }; }, resources(): (ctx: KubeContext, group: KubeResourceGroup) => KubeResource[] { return (ctx, group) => { - return filterResources((group.resources || []), this.filterExpr) + return filterResources((group.resources || []), this._persistent.filters) .sort((a, b) => a.name.localeCompare(b.name)); }; }, @@ -129,7 +136,7 @@ export const useQuickExplorerStore = defineStore({ return (ctx, res) => { const w = this._watcher(ctx, res); return w - ? filterObjects(w.objects(), this.filterExpr).sort((a, b) => a.name.localeCompare(b.name)) + ? filterObjects(w.objects(), this._persistent.filters).sort((a, b) => a.name.localeCompare(b.name)) : []; }; }, @@ -164,7 +171,7 @@ export const useQuickExplorerStore = defineStore({ }, isResourceOpen(state): (ctx: KubeContext, res: KubeResource) => boolean { - return (ctx, res) => (!!this.filterExpr && isApplicableObjectFilterExpr(res, this.filterExpr)) || + return (ctx, res) => isApplicableObjectFilterExpr(res, this.filterExpr, this._persistent.filters) || !!_getOrCreateResourceNode(state._persistent.tree, ctx.name, res.groupVersion, res.kind).open; }, @@ -294,10 +301,12 @@ export const useQuickExplorerStore = defineStore({ setFilterExpr(f: string) { this._persistent.filterExpr = f.trim(); + this._persistent.filters = parseFilterExpr(this._persistent.filterExpr); }, clearFilterExpr() { this._persistent.filterExpr = null; + this._persistent.filters = []; }, _initWatcher(ctx: KubeContext, res: KubeResource) { diff --git a/ui/src/stores/kubeExplorer/relatedExplorerStore.ts b/ui/src/stores/kubeExplorer/relatedExplorerStore.ts index 8f6b55e..8ddb222 100644 --- a/ui/src/stores/kubeExplorer/relatedExplorerStore.ts +++ b/ui/src/stores/kubeExplorer/relatedExplorerStore.ts @@ -11,7 +11,14 @@ import { RelatedWatcher } from "../../common/watchers"; import { useKubeDataStore } from "../kubeDataStore"; import { useKubeWatchStore } from "../kubeWatchStore"; -import { filterObjects, filterResources, filterResourceGroups, isApplicableObjectFilterExpr } from "./filter"; +import { + type Filter, + filterObjects, + filterResources, + filterResourceGroups, + isApplicableObjectFilterExpr, + parseFilterExpr, +} from "./filter"; interface TreeNode { open?: boolean; @@ -42,6 +49,7 @@ export const useRelatedExplorerStore = defineStore({ } as Tree, filterExpr: null as string | null, + filters: [] as Filter[], }), getters: { @@ -61,7 +69,7 @@ export const useRelatedExplorerStore = defineStore({ return []; } - return filterResourceGroups(useKubeDataStore().resourceGroups(ctx), this.filterExpr) + return filterResourceGroups(useKubeDataStore().resourceGroups(ctx), this.filters) .filter((group) => (group.resources || []).some((res) => this.objects(ctx, res).length > 0)) .sort((a, b) => a.groupVersion.localeCompare(b.groupVersion)); }; @@ -73,7 +81,7 @@ export const useRelatedExplorerStore = defineStore({ return []; } - return filterResources(group.resources || [], this.filterExpr) + return filterResources(group.resources || [], this.filters) .filter((res) => this.objects(ctx, res).length > 0) .sort((a, b) => a.name.localeCompare(b.name)); }; @@ -90,7 +98,7 @@ export const useRelatedExplorerStore = defineStore({ return []; } - return filterObjects(w.objects(), this.filterExpr) + return filterObjects(w.objects(), this.filters) .filter((obj) => obj.resource.groupVersion === res.groupVersion && obj.resource.kind === res.kind) .sort((a, b) => a.name.localeCompare(b.name)); }; @@ -114,7 +122,7 @@ export const useRelatedExplorerStore = defineStore({ }, isResourceOpen(state): (ctx: KubeContext, res: KubeResource) => boolean { - return (ctx, res) => (!!this.filterExpr && isApplicableObjectFilterExpr(res, this.filterExpr)) || + return (ctx, res) => isApplicableObjectFilterExpr(res, this.filterExpr, this.filters) || !!_getOrCreateResourceNode(state.tree, ctx.name, res.groupVersion, res.kind).open; }, @@ -239,10 +247,12 @@ export const useRelatedExplorerStore = defineStore({ setFilterExpr(f: string) { this.filterExpr = f.trim(); + this.filters = parseFilterExpr(this.filterExpr); }, clearFilterExpr() { this.filterExpr = null; + this.filters = []; }, _ensureWatcher(ctx: KubeContext) { diff --git a/ui/src/stores/kubeExplorer/watchedExplorerStore.ts b/ui/src/stores/kubeExplorer/watchedExplorerStore.ts index 03dbd2a..dddb38e 100644 --- a/ui/src/stores/kubeExplorer/watchedExplorerStore.ts +++ b/ui/src/stores/kubeExplorer/watchedExplorerStore.ts @@ -10,7 +10,14 @@ import type { import { useKubeDataStore } from "../kubeDataStore"; import { useKubeWatchStore } from "../kubeWatchStore"; -import { filterObjects, filterResources, filterResourceGroups, isApplicableObjectFilterExpr } from "./filter"; +import { + type Filter, + filterObjects, + filterResources, + filterResourceGroups, + isApplicableObjectFilterExpr, + parseFilterExpr, +} from "./filter"; import { useRelatedExplorerStore } from "./relatedExplorerStore"; interface TreeNode { @@ -38,6 +45,7 @@ export const useWatchedExplorerStore = defineStore({ } as Tree, filterExpr: null as string | null, + filters: [] as Filter[], }), getters: { @@ -52,7 +60,7 @@ export const useWatchedExplorerStore = defineStore({ resourceGroups(): (ctx: KubeContext) => KubeResourceGroup[] { return (ctx: KubeContext) => { - return filterResourceGroups(useKubeDataStore().resourceGroups(ctx), this.filterExpr) + return filterResourceGroups(useKubeDataStore().resourceGroups(ctx), this.filters) .filter((group) => (group.resources || []).some((res) => this.objects(ctx, res).length > 0)) .sort((a, b) => a.groupVersion.localeCompare(b.groupVersion)); }; @@ -60,7 +68,7 @@ export const useWatchedExplorerStore = defineStore({ resources(): (ctx: KubeContext, group: KubeResourceGroup) => KubeResource[] { return (ctx: KubeContext, group: KubeResourceGroup) => { - return filterResources(group.resources || [], this.filterExpr) + return filterResources(group.resources || [], this.filters) .filter((res) => this.objects(ctx, res).length > 0) .sort((a, b) => a.name.localeCompare(b.name)); }; @@ -68,7 +76,7 @@ export const useWatchedExplorerStore = defineStore({ objects() { return (ctx: KubeContext, res: KubeResource) => { - return filterObjects(useKubeWatchStore().objects(ctx, res), this.filterExpr) + return filterObjects(useKubeWatchStore().objects(ctx, res), this.filters) .sort((a, b) => a.name.localeCompare(b.name)); }; }, @@ -91,7 +99,7 @@ export const useWatchedExplorerStore = defineStore({ }, isResourceOpen: (state) => { - return (ctx: KubeContext, res: KubeResource) => (!!state.filterExpr && isApplicableObjectFilterExpr(res, state.filterExpr)) || + return (ctx: KubeContext, res: KubeResource) => isApplicableObjectFilterExpr(res, state.filterExpr, state.filters) || !!_getOrCreateResourceNode(state.tree, ctx.name, res.groupVersion, res.kind).open; }, }, @@ -158,10 +166,12 @@ export const useWatchedExplorerStore = defineStore({ setFilterExpr(f: string) { this.filterExpr = f.trim(); + this.filters = parseFilterExpr(this.filterExpr); }, clearFilterExpr() { this.filterExpr = null; + this.filters = []; }, }, });