From 844bca81ac4dc66a956116465ea95792369ac148 Mon Sep 17 00:00:00 2001 From: Janson Bunce Date: Wed, 28 Aug 2024 09:34:26 -0700 Subject: [PATCH 1/5] moves search bar to top of drawer and displays it on inventory route --- frontend/src/components/DrawerInterior.tsx | 16 - frontend/src/components/FilterDrawerV2.tsx | 2 + .../src/components/OrganizationSearch.tsx | 29 +- frontend/src/pages/Search/Dashboard.tsx | 24 +- frontend/src/pages/Search/FilterDrawer.tsx | 497 ------------------ frontend/src/pages/Search/FilterTags.tsx | 15 + 6 files changed, 65 insertions(+), 518 deletions(-) delete mode 100644 frontend/src/pages/Search/FilterDrawer.tsx diff --git a/frontend/src/components/DrawerInterior.tsx b/frontend/src/components/DrawerInterior.tsx index 1777496e..1b4e0fd4 100644 --- a/frontend/src/components/DrawerInterior.tsx +++ b/frontend/src/components/DrawerInterior.tsx @@ -23,7 +23,6 @@ import { FiberManualRecordRounded } from '@mui/icons-material'; import FilterAltIcon from '@mui/icons-material/FilterAlt'; -import { SearchBar } from 'components'; import { TaggedArrayInput, FacetFilter } from 'components'; import { ContextType } from '../context/SearchProvider'; import { SavedSearch } from '../types/saved-search'; @@ -59,7 +58,6 @@ export const DrawerInterior: React.FC = (props) => { removeFilter, facets, clearFilters, - searchTerm, setSearchTerm } = props; const { apiGet, apiDelete } = useAuthContext(); @@ -137,20 +135,6 @@ export const DrawerInterior: React.FC = (props) => { -
- { - if (location.pathname !== '/inventory') - history.push('/inventory?q=' + value); - setSearchTerm(value, { - shouldClearFilters: false, - autocompleteResults: false - }); - }} - /> -
{clearFilters && ( diff --git a/frontend/src/components/FilterDrawerV2.tsx b/frontend/src/components/FilterDrawerV2.tsx index abdcc9a5..8fcc9364 100644 --- a/frontend/src/components/FilterDrawerV2.tsx +++ b/frontend/src/components/FilterDrawerV2.tsx @@ -48,6 +48,8 @@ export const FilterDrawer: FC< addFilter={addFilter} removeFilter={removeFilter} filters={filters} + setSearchTerm={setSearchTerm} + searchTerm={searchTerm} /> {matchPath( ['/inventory', '/inventory/domains', '/inventory/vulnerabilities'], diff --git a/frontend/src/components/OrganizationSearch.tsx b/frontend/src/components/OrganizationSearch.tsx index 0db01414..92692b12 100644 --- a/frontend/src/components/OrganizationSearch.tsx +++ b/frontend/src/components/OrganizationSearch.tsx @@ -7,6 +7,7 @@ import { AccordionDetails, AccordionSummary, Autocomplete, + Box, Checkbox, Divider, FormControlLabel, @@ -20,6 +21,8 @@ import { ExpandMore } from '@mui/icons-material'; import { debounce } from 'utils/debounce'; import { useStaticsContext } from 'context/StaticsContext'; import { REGIONAL_USER_CAN_SEARCH_OTHER_REGIONS } from 'hooks/useUserTypeFilters'; +import { SearchBar } from './SearchBar'; +import { useHistory, useLocation } from 'react-router-dom'; const GLOBAL_ADMIN = 3; const REGIONAL_ADMIN = 2; @@ -50,12 +53,16 @@ interface OrganizationSearchProps { filterType: 'all' | 'any' | 'none' ) => void; filters: any[]; + setSearchTerm: (s: string, opts?: any) => void; + searchTerm: string; } export const OrganizationSearch: React.FC = ({ addFilter, removeFilter, - filters + filters, + searchTerm: domainSearchTerm, + setSearchTerm: setDomainSearchTerm }) => { const { setShowMaps, user, apiPost } = useAuthContext(); @@ -167,10 +174,30 @@ export const OrganizationSearch: React.FC = ({ }, [regionFilterValues] ); + const history = useHistory(); + const location = useLocation(); return ( <> + + { + if (location.pathname !== '/inventory') { + history.push(`/inventory?q=${value}`); + setDomainSearchTerm(value, { + shouldClearFilters: false, + refresh: true + }); + } + setDomainSearchTerm(value, { + shouldClearFilters: false + }); + }} + /> + = ( noResults } = props; + console.log('search term', searchTerm); + const [selectedDomain, setSelectedDomain] = useState(''); const [resultsScrolled] = useState(false); const { @@ -114,11 +116,9 @@ export const DashboardUI: React.FC = ( useEffect(() => { if (props.location.search === '') { // Search on initial load - setSearchTerm('', { shouldClearFilters: false }); } return () => { localStorage.removeItem('savedSearch'); - setSearchTerm('', { shouldClearFilters: false }); }; }, [setSearchTerm, props.location.search]); @@ -151,6 +151,22 @@ export const DashboardUI: React.FC = ( } }; + const filtersToDisplay = useMemo(() => { + if (searchTerm !== '') { + return [ + ...filters, + { + field: 'query', + values: [searchTerm], + onClear: () => setSearchTerm('', { shouldClearFilters: false }) + } + ]; + } + return filters; + }, [filters, searchTerm, setSearchTerm]); + + console.log(filtersToDisplay); + return ( = ( justifyContent="center" > - + void; - searchTerm: ContextType['searchTerm']; - setSearchTerm: ContextType['setSearchTerm']; -} - -const FiltersApplied: React.FC = () => { - return ( -
- Filters Applied -
- ); -}; - -const Accordion = MuiAccordion; -const AccordionSummary = MuiAccordionSummary; - -export const FilterDrawer: React.FC = (props) => { - const { - filters, - addFilter, - removeFilter, - facets, - clearFilters, - searchTerm, - setSearchTerm - } = props; - const { apiGet, apiDelete } = useAuthContext(); - const [savedSearches, setSavedSearches] = useState([]); - const [savedSearchCount, setSavedSearchCount] = useState(0); - const history = useHistory(); - const location = useLocation(); - - useEffect(() => { - const fetchSearches = async () => { - try { - const response = await apiGet('/saved-searches'); - setSavedSearches(response.result); - setSavedSearchCount(response.result.length); - } catch (error) { - console.error('Error fetching searches:', error); - } - }; - fetchSearches(); - }, [apiGet]); - - const deleteSearch = async (id: string) => { - try { - await apiDelete(`/saved-searches/${id}`, { body: {} }); - const updatedSearches = await apiGet('/saved-searches'); // Get current saved searches - setSavedSearches(updatedSearches.result); // Update the saved searches - setSavedSearchCount(updatedSearches.result.length); // Update the count - } catch (e) { - console.log(e); - } - }; - - const filtersByColumn = useMemo( - () => - filters.reduce( - (allFilters, nextFilter) => ({ - ...allFilters, - [nextFilter.field]: nextFilter.values - }), - {} as Record - ), - [filters] - ); - - const portFacet: any[] = facets['services.port'] - ? facets['services.port'][0].data - : []; - - const fromDomainFacet: any[] = facets['fromRootDomain'] - ? facets['fromRootDomain'][0].data - : []; - - const cveFacet: any[] = facets['vulnerabilities.cve'] - ? facets['vulnerabilities.cve'][0].data - : []; - - const severityFacet: any[] = facets['vulnerabilities.severity'] - ? facets['vulnerabilities.severity'][0].data - : []; - - // Always show all severities - for (const value of ['Critical', 'High', 'Medium', 'Low']) { - if (!severityFacet.find((severity) => value === severity.value)) - severityFacet.push({ value, count: 0 }); - } - - return ( - -
- { - if (location.pathname !== '/inventory') - history.push('/inventory?q=' + value); - setSearchTerm(value, { - shouldClearFilters: false, - autocompleteResults: false - }); - }} - /> -
-
-
-

Filter

-
- {clearFilters && ( -
- -
- )} -
- - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
IP(s)
- {filtersByColumn['ip']?.length > 0 && } -
- - addFilter('ip', value, 'any')} - onRemoveTag={(value) => removeFilter('ip', value, 'any')} - /> - -
- - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
Domain(s)
- {filtersByColumn['name']?.length > 0 && } -
- - addFilter('name', value, 'any')} - onRemoveTag={(value) => removeFilter('name', value, 'any')} - /> - -
- {fromDomainFacet.length > 0 && ( - - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
Root Domain(s)
- {filtersByColumn['fromRootDomain']?.length > 0 && ( - - )} -
- - addFilter('fromRootDomain', value, 'any')} - onDeselect={(value) => - removeFilter('fromRootDomain', value, 'any') - } - /> - -
- )} - {portFacet.length > 0 && ( - - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
Port(s)
- {filtersByColumn['services.port']?.length > 0 && } -
- - addFilter('services.port', value, 'any')} - onDeselect={(value) => - removeFilter('services.port', value, 'any') - } - /> - -
- )} - {cveFacet.length > 0 && ( - - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
CVE(s)
- {filtersByColumn['vulnerabilities.cve']?.length > 0 && ( - - )} -
- - - addFilter('vulnerabilities.cve', value, 'any') - } - onDeselect={(value) => - removeFilter('vulnerabilities.cve', value, 'any') - } - /> - -
- )} - {severityFacet.length > 0 && ( - - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
Severity
- {filtersByColumn['vulnerabilities.severity']?.length > 0 && ( - - )} -
- - - addFilter('vulnerabilities.severity', value, 'any') - } - onDeselect={(value) => - removeFilter('vulnerabilities.severity', value, 'any') - } - /> - -
- )} - - } - classes={{ - root: classes.root2, - content: classes.content, - disabled: classes.disabled2, - expanded: classes.expanded2 - }} - > -
-

Saved Searches

-
-
- - - - {savedSearches.length > 0 ? ( - ({ ...search }))} - rowCount={savedSearchCount} - columns={[ - { - field: 'name', - headerName: 'Name', - flex: 1, - width: 100, - description: 'Name', - renderCell: (cellValues) => { - const applyFilter = () => { - if (clearFilters) clearFilters(); - localStorage.setItem( - 'savedSearch', - JSON.stringify(cellValues.row) - ); - setSearchTerm(cellValues.row.searchTerm, { - shouldClearFilters: false, - autocompleteResults: false - }); - if (location.pathname !== '/inventory') - history.push( - '/inventory?q=' + cellValues.row.searchTerm - ); - - // Apply the filters - cellValues.row.filters.forEach((filter) => { - filter.values.forEach((value) => { - addFilter(filter.field, value, 'any'); - }); - }); - }; - return ( -
{ - if (e.key === 'Enter') { - applyFilter(); - } - }} - style={{ - cursor: 'pointer', - textAlign: 'left', - width: '100%' - }} - > - {cellValues.value} -
- ); - } - }, - { - field: 'actions', - headerName: '', - flex: 0.1, - renderCell: (cellValues) => { - const searchId = cellValues.id.toString(); - return ( -
- { - e.stopPropagation(); - deleteSearch(searchId); - }} - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter') { - deleteSearch(searchId); - } - }} - > - - -
- ); - } - } - ]} - initialState={{ - pagination: { - paginationModel: { - pageSize: 5 - } - } - }} - pageSizeOptions={[5, 10]} - disableRowSelectionOnClick - sx={{ - disableColumnfilter: 'true', - '& .MuiDataGrid-row:hover': { - cursor: 'pointer' - } - }} - /> - ) : ( -
No Saved Searches
- )} -
-
-
-
-
- ); -}; - -export const FilterDrawerWithSearch = withSearch( - ({ searchTerm, setSearchTerm }: ContextType) => ({ - searchTerm, - setSearchTerm - }) -)(FilterDrawer); diff --git a/frontend/src/pages/Search/FilterTags.tsx b/frontend/src/pages/Search/FilterTags.tsx index a27eb03e..820db849 100644 --- a/frontend/src/pages/Search/FilterTags.tsx +++ b/frontend/src/pages/Search/FilterTags.tsx @@ -15,6 +15,7 @@ interface FieldToLabelMap { [key: string]: { labelAccessor: (t: any) => any; filterValueAccssor: (t: any) => any; + overrideRemove?: (t: any) => void; }; } @@ -35,6 +36,14 @@ const FIELD_TO_LABEL_MAP: FieldToLabelMap = { return t.name; } }, + query: { + labelAccessor: (t) => { + return 'Query'; + }, + filterValueAccssor(t) { + return t; + } + }, 'services.port': { labelAccessor: (t) => { return 'Port'; @@ -56,6 +65,7 @@ const FIELD_TO_LABEL_MAP: FieldToLabelMap = { type FlatFilters = { field: string; label: string; + onClear?: () => void; value: any; values: any[]; type: 'all' | 'none' | 'any'; @@ -113,6 +123,11 @@ export const FilterTags: React.FC = ({ filters, removeFilter }) => { } onDelete={() => { + if (filter.onClear) { + console.log('custom clear'); + filter.onClear(); + return; + } filter.values.forEach((val) => { removeFilter(filter.field, val, filter.type); }); From 06ead077eecca9a7b12c4a1d9c2fdd6a0bcf1193 Mon Sep 17 00:00:00 2001 From: Janson Bunce Date: Wed, 28 Aug 2024 09:51:41 -0700 Subject: [PATCH 2/5] resolve failing test --- .../__snapshots__/layout.spec.tsx.snap | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap b/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap index aa6aed47..ec1c2de5 100644 --- a/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap +++ b/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap @@ -72,6 +72,36 @@ exports[`Layout component matches snapshot 1`] = `
+
+
+
+ +
+ +
+
+
+
From 3ad4c7df8d16971374aa11f3057bf7107ec50617 Mon Sep 17 00:00:00 2001 From: Janson Bunce Date: Wed, 28 Aug 2024 10:17:25 -0700 Subject: [PATCH 3/5] add filter label accessors for remainder of filters to ensuer consistent labeling --- frontend/src/pages/Search/FilterTags.tsx | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/frontend/src/pages/Search/FilterTags.tsx b/frontend/src/pages/Search/FilterTags.tsx index 820db849..05f4c605 100644 --- a/frontend/src/pages/Search/FilterTags.tsx +++ b/frontend/src/pages/Search/FilterTags.tsx @@ -28,6 +28,38 @@ const FIELD_TO_LABEL_MAP: FieldToLabelMap = { return t; } }, + 'vulnerabilities.severity': { + labelAccessor: (t) => { + return 'Severity'; + }, + filterValueAccssor(t) { + return t; + } + }, + ip: { + labelAccessor: (t) => { + return 'IP'; + }, + filterValueAccssor(t) { + return t; + } + }, + name: { + labelAccessor: (t) => { + return 'Name'; + }, + filterValueAccssor(t) { + return t; + } + }, + fromRootDomain: { + labelAccessor: (t) => { + return 'Root Domain(s)'; + }, + filterValueAccssor(t) { + return t; + } + }, organizationId: { labelAccessor: (t) => { return 'Organization'; From 5d80a1058f0a4f5204a41005a42c6dbd49dd67b6 Mon Sep 17 00:00:00 2001 From: Janson Bunce Date: Wed, 28 Aug 2024 10:17:38 -0700 Subject: [PATCH 4/5] add filter label accessors for remainder of filters to ensuer consistent labeling --- frontend/src/pages/Search/Dashboard.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/Search/Dashboard.tsx b/frontend/src/pages/Search/Dashboard.tsx index c4defdc7..3b414ac1 100644 --- a/frontend/src/pages/Search/Dashboard.tsx +++ b/frontend/src/pages/Search/Dashboard.tsx @@ -59,8 +59,6 @@ export const DashboardUI: React.FC = ( noResults } = props; - console.log('search term', searchTerm); - const [selectedDomain, setSelectedDomain] = useState(''); const [resultsScrolled] = useState(false); const { From e4d42293148aec449f67f36c85be27804acca948 Mon Sep 17 00:00:00 2001 From: Janson Bunce Date: Thu, 5 Sep 2024 14:45:10 -0700 Subject: [PATCH 5/5] fix save search button --- frontend/src/pages/Search/Inventory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Search/Inventory.tsx b/frontend/src/pages/Search/Inventory.tsx index f50412c0..53eaf28b 100644 --- a/frontend/src/pages/Search/Inventory.tsx +++ b/frontend/src/pages/Search/Inventory.tsx @@ -73,7 +73,7 @@ export const DashboardUI: React.FC = ( // Could be used for validation purposes in new dialogue // const { savedSearches } = useSavedSearchContext(); - const advanceFiltersReq = filters.length > 1; //Prevents a user from saving a search without advanced filters + const advanceFiltersReq = filters.length > 1 || searchTerm !== ''; //Prevents a user from saving a search without advanced filters const search: | (SavedSearch & {