From 70237a1996d5963ee33ba950a0d556f7cab003c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:15:56 -0400 Subject: [PATCH] fix(topology): nodes can be filtered by label/annotation (#1312) (#1314) Signed-off-by: Andrew Azores Co-authored-by: Thuan Vo (cherry picked from commit 9526cb5c18a16371a1a09e98313ea0d34c47c3b9) Co-authored-by: Andrew Azores --- src/app/Shared/Services/api.types.ts | 10 +++++++++- src/app/Topology/Entity/EntityAnnotations.tsx | 3 ++- src/app/Topology/Shared/utils.tsx | 14 ++++++------- src/app/Topology/Toolbar/TopologyFilters.tsx | 20 +++++++++++++++++-- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 021b84eef..71c2d3b5e 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { RecordingReplace } from '@app/CreateRecording/types'; import { AlertVariant } from '@patternfly/react-core'; +import _ from 'lodash'; import { Observable } from 'rxjs'; export type ApiVersion = 'v1' | 'v2' | 'v2.1' | 'v2.2' | 'v2.3' | 'v2.4' | 'v3' | 'beta'; @@ -28,6 +28,14 @@ export interface KeyValue { value: string; } +export const isKeyValue = (o: any): o is KeyValue => { + return typeof o === 'object' && _.isEqual(new Set(['key', 'value']), new Set(Object.getOwnPropertyNames(o))); +}; + +export const keyValueToString = (kv: KeyValue): string => { + return `${kv.key}=${kv.value}`; +}; + export interface Metadata { labels: KeyValue[]; } diff --git a/src/app/Topology/Entity/EntityAnnotations.tsx b/src/app/Topology/Entity/EntityAnnotations.tsx index 06f715342..0b139ab99 100644 --- a/src/app/Topology/Entity/EntityAnnotations.tsx +++ b/src/app/Topology/Entity/EntityAnnotations.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { keyValueToString } from '@app/Shared/Services/api.types'; import { Label, LabelGroup } from '@patternfly/react-core'; import * as React from 'react'; import { EmptyText } from '../../Shared/Components/EmptyText'; @@ -27,7 +28,7 @@ export const EntityAnnotations: React.FC<{ annotations?: Annotations; maxDisplay return annotations ? Object.keys(annotations).map((groupK) => ({ groupLabel: groupK, - annotations: annotations[groupK].map((kv) => `${kv.key}=${kv.value}`), + annotations: annotations[groupK].map((kv) => keyValueToString(kv)), })) : []; }, [annotations]); diff --git a/src/app/Topology/Shared/utils.tsx b/src/app/Topology/Shared/utils.tsx index 97b25887e..9a8dc9b5f 100644 --- a/src/app/Topology/Shared/utils.tsx +++ b/src/app/Topology/Shared/utils.tsx @@ -17,7 +17,7 @@ import { JmxAuthDescription } from '@app/Shared/Components/JmxAuthDescription'; import { JmxSslDescription } from '@app/Shared/Components/JmxSslDescription'; import { TopologyFilters } from '@app/Shared/Redux/Filters/TopologyFilterSlice'; -import { NodeType, EnvironmentNode, TargetNode } from '@app/Shared/Services/api.types'; +import { NodeType, EnvironmentNode, TargetNode, keyValueToString } from '@app/Shared/Services/api.types'; import { DEFAULT_EMPTY_UNIVERSE, isTargetNode } from '@app/Shared/Services/api.utils'; import { Button, @@ -110,8 +110,7 @@ export const isGroupNodeFiltered = ( matched = matched && filter.Name.includes(groupNode.name); } if (filter.Label && filter.Label.length) { - matched = - matched && Object.entries(groupNode.labels).filter(([k, v]) => filter.Label.includes(`${k}=${v}`)).length > 0; + matched = matched && groupNode.labels.some((kv) => filter.Label.includes(keyValueToString(kv))); } return matched; }; @@ -131,16 +130,15 @@ export const isTargetNodeFiltered = ({ target }: TargetNode, filters?: TopologyF matched = matched && target.jvmId !== undefined && filters.JvmId.includes(target.jvmId); } if (filters.Label && filters.Label.length) { - matched = - matched && Object.entries(target.labels || {}).filter(([k, v]) => filters.Label.includes(`${k}=${v}`)).length > 0; + matched = matched && target.labels.some((kv) => filters.Label.includes(keyValueToString(kv))); } if (filters.Annotation && filters.Annotation.length) { const annotations = target.annotations; matched = matched && - [...Object.entries(annotations?.cryostat || {}), ...Object.entries(annotations?.platform || {})].filter( - ([k, v]) => filters.Annotation.includes(`${k}=${v}`), - ).length > 0; + [...annotations?.cryostat, ...annotations?.platform].some((kv) => + filters.Annotation.includes(keyValueToString(kv)), + ); } return matched; }; diff --git a/src/app/Topology/Toolbar/TopologyFilters.tsx b/src/app/Topology/Toolbar/TopologyFilters.tsx index 055971cfe..564aa70ce 100644 --- a/src/app/Topology/Toolbar/TopologyFilters.tsx +++ b/src/app/Topology/Toolbar/TopologyFilters.tsx @@ -24,7 +24,7 @@ import { topologyUpdateCategoryIntent, topologyUpdateCategoryTypeIntent, } from '@app/Shared/Redux/ReduxStore'; -import { EnvironmentNode, TargetNode } from '@app/Shared/Services/api.types'; +import { EnvironmentNode, TargetNode, isKeyValue, keyValueToString } from '@app/Shared/Services/api.types'; import { flattenTree, getUniqueNodeTypes, isTargetNode } from '@app/Shared/Services/api.utils'; import { getDisplayFieldName } from '@app/utils/utils'; import { @@ -272,7 +272,10 @@ export const TopologyFilter: React.FC<{ isDisabled?: boolean }> = ({ isDisabled, value={{ toString: () => opt, compareTo: (other) => { - const regex = new RegExp(typeof other === 'string' ? other : other.value, 'i'); + const regex = new RegExp( + typeof other === 'string' ? other : isKeyValue(other) ? keyValueToString(other) : `${other}`, + 'i', + ); return regex.test(opt); }, ...{ @@ -349,6 +352,19 @@ export const fieldValueToStrings = (value: unknown): string[] => { } if (typeof value === 'object') { if (Array.isArray(value)) { + if (value.length > 0 && typeof value[0] === 'object') { + if (isKeyValue(value[0])) { + return value.map(keyValueToString); + } else { + return value.map((o) => { + let str = ''; + for (const p in Object.getOwnPropertyNames(o)) { + str += `${p}=${o[p]}`; + } + return str; + }); + } + } return value.map((v) => `${v}`); } else { return Object.entries(value as object).map(([k, v]) => `${k}=${v}`);