Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { useDataViewFilters } from '@patternfly/react-data-view';
import { useSearchParams } from 'react-router';
import { useExactSearch } from '@console/app/src/components/user-preferences/search/useExactSearch';
Expand Down Expand Up @@ -38,6 +38,39 @@ export const useConsoleDataViewFilters = <
setSearchParams,
});

// Sync URL search params → internal filter state.
// useDataViewFilters only reads searchParams on mount (empty deps useEffect).
// This effect ensures filters stay in sync when the URL changes externally
// (e.g., the Search page updating query params without remounting).
const filtersRef = useRef(filters);
filtersRef.current = filters;
useEffect(() => {
const updates: Partial<TFilters> = {};
let hasChanges = false;
for (const key of Object.keys(filtersRef.current)) {
const currentValue = filtersRef.current[key];
if (Array.isArray(currentValue)) {
const urlValues = searchParams.getAll(key);
if (
urlValues.length !== currentValue.length ||
urlValues.some((v, i) => v !== currentValue[i])
) {
updates[key] = urlValues;
hasChanges = true;
}
} else {
const urlValue = searchParams.get(key) ?? '';
if (urlValue !== currentValue) {
updates[key] = urlValue;
hasChanges = true;
}
}
}
if (hasChanges) {
onSetFilters(updates as TFilters);
}
}, [searchParams, onSetFilters]);

const filteredData = useMemo(
() =>
data?.filter((resource) => {
Expand Down
91 changes: 55 additions & 36 deletions frontend/public/components/search.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as _ from 'lodash';
import type { FC, MouseEvent } from 'react';
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo, memo } from 'react';
import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle';
import { useDebounceCallback } from '@console/shared/src/hooks/useDebounceCallback';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -51,37 +51,51 @@ import { useActivePerspective } from '@console/dynamic-plugin-sdk/src/perspectiv
import { useActiveNamespace, useK8sModel } from '@console/dynamic-plugin-sdk/src/lib-core';
import { ALL_NAMESPACES_KEY } from '@console/shared/src/constants';

const ResourceList = ({ kind, mock, namespace, selector, nameFilter }) => {
const { plural } = useParams<{ plural?: string }>();
const [kindObj] = useK8sModel(kind || plural);
const resourceListPageExtensions = useExtensions<ResourceListPage>(isResourceListPage);
if (!kindObj) {
return <LoadingBox />;
}
const ResourceList = memo(
({
kind,
mock,
namespace,
selector,
nameFilter,
}: {
kind: string;
mock: boolean;
namespace: string;
selector: any;
nameFilter: string;
}) => {
const { plural } = useParams<{ plural?: string }>();
const [kindObj] = useK8sModel(kind || plural);
const resourceListPageExtensions = useExtensions<ResourceListPage>(isResourceListPage);
if (!kindObj) {
return <LoadingBox />;
}

const componentLoader = getResourceListPages(resourceListPageExtensions).get(
referenceForModel(kindObj),
() => Promise.resolve(DefaultPage),
);
const ns = kindObj.namespaced ? namespace : undefined;
const componentLoader = getResourceListPages(resourceListPageExtensions).get(
referenceForModel(kindObj),
() => Promise.resolve(DefaultPage),
);
const ns = kindObj.namespaced ? namespace : undefined;

return (
<AsyncComponent
loader={componentLoader}
namespace={ns === ALL_NAMESPACES_KEY ? undefined : ns}
selector={selector}
nameFilter={nameFilter}
kind={kindObj.crd ? referenceForModel(kindObj) : kindObj.kind}
showTitle={false}
hideTextFilter
autoFocus={false}
mock={mock}
badge={getBadgeFromType(kindObj.badge)}
hideNameLabelFilters
hideColumnManagement
/>
);
};
return (
<AsyncComponent
loader={componentLoader}
namespace={ns === ALL_NAMESPACES_KEY ? undefined : ns}
selector={selector}
nameFilter={nameFilter}
kind={kindObj.crd ? referenceForModel(kindObj) : kindObj.kind}
showTitle={false}
hideTextFilter
autoFocus={false}
mock={mock}
badge={getBadgeFromType(kindObj.badge)}
hideNameLabelFilters
hideColumnManagement
/>
);
},
);

const SearchPage_: FC<SearchProps> = (props) => {
const { setQueryArgument, removeQueryArguments } = useQueryParamsMutator();
Expand Down Expand Up @@ -120,18 +134,20 @@ const SearchPage_: FC<SearchProps> = (props) => {
const validTags = _.reject(tags, (tag) => requirementFromString(tag) === undefined);
setLabelFilter(validTags);
setTypeaheadNameFilter(name || '');
setDebouncedNameFilter(name || '');
}, [location.search]);

const debouncedNameFilterCallback = useDebounceCallback((nameFilter: string) => {
setDebouncedNameFilter(nameFilter);
setQueryArgument('name', nameFilter);
}, 300);

useEffect(() => {
debouncedNameFilterCallback(typeaheadNameFilter);
}, [typeaheadNameFilter, debouncedNameFilterCallback]);

const updateSelectedItems = (selection: string) => {
const updateItems = selectedItems;
const updateItems = new Set(selectedItems);
fireTelemetryEvent('search-resource-selected', {
resource: selection,
});
Expand All @@ -141,7 +157,7 @@ const SearchPage_: FC<SearchProps> = (props) => {
};

const updateNewItems = (_filter: string, { key }: ToolbarLabel) => {
const updateItems = selectedItems;
const updateItems = new Set(selectedItems);
updateItems.has(key) ? updateItems.delete(key) : updateItems.add(key);
setSelectedItems(updateItems);
setQueryArgument('kind', [...updateItems].join(','));
Expand All @@ -154,6 +170,7 @@ const SearchPage_: FC<SearchProps> = (props) => {

const clearNameFilter = () => {
setTypeaheadNameFilter('');
setDebouncedNameFilter('');
setQueryArgument('name', '');
};

Expand All @@ -165,6 +182,7 @@ const SearchPage_: FC<SearchProps> = (props) => {
const clearAll = () => {
setSelectedItems(new Set([]));
setTypeaheadNameFilter('');
setDebouncedNameFilter('');
setLabelFilter([]);
removeQueryArguments('kind', 'name', 'q');
};
Expand All @@ -188,7 +206,6 @@ const SearchPage_: FC<SearchProps> = (props) => {

const updateNameFilter = (value: string) => {
setTypeaheadNameFilter(value);
setQueryArgument('name', value);
};

const updateLabelFilter = (value: string, endOfString: boolean) => {
Expand Down Expand Up @@ -239,6 +256,8 @@ const SearchPage_: FC<SearchProps> = (props) => {
return model.labelKey ? t(model.labelKey) : model.label;
};

const selector = useMemo(() => selectorFromString(labelFilter.join(',')), [labelFilter]);

return (
<>
<DocumentTitle>{t('public~Search')}</DocumentTitle>
Expand Down Expand Up @@ -338,11 +357,11 @@ const SearchPage_: FC<SearchProps> = (props) => {
{!isCollapsed && (
<ResourceList
kind={resource}
selector={selectorFromString(labelFilter.join(','))}
nameFilter={typeaheadNameFilter}
selector={selector}
nameFilter={debouncedNameFilter}
namespace={namespace}
mock={noProjectsAvailable}
key={`${resource}-${labelFilter.join(',')}-${debouncedNameFilter}`}
key={resource}
/>
)}
</AccordionContent>
Expand Down