Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize filter dropdowns #2393

Merged
merged 3 commits into from
Dec 11, 2024
Merged
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
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
Loading