From aca6a6c3060fc0c04eab1045ecc3df53b2adc8df Mon Sep 17 00:00:00 2001 From: karthik Date: Thu, 18 Nov 2021 14:32:26 +0530 Subject: [PATCH 1/3] Add filter by label in topology --- .../topology/components/HelmReleaseGroup.tsx | 3 +- .../topology/components/HelmReleaseNode.tsx | 3 +- .../components/groups/KnativeServiceGroup.tsx | 2 +- .../components/groups/KnativeServiceNode.tsx | 3 +- .../topology/components/nodes/EventSource.tsx | 4 +- .../components/nodes/EventingPubSubNode.tsx | 4 +- .../src/topology/components/nodes/VmNode.tsx | 2 +- .../topology/locales/en/topology.json | 3 + .../graph-view/components/nodes/BaseNode.tsx | 2 +- .../nodes/trapezoidNode/TrapezoidBaseNode.tsx | 2 +- .../src/components/page/TopologyView.tsx | 13 ++- .../src/filters/NameLabelFilterDropdown.tsx | 82 +++++++++++++++++++ .../src/filters/TopologyFilterBar.tsx | 79 ++++++++++++++---- .../filters/__tests__/useSearchFilter.spec.ts | 36 +++++++- .../topology/src/filters/filter-utils.ts | 25 +++++- .../topology/src/filters/useSearchFilter.ts | 19 +++-- .../components/OperatorBackedServiceGroup.tsx | 4 +- .../components/OperatorBackedServiceNode.tsx | 3 +- 18 files changed, 248 insertions(+), 41 deletions(-) create mode 100644 frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx diff --git a/frontend/packages/helm-plugin/src/topology/components/HelmReleaseGroup.tsx b/frontend/packages/helm-plugin/src/topology/components/HelmReleaseGroup.tsx index 12df9286f2c..2ce025b01c0 100644 --- a/frontend/packages/helm-plugin/src/topology/components/HelmReleaseGroup.tsx +++ b/frontend/packages/helm-plugin/src/topology/components/HelmReleaseGroup.tsx @@ -25,6 +25,7 @@ import { useSearchFilter, SHOW_LABELS_FILTER_ID, } from '@console/topology/src/filters'; +import { getResource } from '@console/topology/src/utils'; type HelmReleaseGroupProps = { element: Node; @@ -47,7 +48,7 @@ const HelmReleaseGroup: React.FC = ({ const [{ dragging }, dragNodeRef] = useDragNode(noRegroupDragSourceSpec); const [{ dragging: labelDragging }, dragLabelRef] = useDragNode(noRegroupDragSourceSpec); const nodeRefs = useCombineRefs(innerHoverRef, dragNodeRef); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), getResource(element)?.metadata?.labels); const displayFilters = useDisplayFilters(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover || innerHover; diff --git a/frontend/packages/helm-plugin/src/topology/components/HelmReleaseNode.tsx b/frontend/packages/helm-plugin/src/topology/components/HelmReleaseNode.tsx index 5497a12de76..8770a776e68 100644 --- a/frontend/packages/helm-plugin/src/topology/components/HelmReleaseNode.tsx +++ b/frontend/packages/helm-plugin/src/topology/components/HelmReleaseNode.tsx @@ -22,6 +22,7 @@ import { noRegroupDragSourceSpec, } from '@console/topology/src/components/graph-view'; import { useSearchFilter } from '@console/topology/src/filters/useSearchFilter'; +import { getResource } from '@console/topology/src/utils'; type HelmReleaseNodeProps = { element: Node; @@ -42,7 +43,7 @@ const HelmReleaseNode: React.FC = ({ const [hover, hoverRef] = useHover(); const [{ dragging }, dragNodeRef] = useDragNode(noRegroupDragSourceSpec); const refs = useCombineRefs(dragNodeRef, dndDropRef, hoverRef); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), getResource(element)?.metadata?.labels); const { groupResources } = element.getData(); const [groupSize, groupRef] = useSize([groupResources]); const width = groupSize ? groupSize.width : 0; diff --git a/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceGroup.tsx b/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceGroup.tsx index bf9db6fe75c..576d51bbfc6 100644 --- a/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceGroup.tsx +++ b/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceGroup.tsx @@ -93,7 +93,7 @@ const KnativeServiceGroup: React.FC = ({ ); useAnchor(React.useCallback((node: Node) => new RectAnchor(node, 1.5 + EVENT_MARKER_RADIUS), [])); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), getResource(element)?.metadata?.labels); const displayFilters = useDisplayFilters(); const allowEdgeCreation = useAllowEdgeCreation(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); diff --git a/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceNode.tsx b/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceNode.tsx index 319ba6a92a1..926831ade11 100644 --- a/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceNode.tsx +++ b/frontend/packages/knative-plugin/src/topology/components/groups/KnativeServiceNode.tsx @@ -24,6 +24,7 @@ import { GroupNodeAnchor, } from '@console/topology/src/components/graph-view'; import { useSearchFilter, useAllowEdgeCreation } from '@console/topology/src/filters'; +import { getResource } from '@console/topology/src/utils'; import { TYPE_KNATIVE_SERVICE, EVENT_MARKER_RADIUS } from '../../const'; type KnativeServiceNodeProps = { @@ -60,7 +61,7 @@ const KnativeServiceNode: React.FC = ({ const dragProps = React.useMemo(() => ({ element }), [element]); const [{ dragging }, dragNodeRef] = useDragNode(dragSpec, dragProps); const refs = useCombineRefs(hoverRef, dragNodeRef); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), getResource(element)?.metadata?.labels); const allowEdgeCreation = useAllowEdgeCreation(); const { kind } = element.getData().data; const { groupResources } = element.getData(); diff --git a/frontend/packages/knative-plugin/src/topology/components/nodes/EventSource.tsx b/frontend/packages/knative-plugin/src/topology/components/nodes/EventSource.tsx index 14f54863571..870c3377db9 100644 --- a/frontend/packages/knative-plugin/src/topology/components/nodes/EventSource.tsx +++ b/frontend/packages/knative-plugin/src/topology/components/nodes/EventSource.tsx @@ -58,13 +58,13 @@ const EventSource: React.FC = ({ const svgAnchorRef = useSvgAnchor(); const [hover, hoverRef] = useHover(); const groupRefs = useCombineRefs(dragNodeRef, dndDropRef, hoverRef); - const [filtered] = useSearchFilter(element.getLabel()); + const { data, resources, resource } = element.getData(); + const [filtered] = useSearchFilter(element.getLabel(), resource?.metadata?.labels); const displayFilters = useDisplayFilters(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover; const { width, height } = element.getBounds(); const size = Math.min(width, height); - const { data, resources } = element.getData(); const allowEdgeCreation = useAllowEdgeCreation(); const isKafkaConnectionLinkPresent = element.getSourceEdges()?.filter((edge: Edge) => edge.getType() === TYPE_KAFKA_CONNECTION_LINK) diff --git a/frontend/packages/knative-plugin/src/topology/components/nodes/EventingPubSubNode.tsx b/frontend/packages/knative-plugin/src/topology/components/nodes/EventingPubSubNode.tsx index 77cdccf9bbd..8f577c495fe 100644 --- a/frontend/packages/knative-plugin/src/topology/components/nodes/EventingPubSubNode.tsx +++ b/frontend/packages/knative-plugin/src/topology/components/nodes/EventingPubSubNode.tsx @@ -80,13 +80,13 @@ const EventingPubSubNode: React.FC = ({ const { t } = useTranslation(); const groupRefs = useCombineRefs(dragNodeRef, dndDropRef, hoverRef); - const [filtered] = useSearchFilter(element.getLabel()); + const { data, resource } = element.getData(); + const [filtered] = useSearchFilter(element.getLabel(), resource?.metadata?.labels); const displayFilters = useDisplayFilters(); const allowEdgeCreation = useAllowEdgeCreation(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover; const { width, height } = element.getBounds(); - const { data } = element.getData(); const resourceObj = getTopologyResourceObject(element.getData()); const resourceModel = diff --git a/frontend/packages/kubevirt-plugin/src/topology/components/nodes/VmNode.tsx b/frontend/packages/kubevirt-plugin/src/topology/components/nodes/VmNode.tsx index cebf4418147..fadfdcec92b 100644 --- a/frontend/packages/kubevirt-plugin/src/topology/components/nodes/VmNode.tsx +++ b/frontend/packages/kubevirt-plugin/src/topology/components/nodes/VmNode.tsx @@ -82,13 +82,13 @@ const ObservedVmNode: React.FC = ({ const { kind, osImage, vmStatusBundle } = vmData; const displayFilters = useDisplayFilters(); const allowEdgeCreation = useAllowEdgeCreation(); - const [filtered] = useSearchFilter(element.getLabel()); const iconRadius = Math.min(width, height) * 0.25; const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover; const tipContent = `Create a visual connector`; const resourceObj = getResource(element); const resourceModel = modelFor(referenceFor(resourceObj)); + const [filtered] = useSearchFilter(element.getLabel(), resourceObj?.metadata?.labels); const editAccess = useAccessReview({ group: resourceModel.apiGroup, verb: 'patch', diff --git a/frontend/packages/topology/locales/en/topology.json b/frontend/packages/topology/locales/en/topology.json index f57b10c7113..d86b0db37cd 100644 --- a/frontend/packages/topology/locales/en/topology.json +++ b/frontend/packages/topology/locales/en/topology.json @@ -130,6 +130,8 @@ "Pod count": "Pod count", "Labels": "Labels", "Application groupings": "Application groupings", + "Name": "Name", + "Label": "Label", "Mode": "Mode", "Connectivity": "Connectivity", "Consumption": "Consumption", @@ -139,6 +141,7 @@ "Display options": "Display options", "Clear all filters": "Clear all filters", "Filter by resource": "Filter by resource", + "Find by label...": "Find by label...", "Find by name...": "Find by name...", "Find by name": "Find by name", "Search results may appear outside of the visible area. <2>Click here to fit to the screen.": "Search results may appear outside of the visible area. <2>Click here to fit to the screen.", diff --git a/frontend/packages/topology/src/components/graph-view/components/nodes/BaseNode.tsx b/frontend/packages/topology/src/components/graph-view/components/nodes/BaseNode.tsx index 2de77d673b2..d716571d087 100644 --- a/frontend/packages/topology/src/components/graph-view/components/nodes/BaseNode.tsx +++ b/frontend/packages/topology/src/components/graph-view/components/nodes/BaseNode.tsx @@ -85,7 +85,7 @@ const BaseNode: React.FC = ({ name: resourceObj.metadata.name, namespace: resourceObj.metadata.namespace, }); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), resourceObj?.metadata?.labels); const displayFilters = useDisplayFilters(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover; diff --git a/frontend/packages/topology/src/components/graph-view/components/nodes/trapezoidNode/TrapezoidBaseNode.tsx b/frontend/packages/topology/src/components/graph-view/components/nodes/trapezoidNode/TrapezoidBaseNode.tsx index b47ad830ea4..29bfd01959d 100644 --- a/frontend/packages/topology/src/components/graph-view/components/nodes/trapezoidNode/TrapezoidBaseNode.tsx +++ b/frontend/packages/topology/src/components/graph-view/components/nodes/trapezoidNode/TrapezoidBaseNode.tsx @@ -87,7 +87,7 @@ const TrapezoidBaseNode: React.FC = ({ name: resourceObj.metadata.name, namespace: resourceObj.metadata.namespace, }); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), resourceObj?.metadata?.labels); const displayFilters = useDisplayFilters(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover; diff --git a/frontend/packages/topology/src/components/page/TopologyView.tsx b/frontend/packages/topology/src/components/page/TopologyView.tsx index de5f321fba8..50e47c52ff7 100644 --- a/frontend/packages/topology/src/components/page/TopologyView.tsx +++ b/frontend/packages/topology/src/components/page/TopologyView.tsx @@ -41,7 +41,13 @@ import { TopologyDecoratorProvider, TopologyDisplayFilters, } from '../../extensions/topology'; -import { getTopologySearchQuery, useAppliedDisplayFilters, useDisplayFilters } from '../../filters'; +import { + getTopologySearchQuery, + TOPOLOGY_LABELS_FILTER_KEY, + TOPOLOGY_SEARCH_FILTER_KEY, + useAppliedDisplayFilters, + useDisplayFilters, +} from '../../filters'; import { FilterContext } from '../../filters/FilterProvider'; import TopologyFilterBar from '../../filters/TopologyFilterBar'; import { setSupportedTopologyFilters, setSupportedTopologyKinds } from '../../redux/action'; @@ -157,7 +163,8 @@ export const ConnectedTopologyView: React.FC = ({ FileUploadContext, ); - const searchParams = queryParams.get('searchQuery'); + const searchParams = queryParams.get(TOPOLOGY_SEARCH_FILTER_KEY); + const labelParams = queryParams.get(TOPOLOGY_LABELS_FILTER_KEY); const fileTypes = supportedFileExtensions.map((ex) => `.${ex}`).toString(); const onSelect = React.useCallback((entity?: GraphElement) => { @@ -302,7 +309,7 @@ export const ConnectedTopologyView: React.FC = ({ } else { document.body.classList.remove(FILTER_ACTIVE_CLASS); } - }, [searchParams]); + }, [searchParams, labelParams]); const viewContent = React.useMemo( () => diff --git a/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx b/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx new file mode 100644 index 00000000000..dfa822cbdac --- /dev/null +++ b/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { Dropdown, DropdownToggle, DropdownItem } from '@patternfly/react-core'; +import { CaretDownIcon, FilterIcon } from '@patternfly/react-icons'; +import { useTranslation } from 'react-i18next'; +import AutocompleteInput from '@console/internal/components/autocomplete'; +import { TextFilter } from '@console/internal/components/factory'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { NameLabelFilterValues } from './filter-utils'; + +type NameLabelFilterDropdownProps = { + data: K8sResourceKind[]; + onChange: (type: string, value: string, endOfString: boolean) => void; + nameFilterInput: string; + labelFilterInput: string; +}; + +const NameLabelFilterDropdown: React.FC = (props) => { + const { data, onChange, nameFilterInput, labelFilterInput } = props; + + const [isOpen, setOpen] = React.useState(false); + const [selected, setSelected] = React.useState(NameLabelFilterValues.Name); + + const { t } = useTranslation(); + + const onToggle = (open: boolean) => setOpen(open); + const onSelect = (event: React.SyntheticEvent) => { + setSelected((event.target as HTMLInputElement).name as NameLabelFilterValues); + setOpen(!isOpen); + }; + const dropdownItems = [ + + {t(NameLabelFilterValues.Name)} + , + + {t(NameLabelFilterValues.Label)} + , + ]; + + const handleInputValue = (value: string) => { + onChange(selected, value, false); + }; + + return ( +
+ + <> + {t(selected)} + + + } + isOpen={isOpen} + dropdownItems={dropdownItems} + /> + {selected === NameLabelFilterValues.Label ? ( + { + onChange(NameLabelFilterValues.Label, label, true); + }} + showSuggestions + textValue={labelFilterInput} + setTextValue={handleInputValue} + placeholder={t('topology~Find by label...')} + data={data} + className="co-text-node" + labelPath={'metadata.labels'} + /> + ) : ( + + )} +
+ ); +}; + +export default NameLabelFilterDropdown; diff --git a/frontend/packages/topology/src/filters/TopologyFilterBar.tsx b/frontend/packages/topology/src/filters/TopologyFilterBar.tsx index 810933f4b3a..7a326490e9e 100644 --- a/frontend/packages/topology/src/filters/TopologyFilterBar.tsx +++ b/frontend/packages/topology/src/filters/TopologyFilterBar.tsx @@ -7,31 +7,40 @@ import { ToolbarContent, Popover, Button, + ToolbarFilter, } from '@patternfly/react-core'; import { InfoCircleIcon } from '@patternfly/react-icons'; -import { Visualization } from '@patternfly/react-topology'; +import { Visualization, isNode } from '@patternfly/react-topology'; import { Trans, useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { TextFilter } from '@console/internal/components/factory'; -import { ExternalLink } from '@console/internal/components/utils'; +import { ExternalLink, setQueryArgument } from '@console/internal/components/utils'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { ConsoleLinkModel } from '@console/internal/models'; import { K8sResourceKind, referenceForModel } from '@console/internal/module/k8s'; +import { requirementFromString } from '@console/internal/module/k8s/selector-requirement'; import { getActiveNamespace } from '@console/internal/reducers/ui'; import { RootState } from '@console/internal/redux'; import { useQueryParams } from '@console/shared'; import ExportApplication from '../components/export-app/ExportApplication'; import TopologyQuickSearchButton from '../components/quick-search/TopologyQuickSearchButton'; import { TopologyViewType } from '../topology-types'; +import { getResource } from '../utils'; import { getNamespaceDashboardKialiLink } from '../utils/topology-utils'; import { + clearAll, + clearLabelFilter, + clearNameFilter, getSupportedTopologyFilters, getSupportedTopologyKinds, onSearchChange, + NameLabelFilterValues, + TOPOLOGY_LABELS_FILTER_KEY, + TOPOLOGY_SEARCH_FILTER_KEY, } from './filter-utils'; import FilterDropdown from './FilterDropdown'; import { FilterContext } from './FilterProvider'; import KindFilterDropdown from './KindFilterDropdown'; +import NameLabelFilterDropdown from './NameLabelFilterDropdown'; import './TopologyFilterBar.scss'; @@ -61,6 +70,7 @@ const TopologyFilterBar: React.FC = ({ }) => { const { t } = useTranslation(); const { filters, setTopologyFilters: onFiltersChange } = React.useContext(FilterContext); + const [labelFilterInput, setLabelFilterInput] = React.useState(''); const [consoleLinks] = useK8sWatchResource({ isList: true, kind: referenceForModel(ConsoleLinkModel), @@ -68,15 +78,43 @@ const TopologyFilterBar: React.FC = ({ }); const kialiLink = getNamespaceDashboardKialiLink(consoleLinks, namespace); const queryParams = useQueryParams(); - const searchQuery = queryParams.get('searchQuery') || ''; + const searchQuery = queryParams.get(TOPOLOGY_SEARCH_FILTER_KEY) || ''; + const labelsQuery = queryParams.get(TOPOLOGY_LABELS_FILTER_KEY)?.split(',') || []; - const onTextFilterChange = (text) => { - const query = text?.trim(); + const updateNameFilter = (value: string) => { + const query = value?.trim(); onSearchChange(query); }; + const updateLabelFilter = (value: string, endOfString: boolean) => { + setLabelFilterInput(value); + if (requirementFromString(value) !== undefined && endOfString) { + const updatedLabels = [...new Set([...labelsQuery, value])]; + setQueryArgument(TOPOLOGY_LABELS_FILTER_KEY, updatedLabels.join(',')); + setLabelFilterInput(''); + } + }; + + const updateSearchFilter = (type: string, value: string, endOfString: boolean) => { + type === NameLabelFilterValues.Label + ? updateLabelFilter(value, endOfString) + : updateNameFilter(value); + }; + + const removeLabelFilter = (filter: string, value: string) => { + const newLabels = labelsQuery.filter((keepItem: string) => keepItem !== value); + newLabels.length > 0 + ? setQueryArgument(TOPOLOGY_LABELS_FILTER_KEY, newLabels.join(',')) + : clearLabelFilter(); + }; + + const resources = (visualization?.getElements() || []) + .filter(isNode) + .map(getResource) + .filter((r) => !!r); + return ( - + setIsQuickSearchOpen(true)} /> @@ -104,14 +142,25 @@ const TopologyFilterBar: React.FC = ({ - + + 0 ? [searchQuery] : []} + deleteChip={clearNameFilter} + categoryName={t('topology~Name')} + > + + + {viewType === TopologyViewType.graph ? ( diff --git a/frontend/packages/topology/src/filters/__tests__/useSearchFilter.spec.ts b/frontend/packages/topology/src/filters/__tests__/useSearchFilter.spec.ts index e266df25164..e26f3e7ef56 100644 --- a/frontend/packages/topology/src/filters/__tests__/useSearchFilter.spec.ts +++ b/frontend/packages/topology/src/filters/__tests__/useSearchFilter.spec.ts @@ -6,23 +6,27 @@ jest.mock('../filter-utils', () => ({ })); let mockCurrentSearchQuery = ''; +let mockLabelsQuery = ''; jest.mock('@console/shared', () => { const ActualShared = require.requireActual('@console/shared'); return { ...ActualShared, - useQueryParams: () => new Map().set('searchQuery', mockCurrentSearchQuery), + useQueryParams: () => + new Map().set('searchQuery', mockCurrentSearchQuery).set('labels', mockLabelsQuery), }; }); const testUseSearchFilter = ( text: string | null | undefined, searchQuery: string | undefined, + labels?: { [key: string]: string }, + labelsQuery?: string, ): ReturnType => { mockCurrentSearchQuery = searchQuery; - + mockLabelsQuery = labelsQuery; // eslint-disable-next-line react-hooks/rules-of-hooks - return useSearchFilter(text); + return useSearchFilter(text, labels); }; describe('useSearchFilter', () => { @@ -54,6 +58,32 @@ describe('useSearchFilter', () => { }); }); + it('should match labels to labels query even if the name filter does not match', () => { + testHook(() => { + expect(testUseSearchFilter(null, 'test', { foo: 'bar' }, 'foo=bar')[0]).toBe(true); + expect( + testUseSearchFilter(null, 'test', { foo: 'bar', bar: 'baz' }, 'foo=bar,bar=baz')[0], + ).toBe(true); + }); + }); + + it('should match text to search query even if the labels filter does not match', () => { + testHook(() => { + expect(testUseSearchFilter('test', 'test', { foo: 'bar' }, 'foo=')[0]).toBe(true); + expect(testUseSearchFilter('search', 'search', {}, 'foo=bar')[0]).toBe(true); + }); + }); + + it('should not match labels to labels query', () => { + testHook(() => { + expect(testUseSearchFilter(null, null, { foo: 'test' }, 'foo=bar,bar=baz')[0]).toBe(false); + expect(testUseSearchFilter(null, null, { foo: 'bar' }, '')[0]).toBe(false); + expect(testUseSearchFilter(null, null, { foo: 'bar' }, null)[0]).toBe(false); + expect(testUseSearchFilter(null, null, {}, null)[0]).toBe(false); + expect(testUseSearchFilter(null, null, { foo: 'bar' }, 'foo=bar,bar=baz')[0]).toBe(false); + }); + }); + it('should return search query', () => { testHook(() => { expect(testUseSearchFilter(null, undefined)[1]).toBe(undefined); diff --git a/frontend/packages/topology/src/filters/filter-utils.ts b/frontend/packages/topology/src/filters/filter-utils.ts index 0d42f3256b5..b1eb98d65f6 100644 --- a/frontend/packages/topology/src/filters/filter-utils.ts +++ b/frontend/packages/topology/src/filters/filter-utils.ts @@ -13,6 +13,14 @@ import { import { DEFAULT_TOPOLOGY_FILTERS, EXPAND_GROUPS_FILTER_ID, SHOW_GROUPS_FILTER_ID } from './const'; export const TOPOLOGY_SEARCH_FILTER_KEY = 'searchQuery'; +export const TOPOLOGY_LABELS_FILTER_KEY = 'labels'; + +export enum NameLabelFilterValues { + // t('topology~Name') + Name = 'Name', + // t('topology~Label') + Label = 'Label', +} export const onSearchChange = (searchQuery: string): void => { if (searchQuery.length > 0) { @@ -22,6 +30,18 @@ export const onSearchChange = (searchQuery: string): void => { } }; +export const clearNameFilter = () => { + onSearchChange(''); +}; +export const clearLabelFilter = () => { + removeQueryArgument(TOPOLOGY_LABELS_FILTER_KEY); +}; + +export const clearAll = () => { + clearNameFilter(); + clearLabelFilter(); +}; + export const getSupportedTopologyFilters = (state: RootState): string[] => { const topology = state?.plugins?.devconsole?.topology; return topology ? topology.get('supportedFilters') : DEFAULT_TOPOLOGY_FILTERS.map((f) => f.id); @@ -32,7 +52,10 @@ export const getSupportedTopologyKinds = (state: RootState): { [key: string]: nu return topology ? topology.get('supportedKinds') : {}; }; -export const getTopologySearchQuery = () => getQueryArgument(TOPOLOGY_SEARCH_FILTER_KEY) ?? ''; +export const getTopologySearchQuery = () => + getQueryArgument(TOPOLOGY_SEARCH_FILTER_KEY) ?? + getQueryArgument(TOPOLOGY_LABELS_FILTER_KEY) ?? + ''; export const getFilterById = (id: string, filters: DisplayFilters): TopologyDisplayOption => { if (!filters) { diff --git a/frontend/packages/topology/src/filters/useSearchFilter.ts b/frontend/packages/topology/src/filters/useSearchFilter.ts index bebc5ca329b..081e91c08d2 100644 --- a/frontend/packages/topology/src/filters/useSearchFilter.ts +++ b/frontend/packages/topology/src/filters/useSearchFilter.ts @@ -4,15 +4,24 @@ import { toLower } from 'lodash'; import { useQueryParams } from '@console/shared/src'; const fuzzyCaseInsensitive = (a: string, b: string): boolean => fuzzy(toLower(a), toLower(b)); - -const useSearchFilter = (text: string): [boolean, string] => { +const useSearchFilter = ( + name: string, + labels: { [key: string]: string } = {}, +): [boolean, string] => { const queryParams = useQueryParams(); const searchQuery = queryParams.get('searchQuery'); - const filtered = React.useMemo(() => fuzzyCaseInsensitive(searchQuery, text), [ + const labelsQuery = queryParams.get('labels')?.split(',') ?? []; + const labelsString = Object.entries(labels).map((label) => label.join('=')); + + const labelsMatched = React.useMemo( + () => labelsQuery.every((label) => labelsString.includes(label)), + [labelsQuery, labelsString], + ); + const filtered = React.useMemo(() => fuzzyCaseInsensitive(searchQuery, name), [ searchQuery, - text, + name, ]); - return [filtered && !!searchQuery, searchQuery]; + return [(filtered && !!searchQuery) || (labelsMatched && labelsQuery.length > 0), searchQuery]; }; export { useSearchFilter }; diff --git a/frontend/packages/topology/src/operators/components/OperatorBackedServiceGroup.tsx b/frontend/packages/topology/src/operators/components/OperatorBackedServiceGroup.tsx index 78032bebb27..6fdd668971e 100644 --- a/frontend/packages/topology/src/operators/components/OperatorBackedServiceGroup.tsx +++ b/frontend/packages/topology/src/operators/components/OperatorBackedServiceGroup.tsx @@ -29,7 +29,7 @@ import { useSearchFilter, SHOW_LABELS_FILTER_ID, } from '../../filters'; -import { getResourceKind } from '../../utils/topology-utils'; +import { getResource, getResourceKind } from '../../utils/topology-utils'; type OperatorBackedServiceGroupProps = { element: Node; @@ -56,7 +56,7 @@ const OperatorBackedServiceGroup: React.FC = ({ const hasChildren = element.getChildren()?.length > 0; const { data } = element.getData(); const ownerReferenceKind = referenceFor({ kind: data.operatorKind, apiVersion: data.apiVersion }); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), getResource(element)?.metadata?.labels); const displayFilters = useDisplayFilters(); const showLabelsFilter = getFilterById(SHOW_LABELS_FILTER_ID, displayFilters); const showLabels = showLabelsFilter?.value || hover || innerHover; diff --git a/frontend/packages/topology/src/operators/components/OperatorBackedServiceNode.tsx b/frontend/packages/topology/src/operators/components/OperatorBackedServiceNode.tsx index 21cb36188b0..fc47ec79445 100644 --- a/frontend/packages/topology/src/operators/components/OperatorBackedServiceNode.tsx +++ b/frontend/packages/topology/src/operators/components/OperatorBackedServiceNode.tsx @@ -23,6 +23,7 @@ import { GroupNodeAnchor, } from '../../components/graph-view'; import { useSearchFilter } from '../../filters/useSearchFilter'; +import { getResource } from '../../utils/topology-utils'; type OperatorBackedServiceNodeProps = { element: Node; @@ -44,7 +45,7 @@ const OperatorBackedServiceNode: React.FC = ({ const [hover, hoverRef] = useHover(); const [{ dragging }, dragNodeRef] = useDragNode(noRegroupDragSourceSpec); const refs = useCombineRefs(hoverRef, dragNodeRef, dndDropRef); - const [filtered] = useSearchFilter(element.getLabel()); + const [filtered] = useSearchFilter(element.getLabel(), getResource(element)?.metadata?.labels); const kind = 'Operator'; const { groupResources } = element.getData(); const [groupSize, groupRef] = useSize([groupResources]); From 331dc23b180a89fa8ca7050b20e77a8625535918 Mon Sep 17 00:00:00 2001 From: karthik Date: Wed, 24 Nov 2021 21:56:02 +0530 Subject: [PATCH 2/3] Add label filter to topology list view --- .../src/components/list-view/TopologyListViewNode.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/packages/topology/src/components/list-view/TopologyListViewNode.tsx b/frontend/packages/topology/src/components/list-view/TopologyListViewNode.tsx index 9aaee286ff9..927f2cb1ed2 100644 --- a/frontend/packages/topology/src/components/list-view/TopologyListViewNode.tsx +++ b/frontend/packages/topology/src/components/list-view/TopologyListViewNode.tsx @@ -22,7 +22,7 @@ import { shouldHideMonitoringAlertDecorator, } from '@console/shared'; import { useSearchFilter } from '../../filters'; -import { getResourceKind } from '../../utils/topology-utils'; +import { getResource, getResourceKind } from '../../utils/topology-utils'; import { AlertsCell, GroupResourcesCell, @@ -68,7 +68,7 @@ const TopologyListViewNode: React.FC children, }) => { const { t } = useTranslation(); - const [filtered] = useSearchFilter(item.getLabel()); + const [filtered] = useSearchFilter(item.getLabel(), getResource(item)?.metadata?.labels); if (!item.isVisible) { return null; } From 2ef6d4110e2df6993a25148ab1fbe208624df381 Mon Sep 17 00:00:00 2001 From: karthik Date: Tue, 30 Nov 2021 16:49:22 +0530 Subject: [PATCH 3/3] Disabled the Name label dropdown in topology empty state --- .../topology/src/filters/NameLabelFilterDropdown.tsx | 12 ++++++++++-- .../topology/src/filters/TopologyFilterBar.tsx | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx b/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx index dfa822cbdac..ba10620e109 100644 --- a/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx +++ b/frontend/packages/topology/src/filters/NameLabelFilterDropdown.tsx @@ -8,6 +8,7 @@ import { K8sResourceKind } from '@console/internal/module/k8s'; import { NameLabelFilterValues } from './filter-utils'; type NameLabelFilterDropdownProps = { + isDisabled: boolean; data: K8sResourceKind[]; onChange: (type: string, value: string, endOfString: boolean) => void; nameFilterInput: string; @@ -15,7 +16,7 @@ type NameLabelFilterDropdownProps = { }; const NameLabelFilterDropdown: React.FC = (props) => { - const { data, onChange, nameFilterInput, labelFilterInput } = props; + const { data, onChange, nameFilterInput, labelFilterInput, isDisabled } = props; const [isOpen, setOpen] = React.useState(false); const [selected, setSelected] = React.useState(NameLabelFilterValues.Name); @@ -45,7 +46,12 @@ const NameLabelFilterDropdown: React.FC = (props) + <> {t(selected)} @@ -73,6 +79,8 @@ const NameLabelFilterDropdown: React.FC = (props) placeholder={t('topology~Find by name...')} value={nameFilterInput} aria-labelledby="toggle-id" + isDisabled={isDisabled} + autoFocus /> )} diff --git a/frontend/packages/topology/src/filters/TopologyFilterBar.tsx b/frontend/packages/topology/src/filters/TopologyFilterBar.tsx index 7a326490e9e..0139065efd7 100644 --- a/frontend/packages/topology/src/filters/TopologyFilterBar.tsx +++ b/frontend/packages/topology/src/filters/TopologyFilterBar.tsx @@ -158,6 +158,7 @@ const TopologyFilterBar: React.FC = ({ nameFilterInput={searchQuery} labelFilterInput={labelFilterInput} data={resources} + isDisabled={isDisabled} />