From 3a11e79aa30cfcb603c1c1d7e304a28bdf7fbfa8 Mon Sep 17 00:00:00 2001 From: Manan Vaghasiya Date: Wed, 11 Dec 2024 10:06:54 +0530 Subject: [PATCH] Optimize filter dropdowns (#2393) * 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. --- .../forms/SearchableContainerList.tsx | 36 ++-- .../components/forms/SearchableHostList.tsx | 164 ++++++++++-------- .../integrations/components/ReportsTable.tsx | 7 - .../integration-form/AdvancedFilter.tsx | 3 - .../components/report-form/AdvanceFilter.tsx | 3 - .../features/malwares/pages/MalwareScans.tsx | 7 - .../malwares/pages/UniqueMalwares.tsx | 7 - .../src/features/postures/pages/Accounts.tsx | 7 - .../features/secrets/pages/SecretScans.tsx | 7 - .../features/secrets/pages/UniqueSecrets.tsx | 7 - .../settings/pages/DiagnosticLogs.tsx | 3 - .../tables/ContainersTable.tsx | 7 - .../data-components/tables/HostsTable.tsx | 7 - .../data-components/tables/PodsTable.tsx | 7 - .../vulnerabilities/pages/RuntimeBom.tsx | 7 - .../pages/UniqueVulnerabilities.tsx | 7 - .../pages/VulnerabilityScans.tsx | 7 - .../src/components/select-v2/Combobox.tsx | 20 ++- 18 files changed, 135 insertions(+), 178 deletions(-) diff --git a/deepfence_frontend/apps/dashboard/src/components/forms/SearchableContainerList.tsx b/deepfence_frontend/apps/dashboard/src/components/forms/SearchableContainerList.tsx index 3ba44026fc..7d42148303 100644 --- a/deepfence_frontend/apps/dashboard/src/components/forms/SearchableContainerList.tsx +++ b/deepfence_frontend/apps/dashboard/src/components/forms/SearchableContainerList.tsx @@ -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); @@ -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, @@ -67,6 +72,7 @@ const SearchableContainer = ({ descending: false, }, }), + enabled: isOpen, keepPreviousData: true, getNextPageParam: (lastPage, allPages) => { if (lastPage.containers.length < PAGE_SIZE) return null; @@ -79,7 +85,9 @@ const SearchableContainer = ({ }); const onEndReached = () => { - if (hasNextPage) fetchNextPage(); + if (hasNextPage) { + fetchNextPage({ cancelRefetch: true }); + } }; return ( @@ -94,7 +102,9 @@ const SearchableContainer = ({ setValue={setSearchText} defaultSelectedValue={defaultSelectedContainers} name={fieldName} - loading={isFetchingNextPage} + loading={isFetchingNextPage && !isRefetching} + open={isOpen} + setOpen={setIsOpen} > {isSelectVariantType ? ( { return triggerVariant === 'select'; }, [triggerVariant]); + const [isOpen, setIsOpen] = useState(false); + return ( { {isSelectVariantType ? ( { startIcon={} /> ) : ( - } - > - Select container - + Select container )} + } > - + ); }; diff --git a/deepfence_frontend/apps/dashboard/src/components/forms/SearchableHostList.tsx b/deepfence_frontend/apps/dashboard/src/components/forms/SearchableHostList.tsx index ab95a2ec05..708eaeb4fc 100644 --- a/deepfence_frontend/apps/dashboard/src/components/forms/SearchableHostList.tsx +++ b/deepfence_frontend/apps/dashboard/src/components/forms/SearchableHostList.tsx @@ -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; @@ -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, @@ -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( defaultSelectedHosts ?? [], @@ -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, @@ -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) => { @@ -83,89 +98,98 @@ const SearchableHost = ({ }, }); - const searchHost = debounce((query: string) => { - setSearchText(query); - }, 1000); - const onEndReached = () => { if (hasNextPage) fetchNextPage(); }; return ( - <> - : 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} + { + setSelectedHosts(values as string[]); + onChange?.(values as string[]); + }} + value={searchText} + setValue={setSearchText} + defaultSelectedValue={defaultSelectedHosts} + name={fieldName} + loading={isFetchingNextPage && !isRefetching} + open={isOpen} + setOpen={setIsOpen} + > + {isSelectVariantType ? ( + + selectedHosts.length > 0 + ? `${selectedHosts.length} selected` + : displayValue ?? null + } + placeholder="Select host" + label="Host" + helperText={helperText} + color={color} + /> + ) : ( + Select host + )} + {data?.pages - .flatMap((page) => { - return page.hosts; - }) - .map((host, index) => { - return ( - - {host.nodeName} - - ); - })} - - + .flatMap((page) => page.hosts) + .map((host, index) => ( + + {host.nodeName} + + ))} + + ); }; 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 ( - } - placeholder="Select host" - multiple - onQueryChange={() => { - // no operation - }} - getDisplayValue={() => { - return props.displayValue ? props.displayValue : 'Select host'; - }} + + {isSelectVariantType ? ( + + defaultSelectedHosts.length > 0 + ? `${defaultSelectedHosts.length} selected` + : displayValue ?? null + } + placeholder="Select host" + label="Host" + /> + ) : ( + Select host + )} + - + } > - + ); }; diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/components/ReportsTable.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/components/ReportsTable.tsx index 54c51e3355..f9f084c769 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/components/ReportsTable.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/components/ReportsTable.tsx @@ -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'); diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/components/integration-form/AdvancedFilter.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/components/integration-form/AdvancedFilter.tsx index 32cb8c7da1..5d32994aea 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/components/integration-form/AdvancedFilter.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/components/integration-form/AdvancedFilter.tsx @@ -107,9 +107,6 @@ export const AdvancedFilters = ({ onChange={(value) => { setHosts(value); }} - onClearAll={() => { - setHosts([]); - }} agentRunning={false} active={false} /> diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/components/report-form/AdvanceFilter.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/components/report-form/AdvanceFilter.tsx index eab2c40757..9e79943839 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/components/report-form/AdvanceFilter.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/components/report-form/AdvanceFilter.tsx @@ -110,9 +110,6 @@ export const AdvancedFilter = ({ onChange={(value) => { setHosts(value); }} - onClearAll={() => { - setHosts([]); - }} agentRunning={false} active={!deadNodes} /> diff --git a/deepfence_frontend/apps/dashboard/src/features/malwares/pages/MalwareScans.tsx b/deepfence_frontend/apps/dashboard/src/features/malwares/pages/MalwareScans.tsx index 669cc04d3a..b26d797505 100644 --- a/deepfence_frontend/apps/dashboard/src/features/malwares/pages/MalwareScans.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/malwares/pages/MalwareScans.tsx @@ -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'); diff --git a/deepfence_frontend/apps/dashboard/src/features/malwares/pages/UniqueMalwares.tsx b/deepfence_frontend/apps/dashboard/src/features/malwares/pages/UniqueMalwares.tsx index 7a98489eb7..0fecd352b4 100644 --- a/deepfence_frontend/apps/dashboard/src/features/malwares/pages/UniqueMalwares.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/malwares/pages/UniqueMalwares.tsx @@ -145,13 +145,6 @@ const Filters = () => { { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/postures/pages/Accounts.tsx b/deepfence_frontend/apps/dashboard/src/features/postures/pages/Accounts.tsx index 052f52794f..c6c5717fc5 100644 --- a/deepfence_frontend/apps/dashboard/src/features/postures/pages/Accounts.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/postures/pages/Accounts.tsx @@ -523,13 +523,6 @@ const Filters = () => { scanType={'none'} displayValue={FILTER_SEARCHPARAMS['hosts']} defaultSelectedHosts={searchParams.getAll('hosts')} - onClearAll={() => { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/secrets/pages/SecretScans.tsx b/deepfence_frontend/apps/dashboard/src/features/secrets/pages/SecretScans.tsx index 5757c8efa6..f5fb91a768 100644 --- a/deepfence_frontend/apps/dashboard/src/features/secrets/pages/SecretScans.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/secrets/pages/SecretScans.tsx @@ -544,13 +544,6 @@ const Filters = () => { scanType={ScanTypeEnum.SecretScan} defaultSelectedHosts={searchParams.getAll('hosts')} isScannedForSecrets - onClearAll={() => { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/secrets/pages/UniqueSecrets.tsx b/deepfence_frontend/apps/dashboard/src/features/secrets/pages/UniqueSecrets.tsx index e840e71c19..021d4d512c 100644 --- a/deepfence_frontend/apps/dashboard/src/features/secrets/pages/UniqueSecrets.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/secrets/pages/UniqueSecrets.tsx @@ -146,13 +146,6 @@ const Filters = () => { { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/settings/pages/DiagnosticLogs.tsx b/deepfence_frontend/apps/dashboard/src/features/settings/pages/DiagnosticLogs.tsx index 99be2d823d..8174154fa1 100644 --- a/deepfence_frontend/apps/dashboard/src/features/settings/pages/DiagnosticLogs.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/settings/pages/DiagnosticLogs.tsx @@ -478,9 +478,6 @@ const AgentDiagnosticsLogsModal = ({ onChange={(value) => { setHosts(value); }} - onClearAll={() => { - setHosts([]); - }} helperText={fetcher?.data?.fieldErrors?.node_ids} color={fetcher?.data?.fieldErrors?.node_ids ? 'error' : 'default'} /> diff --git a/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/ContainersTable.tsx b/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/ContainersTable.tsx index 6acee1eaf2..32f240824d 100644 --- a/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/ContainersTable.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/ContainersTable.tsx @@ -382,13 +382,6 @@ function Filters() { valueKey="hostName" scanType={ScanTypeEnum.VulnerabilityScan} defaultSelectedHosts={searchParams.getAll('hosts')} - onClearAll={() => { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/HostsTable.tsx b/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/HostsTable.tsx index cf826e95b7..4a571c8b81 100644 --- a/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/HostsTable.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/HostsTable.tsx @@ -314,13 +314,6 @@ function Filters() { { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/PodsTable.tsx b/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/PodsTable.tsx index 817de69a82..be1b63e795 100644 --- a/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/PodsTable.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/topology/data-components/tables/PodsTable.tsx @@ -220,13 +220,6 @@ function Filters() { showOnlyKubernetesHosts scanType={ScanTypeEnum.VulnerabilityScan} defaultSelectedHosts={searchParams.getAll('hosts')} - onClearAll={() => { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/RuntimeBom.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/RuntimeBom.tsx index 40d2a32cc9..33efa945e2 100644 --- a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/RuntimeBom.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/RuntimeBom.tsx @@ -264,13 +264,6 @@ const Filters = () => { { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx index f18cd9f0ba..5ca0860816 100644 --- a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/UniqueVulnerabilities.tsx @@ -215,13 +215,6 @@ const Filters = () => { { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/VulnerabilityScans.tsx b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/VulnerabilityScans.tsx index 8360c20b78..869f192ee1 100644 --- a/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/VulnerabilityScans.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/vulnerabilities/pages/VulnerabilityScans.tsx @@ -950,13 +950,6 @@ const Filters = () => { scanType={ScanTypeEnum.VulnerabilityScan} defaultSelectedHosts={searchParams.getAll('hosts')} isScannedForVulnerabilities - onClearAll={() => { - setSearchParams((prev) => { - prev.delete('hosts'); - prev.delete('page'); - return prev; - }); - }} onChange={(value) => { setSearchParams((prev) => { prev.delete('hosts'); diff --git a/deepfence_frontend/packages/ui-components/src/components/select-v2/Combobox.tsx b/deepfence_frontend/packages/ui-components/src/components/select-v2/Combobox.tsx index ce0af121a5..223cf6b496 100644 --- a/deepfence_frontend/packages/ui-components/src/components/select-v2/Combobox.tsx +++ b/deepfence_frontend/packages/ui-components/src/components/select-v2/Combobox.tsx @@ -54,9 +54,13 @@ const ComboboxProvider = ( > & { loading?: boolean; name?: string; + open?: boolean; + setOpen?: (open: boolean) => void; }, ) => { - const [open, setOpen] = useState(false); + const [_open, _setOpen] = useState(false); + const open = props.open ?? _open; + const setOpen = props.setOpen ?? _setOpen; return ( {props.children} - {!items.length ? ( + {!items.length && !loading ? (
No results found
) : null} - {loading ? ( + {!items.length && loading ? ( +
+ +
+ ) : null} + {items.length && loading ? (
- ) : ( + ) : null} + {!loading ? ( { onEndReached?.(); }} /> - )} + ) : null} {clearButtonContent ? (