Skip to content

Commit

Permalink
Optimize filter dropdowns (#2393)
Browse files Browse the repository at this point in the history
* Enhance Combobox component with controlled open state and loading indicators

- Added `open` and `setOpen` props to allow external control of the Combobox state.
- Improved loading state handling to display a spinner when loading and no items are present.
- Refactored conditional rendering for better clarity and performance.

* Refactor SearchableContainerList to manage open state and improve loading indicators

- Added `isOpen` and `setIsOpen` props to `SearchableContainer` for better control of the component's visibility.
- Enhanced loading state handling to differentiate between fetching and refetching states.
- Updated the rendering logic to ensure proper display of the Combobox and its content based on the new state management.

* Refactor SearchableHostList and remove onClearAll handlers

- Updated SearchableHostList to utilize new ComboboxV2 components for improved UI and functionality.
- Introduced debounced search functionality for better performance during host searches.
- Removed onClearAll handlers from various components to streamline state management and reduce redundancy.
- Enhanced loading state handling in SearchableHostList for a smoother user experience.
  • Loading branch information
manV authored Dec 11, 2024
1 parent ef43f0d commit 3a11e79
Show file tree
Hide file tree
Showing 18 changed files with 135 additions and 178 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ const SearchableContainer = ({
isScannedForVulnerabilities,
isScannedForSecrets,
isScannedForMalware,
}: Props) => {
isOpen,
setIsOpen,
}: Props & {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}) => {
const [searchText, setSearchText] = useState('');
const debouncedSearchText = useDebouncedValue(searchText, 500);

Expand All @@ -52,7 +57,7 @@ const SearchableContainer = ({
setSelectedContainers(defaultSelectedContainers ?? []);
}, [defaultSelectedContainers]);

const { data, isFetchingNextPage, hasNextPage, fetchNextPage } =
const { data, isFetchingNextPage, hasNextPage, fetchNextPage, isRefetching } =
useSuspenseInfiniteQuery({
...queries.search.containers({
scanType,
Expand All @@ -67,6 +72,7 @@ const SearchableContainer = ({
descending: false,
},
}),
enabled: isOpen,
keepPreviousData: true,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.containers.length < PAGE_SIZE) return null;
Expand All @@ -79,7 +85,9 @@ const SearchableContainer = ({
});

const onEndReached = () => {
if (hasNextPage) fetchNextPage();
if (hasNextPage) {
fetchNextPage({ cancelRefetch: true });
}
};

return (
Expand All @@ -94,7 +102,9 @@ const SearchableContainer = ({
setValue={setSearchText}
defaultSelectedValue={defaultSelectedContainers}
name={fieldName}
loading={isFetchingNextPage}
loading={isFetchingNextPage && !isRefetching}
open={isOpen}
setOpen={setIsOpen}
>
{isSelectVariantType ? (
<ComboboxV2TriggerInput
Expand Down Expand Up @@ -141,13 +151,18 @@ export const SearchableContainerList = (props: Props) => {
return triggerVariant === 'select';
}, [triggerVariant]);

const [isOpen, setIsOpen] = useState(false);

return (
<Suspense
fallback={
<>
<ComboboxV2Provider
defaultSelectedValue={defaultSelectedContainers}
name={fieldName}
open={isOpen}
setOpen={setIsOpen}
loading
>
{isSelectVariantType ? (
<ComboboxV2TriggerInput
Expand All @@ -156,17 +171,18 @@ export const SearchableContainerList = (props: Props) => {
startIcon={<CircleSpinner size="sm" className="w-3 h-3" />}
/>
) : (
<ComboboxV2TriggerButton
startIcon={<CircleSpinner size="sm" className="w-3 h-3" />}
>
Select container
</ComboboxV2TriggerButton>
<ComboboxV2TriggerButton>Select container</ComboboxV2TriggerButton>
)}
<ComboboxV2Content
width={isSelectVariantType ? 'anchor' : 'fixed'}
clearButtonContent="Clear"
searchPlaceholder="Search"
></ComboboxV2Content>
</ComboboxV2Provider>
</>
}
>
<SearchableContainer {...props} />
<SearchableContainer {...props} isOpen={isOpen} setIsOpen={setIsOpen} />
</Suspense>
);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { useSuspenseInfiniteQuery } from '@suspensive/react-query';
import { debounce } from 'lodash-es';
import { Suspense, useEffect, useMemo, useState } from 'react';
import { CircleSpinner, Combobox, ComboboxOption } from 'ui-components';
import {
CircleSpinner,
ComboboxV2Content,
ComboboxV2Item,
ComboboxV2Provider,
ComboboxV2TriggerButton,
ComboboxV2TriggerInput,
} from 'ui-components';

import { queries } from '@/queries';
import { ScanTypeEnum } from '@/types/common';
import { useDebouncedValue } from '@/utils/useDebouncedValue';

export type SearchableHostListProps = {
export interface SearchableHostListProps {
scanType: ScanTypeEnum | 'none';
onChange?: (value: string[]) => void;
onClearAll?: () => void;
defaultSelectedHosts?: string[];
valueKey?: 'nodeId' | 'hostName' | 'nodeName';
active?: boolean;
Expand All @@ -22,13 +28,14 @@ export type SearchableHostListProps = {
isScannedForSecrets?: boolean;
isScannedForMalware?: boolean;
displayValue?: string;
};
}

const fieldName = 'hostFilter';
const PAGE_SIZE = 15;

const SearchableHost = ({
scanType,
onChange,
onClearAll,
defaultSelectedHosts,
valueKey = 'nodeId',
active,
Expand All @@ -41,8 +48,14 @@ const SearchableHost = ({
isScannedForSecrets,
isScannedForMalware,
displayValue,
}: SearchableHostListProps) => {
isOpen,
setIsOpen,
}: SearchableHostListProps & {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}) => {
const [searchText, setSearchText] = useState('');
const debouncedSearchText = useDebouncedValue(searchText, 500);

const [selectedHosts, setSelectedHosts] = useState<string[]>(
defaultSelectedHosts ?? [],
Expand All @@ -56,12 +69,12 @@ const SearchableHost = ({
setSelectedHosts(defaultSelectedHosts ?? []);
}, [defaultSelectedHosts]);

const { data, isFetchingNextPage, hasNextPage, fetchNextPage } =
const { data, isFetchingNextPage, hasNextPage, fetchNextPage, isRefetching } =
useSuspenseInfiniteQuery({
...queries.search.hosts({
scanType,
size: PAGE_SIZE,
searchText,
searchText: debouncedSearchText,
active,
agentRunning,
showOnlyKubernetesHosts,
Expand All @@ -73,8 +86,10 @@ const SearchableHost = ({
descending: false,
},
}),
enabled: isOpen,
keepPreviousData: true,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.hosts.length < PAGE_SIZE) return null;
return allPages.length * PAGE_SIZE;
},
getPreviousPageParam: (firstPage, allPages) => {
Expand All @@ -83,89 +98,98 @@ const SearchableHost = ({
},
});

const searchHost = debounce((query: string) => {
setSearchText(query);
}, 1000);

const onEndReached = () => {
if (hasNextPage) fetchNextPage();
};

return (
<>
<Combobox
startIcon={
isFetchingNextPage ? <CircleSpinner size="sm" className="w-3 h-3" /> : undefined
}
name={fieldName}
triggerVariant={triggerVariant || 'button'}
label={isSelectVariantType ? 'Host' : undefined}
getDisplayValue={() =>
isSelectVariantType && selectedHosts.length > 0
? `${selectedHosts.length} selected`
: displayValue
? displayValue
: null
}
placeholder="Select host"
multiple
value={selectedHosts}
onChange={(values) => {
setSelectedHosts(values);
onChange?.(values);
}}
onQueryChange={searchHost}
clearAllElement="Clear"
onClearAll={onClearAll}
<ComboboxV2Provider
selectedValue={selectedHosts}
setSelectedValue={(values) => {
setSelectedHosts(values as string[]);
onChange?.(values as string[]);
}}
value={searchText}
setValue={setSearchText}
defaultSelectedValue={defaultSelectedHosts}
name={fieldName}
loading={isFetchingNextPage && !isRefetching}
open={isOpen}
setOpen={setIsOpen}
>
{isSelectVariantType ? (
<ComboboxV2TriggerInput
getDisplayValue={() =>
selectedHosts.length > 0
? `${selectedHosts.length} selected`
: displayValue ?? null
}
placeholder="Select host"
label="Host"
helperText={helperText}
color={color}
/>
) : (
<ComboboxV2TriggerButton>Select host</ComboboxV2TriggerButton>
)}
<ComboboxV2Content
width={isSelectVariantType ? 'anchor' : 'fixed'}
clearButtonContent="Clear"
onEndReached={onEndReached}
helperText={helperText}
color={color}
searchPlaceholder="Search"
>
{data?.pages
.flatMap((page) => {
return page.hosts;
})
.map((host, index) => {
return (
<ComboboxOption key={`${host.nodeId}-${index}`} value={host[valueKey]}>
{host.nodeName}
</ComboboxOption>
);
})}
</Combobox>
</>
.flatMap((page) => page.hosts)
.map((host, index) => (
<ComboboxV2Item key={`${host.nodeId}-${index}`} value={host[valueKey]}>
{host.nodeName}
</ComboboxV2Item>
))}
</ComboboxV2Content>
</ComboboxV2Provider>
);
};

export const SearchableHostList = (props: SearchableHostListProps) => {
const { triggerVariant, defaultSelectedHosts = [] } = props;
const { triggerVariant, defaultSelectedHosts = [], displayValue } = props;
const isSelectVariantType = useMemo(() => {
return triggerVariant === 'select';
}, [triggerVariant]);

const [isOpen, setIsOpen] = useState(false);

return (
<Suspense
fallback={
<>
<Combobox
name={fieldName}
value={defaultSelectedHosts}
label={isSelectVariantType ? 'Host' : undefined}
triggerVariant={triggerVariant || 'button'}
startIcon={<CircleSpinner size="sm" className="w-3 h-3" />}
placeholder="Select host"
multiple
onQueryChange={() => {
// no operation
}}
getDisplayValue={() => {
return props.displayValue ? props.displayValue : 'Select host';
}}
<ComboboxV2Provider
defaultSelectedValue={defaultSelectedHosts}
name={fieldName}
open={isOpen}
setOpen={setIsOpen}
loading
>
{isSelectVariantType ? (
<ComboboxV2TriggerInput
getDisplayValue={() =>
defaultSelectedHosts.length > 0
? `${defaultSelectedHosts.length} selected`
: displayValue ?? null
}
placeholder="Select host"
label="Host"
/>
) : (
<ComboboxV2TriggerButton>Select host</ComboboxV2TriggerButton>
)}
<ComboboxV2Content
width={isSelectVariantType ? 'anchor' : 'fixed'}
clearButtonContent="Clear"
searchPlaceholder="Search"
/>
</>
</ComboboxV2Provider>
}
>
<SearchableHost {...props} />
<SearchableHost {...props} isOpen={isOpen} setIsOpen={setIsOpen} />
</Suspense>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,6 @@ export const ReportFilters = () => {
scanType={'none'}
defaultSelectedHosts={searchParams.getAll('host')}
agentRunning={false}
onClearAll={() => {
setSearchParams((prev) => {
prev.delete('host');
prev.delete('page');
return prev;
});
}}
onChange={(value) => {
setSearchParams((prev) => {
prev.delete('host');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@ export const AdvancedFilters = ({
onChange={(value) => {
setHosts(value);
}}
onClearAll={() => {
setHosts([]);
}}
agentRunning={false}
active={false}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ export const AdvancedFilter = ({
onChange={(value) => {
setHosts(value);
}}
onClearAll={() => {
setHosts([]);
}}
agentRunning={false}
active={!deadNodes}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,6 @@ const Filters = () => {
scanType={ScanTypeEnum.MalwareScan}
defaultSelectedHosts={searchParams.getAll('hosts')}
isScannedForMalware
onClearAll={() => {
setSearchParams((prev) => {
prev.delete('hosts');
prev.delete('page');
return prev;
});
}}
onChange={(value) => {
setSearchParams((prev) => {
prev.delete('hosts');
Expand Down
Loading

0 comments on commit 3a11e79

Please sign in to comment.