From 4761847ce527b3b2bb6caee065dd85d363cf0b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 10 Jun 2022 14:23:12 +0100 Subject: [PATCH] feat: upgrade search to use the new search component (#1073) * feat: upgrade project list search to use the new search field * cleanup unused imports * feat: add upgraded search to projects and applications, polish search UX * refactor: TableSearch to new Search common component Co-authored-by: Fredrik Strand Oseberg --- .../Reporting/ReportTable/ReportTable.tsx | 4 +- .../apiToken/ApiTokenTable/ApiTokenTable.tsx | 4 +- .../ProjectRoleList/ProjectRoleList.tsx | 4 +- .../admin/users/UsersList/UsersList.tsx | 4 +- .../ApplicationList/ApplicationList.tsx | 46 ++++-- .../component/common/Search/Search.styles.ts | 46 ++++++ .../src/component/common/Search/Search.tsx | 115 ++++++++++++++ .../SearchDescription/SearchDescription.tsx | 72 +++++++++ .../SearchInstructions/SearchInstructions.tsx | 62 ++++++++ .../SearchSuggestions/SearchSuggestions.tsx | 150 ++++++++++++++++++ .../common/SearchField/SearchField.tsx | 3 + .../TablePlaceholder.styles.ts | 1 + .../common/Table/TableSearch/TableSearch.tsx | 3 + .../TableSearchField/TableSearchField.tsx | 13 +- .../context/ContextList/ContextList.tsx | 4 +- .../EnvironmentTable/EnvironmentTable.tsx | 4 +- .../FeatureToggleListTable.tsx | 6 +- .../ProjectFeatureToggles.tsx | 10 +- .../project/ProjectList/ProjectList.styles.ts | 16 -- .../project/ProjectList/ProjectList.tsx | 108 +++++++++---- .../segments/SegmentTable/SegmentTable.tsx | 4 +- .../StrategiesList/StrategiesList.tsx | 4 +- .../tags/TagTypeList/TagTypeList.tsx | 4 +- 23 files changed, 602 insertions(+), 85 deletions(-) create mode 100644 frontend/src/component/common/Search/Search.styles.ts create mode 100644 frontend/src/component/common/Search/Search.tsx create mode 100644 frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx create mode 100644 frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx create mode 100644 frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx diff --git a/frontend/src/component/Reporting/ReportTable/ReportTable.tsx b/frontend/src/component/Reporting/ReportTable/ReportTable.tsx index 80d8156b79..e692454fb2 100644 --- a/frontend/src/component/Reporting/ReportTable/ReportTable.tsx +++ b/frontend/src/component/Reporting/ReportTable/ReportTable.tsx @@ -1,6 +1,5 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle'; import { - TableSearch, SortableTableHeader, TableCell, TablePlaceholder, @@ -26,6 +25,7 @@ import { formatExpiredAt } from 'component/Reporting/ReportExpiredCell/formatExp import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; import theme from 'themes/theme'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Search } from 'component/common/Search/Search'; interface IReportTableProps { projectId: string; @@ -95,7 +95,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx index fa88bb217c..90801c8197 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx @@ -3,7 +3,6 @@ import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import { PageContent } from 'component/common/PageContent/PageContent'; import { SortableTableHeader, - TableSearch, TableCell, TablePlaceholder, } from 'component/common/Table'; @@ -25,6 +24,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; +import { Search } from 'component/common/Search/Search'; export const ApiTokenTable = () => { const { tokens, loading } = useApiTokens(); @@ -57,7 +57,7 @@ export const ApiTokenTable = () => { }, [setHiddenColumns, hiddenColumns]); const headerSearch = ( - + ); const headerActions = ( diff --git a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx index 6362ae0cf3..bf088cdb8d 100644 --- a/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx @@ -6,7 +6,6 @@ import { TableCell, TableRow, TablePlaceholder, - TableSearch, } from 'component/common/Table'; import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import { ADMIN } from 'component/providers/AccessProvider/permissions'; @@ -28,6 +27,7 @@ import { sortTypes } from 'utils/sortTypes'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import theme from 'themes/theme'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; +import { Search } from 'component/common/Search/Search'; const ROOTROLE = 'root'; const BUILTIN_ROLE_TYPE = 'project'; @@ -190,7 +190,7 @@ const ProjectRoleList = () => { title="Project roles" actions={ <> - diff --git a/frontend/src/component/admin/users/UsersList/UsersList.tsx b/frontend/src/component/admin/users/UsersList/UsersList.tsx index 6679524bcd..5c658955cd 100644 --- a/frontend/src/component/admin/users/UsersList/UsersList.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersList.tsx @@ -7,7 +7,6 @@ import { TableCell, TableRow, TablePlaceholder, - TableSearch, } from 'component/common/Table'; import ChangePassword from './ChangePassword/ChangePassword'; import DeleteUser from './DeleteUser/DeleteUser'; @@ -34,6 +33,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import theme from 'themes/theme'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell'; +import { Search } from 'component/common/Search/Search'; const StyledAvatar = styled(Avatar)(({ theme }) => ({ width: theme.spacing(4), @@ -248,7 +248,7 @@ const UsersList = () => { title="Users" actions={ <> - diff --git a/frontend/src/component/application/ApplicationList/ApplicationList.tsx b/frontend/src/component/application/ApplicationList/ApplicationList.tsx index 48d5ef654d..983a9aaf09 100644 --- a/frontend/src/component/application/ApplicationList/ApplicationList.tsx +++ b/frontend/src/component/application/ApplicationList/ApplicationList.tsx @@ -1,23 +1,40 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { CircularProgress } from '@mui/material'; import { Warning } from '@mui/icons-material'; import { AppsLinkList, styles as themeStyles } from 'component/common'; -import { SearchField } from 'component/common/SearchField/SearchField'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import useApplications from 'hooks/api/getters/useApplications/useApplications'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useSearchParams } from 'react-router-dom'; +import { Search } from 'component/common/Search/Search'; + +type PageQueryType = Partial>; export const ApplicationList = () => { const { applications, loading } = useApplications(); - const [filter, setFilter] = useState(''); + const [searchParams, setSearchParams] = useSearchParams(); + const [searchValue, setSearchValue] = useState( + searchParams.get('search') || '' + ); + + useEffect(() => { + const tableState: PageQueryType = {}; + if (searchValue) { + tableState.search = searchValue; + } + + setSearchParams(tableState, { + replace: true, + }); + }, [searchValue, setSearchParams]); const filteredApplications = useMemo(() => { - const regExp = new RegExp(filter, 'i'); - return filter + const regExp = new RegExp(searchValue, 'i'); + return searchValue ? applications?.filter(a => regExp.test(a.appName)) : applications; - }, [applications, filter]); + }, [applications, searchValue]); const renderNoApplications = () => ( <> @@ -44,10 +61,19 @@ export const ApplicationList = () => { return ( <> -
- -
- }> + + } + /> + } + >
0} diff --git a/frontend/src/component/common/Search/Search.styles.ts b/frontend/src/component/common/Search/Search.styles.ts new file mode 100644 index 0000000000..59e56e1eaa --- /dev/null +++ b/frontend/src/component/common/Search/Search.styles.ts @@ -0,0 +1,46 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + container: { + display: 'flex', + flexGrow: 1, + alignItems: 'center', + position: 'relative', + maxWidth: '400px', + [theme.breakpoints.down('md')]: { + marginTop: theme.spacing(1), + maxWidth: '100%', + }, + }, + search: { + display: 'flex', + alignItems: 'center', + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.grey[300]}`, + borderRadius: theme.shape.borderRadiusExtraLarge, + padding: '3px 5px 3px 12px', + width: '100%', + zIndex: 3, + '&.search-container:focus-within': { + borderColor: theme.palette.primary.light, + boxShadow: theme.boxShadows.main, + }, + }, + searchIcon: { + marginRight: 8, + color: theme.palette.inactiveIcon, + }, + clearContainer: { + width: '30px', + '& > button': { + padding: '7px', + }, + }, + clearIcon: { + color: theme.palette.grey[700], + fontSize: '18px', + }, + inputRoot: { + width: '100%', + }, +})); diff --git a/frontend/src/component/common/Search/Search.tsx b/frontend/src/component/common/Search/Search.tsx new file mode 100644 index 0000000000..2d7247b46c --- /dev/null +++ b/frontend/src/component/common/Search/Search.tsx @@ -0,0 +1,115 @@ +import { useRef, useState } from 'react'; +import { IconButton, InputBase, Tooltip } from '@mui/material'; +import { Search as SearchIcon, Close } from '@mui/icons-material'; +import classnames from 'classnames'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useStyles } from './Search.styles'; +import { SearchSuggestions } from './SearchSuggestions/SearchSuggestions'; +import { IGetSearchContextOutput } from 'hooks/useSearch'; +import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut'; +import { useAsyncDebounce } from 'react-table'; + +interface ISearchProps { + initialValue?: string; + onChange: (value: string) => void; + className?: string; + placeholder?: string; + hasFilters?: boolean; + getSearchContext?: () => IGetSearchContextOutput; +} + +export const Search = ({ + initialValue = '', + onChange, + className, + placeholder: customPlaceholder, + hasFilters, + getSearchContext, +}: ISearchProps) => { + const ref = useRef(); + const { classes: styles } = useStyles(); + const [showSuggestions, setShowSuggestions] = useState(false); + + const [value, setValue] = useState(initialValue); + + const debouncedOnChange = useAsyncDebounce(onChange, 200); + + const onSearchChange = (value: string) => { + debouncedOnChange(value); + setValue(value); + }; + + const hotkey = useKeyboardShortcut( + { modifiers: ['ctrl'], key: 'k', preventDefault: true }, + () => { + if (document.activeElement === ref.current) { + ref.current?.blur(); + } else { + ref.current?.focus(); + } + } + ); + useKeyboardShortcut({ key: 'Escape' }, () => { + if (document.activeElement === ref.current) { + ref.current?.blur(); + } + }); + const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; + + return ( +
+
+ + onSearchChange(e.target.value)} + onFocus={() => setShowSuggestions(true)} + onBlur={() => setShowSuggestions(false)} + /> +
+ + { + onChange(''); + ref.current?.focus(); + }} + > + + + + } + /> +
+
+ + } + /> +
+ ); +}; diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx new file mode 100644 index 0000000000..66396db282 --- /dev/null +++ b/frontend/src/component/common/Search/SearchSuggestions/SearchDescription/SearchDescription.tsx @@ -0,0 +1,72 @@ +import { styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { + getSearchTextGenerator, + IGetSearchContextOutput, +} from 'hooks/useSearch'; +import { VFC } from 'react'; + +const StyledHeader = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.primary, +})); + +const StyledCode = styled('span')(({ theme }) => ({ + backgroundColor: theme.palette.secondaryContainer, + color: theme.palette.text.primary, + padding: theme.spacing(0, 0.5), + borderRadius: theme.spacing(0.5), +})); + +interface ISearchDescriptionProps { + filters: any[]; + getSearchContext: () => IGetSearchContextOutput; + searchableColumnsString: string; +} + +export const SearchDescription: VFC = ({ + filters, + getSearchContext, + searchableColumnsString, +}) => { + const searchContext = getSearchContext(); + const getSearchText = getSearchTextGenerator(searchContext.columns); + const searchText = getSearchText(searchContext.searchValue); + const searchFilters = filters.filter(filter => filter.values.length > 0); + + return ( + <> + + Searching for: +

+ {searchText}{' '} + {searchableColumnsString + ? ` in ${searchableColumnsString}` + : ''} +

+ + } + /> + 0} + show={ + <> + Filtering by: + {searchFilters.map(filter => ( +

+ + {filter.values.join(',')} + {' '} + in {filter.header}. Options:{' '} + {filter.options.join(', ')} +

+ ))} + + } + /> + + ); +}; diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx new file mode 100644 index 0000000000..bfaf3a557e --- /dev/null +++ b/frontend/src/component/common/Search/SearchSuggestions/SearchInstructions/SearchInstructions.tsx @@ -0,0 +1,62 @@ +import { styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { IGetSearchContextOutput } from 'hooks/useSearch'; +import { VFC } from 'react'; + +const StyledHeader = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.primary, +})); + +const StyledCode = styled('span')(({ theme }) => ({ + backgroundColor: theme.palette.secondaryContainer, + color: theme.palette.text.primary, + padding: theme.spacing(0, 0.5), + borderRadius: theme.spacing(0.5), +})); + +interface ISearchInstructionsProps { + filters: any[]; + getSearchContext: () => IGetSearchContextOutput; + searchableColumnsString: string; +} + +export const SearchInstructions: VFC = ({ + filters, + getSearchContext, + searchableColumnsString, +}) => { + return ( + <> + + {filters.length > 0 + ? 'Filter your search with operators like:' + : `Start typing to search${ + searchableColumnsString + ? ` in ${searchableColumnsString}` + : '...' + }`} + + {filters.map(filter => ( +

+ Filter by {filter.header}:{' '} + + {filter.name}:{filter.options[0]} + + 1} + show={ + <> + {' or '} + + {filter.name}: + {filter.options.slice(0, 2).join(',')} + + + } + /> +

+ ))} + + ); +}; diff --git a/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx new file mode 100644 index 0000000000..3ba355c9df --- /dev/null +++ b/frontend/src/component/common/Search/SearchSuggestions/SearchSuggestions.tsx @@ -0,0 +1,150 @@ +import { FilterList } from '@mui/icons-material'; +import { Box, Divider, Paper, styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { + getColumnValues, + getFilterableColumns, + getFilterValues, + IGetSearchContextOutput, +} from 'hooks/useSearch'; +import { useMemo, VFC } from 'react'; +import { SearchDescription } from './SearchDescription/SearchDescription'; +import { SearchInstructions } from './SearchInstructions/SearchInstructions'; + +const randomIndex = (arr: any[]) => Math.floor(Math.random() * arr.length); + +const StyledPaper = styled(Paper)(({ theme }) => ({ + position: 'absolute', + width: '100%', + left: 0, + top: '20px', + zIndex: 2, + padding: theme.spacing(4, 1.5, 1.5), + borderBottomLeftRadius: theme.spacing(1), + borderBottomRightRadius: theme.spacing(1), + boxShadow: '0px 8px 20px rgba(33, 33, 33, 0.15)', + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.secondary, + wordBreak: 'break-word', +})); + +const StyledBox = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(2), +})); + +const StyledFilterList = styled(FilterList)(({ theme }) => ({ + color: theme.palette.text.secondary, +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + border: `1px dashed ${theme.palette.dividerAlternative}`, + margin: theme.spacing(1.5, 0), +})); + +const StyledCode = styled('span')(({ theme }) => ({ + backgroundColor: theme.palette.secondaryContainer, + color: theme.palette.text.primary, + padding: theme.spacing(0, 0.5), + borderRadius: theme.spacing(0.5), +})); + +interface SearchSuggestionsProps { + getSearchContext: () => IGetSearchContextOutput; +} + +export const SearchSuggestions: VFC = ({ + getSearchContext, +}) => { + const searchContext = getSearchContext(); + + const randomRow = useMemo( + () => randomIndex(searchContext.data), + [searchContext.data] + ); + + const filters = getFilterableColumns(searchContext.columns) + .map(column => { + const filterOptions = searchContext.data.map(row => + getColumnValues(column, row) + ); + + return { + name: column.filterName, + header: column.Header ?? column.filterName, + options: [...new Set(filterOptions)].sort((a, b) => + a.localeCompare(b) + ), + suggestedOption: + filterOptions[randomRow] ?? `example-${column.filterName}`, + values: getFilterValues( + column.filterName, + searchContext.searchValue + ), + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + + const searchableColumns = searchContext.columns.filter( + column => column.searchable && column.accessor + ); + + const searchableColumnsString = searchableColumns + .map(column => column.Header ?? column.accessor) + .join(', '); + + const suggestedTextSearch = + searchContext.data.length && searchableColumns.length + ? getColumnValues( + searchableColumns[0], + searchContext.data[randomRow] + ) + : 'example-search-text'; + + return ( + + + + + + } + elseShow={ + + } + /> + + + + 0} + show="Combine filters and search." + /> +

+ Example:{' '} + + {filters.map(filter => ( + + {filter.name}:{filter.suggestedOption}{' '} + + ))} + {suggestedTextSearch} + +

+
+ ); +}; diff --git a/frontend/src/component/common/SearchField/SearchField.tsx b/frontend/src/component/common/SearchField/SearchField.tsx index 5b99d99f77..ac0798d6a7 100644 --- a/frontend/src/component/common/SearchField/SearchField.tsx +++ b/frontend/src/component/common/SearchField/SearchField.tsx @@ -13,6 +13,9 @@ interface ISearchFieldProps { showValueChip?: boolean; } +/** + * @deprecated use `Search` instead. + */ export const SearchField: VFC = ({ updateValue, initialValue = '', diff --git a/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts b/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts index 87eb009842..65c3de6384 100644 --- a/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts +++ b/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts @@ -9,5 +9,6 @@ export const useStyles = makeStyles()(theme => ({ justifyContent: 'center', alignItems: 'center', marginTop: theme.spacing(2), + width: '100%', }, })); diff --git a/frontend/src/component/common/Table/TableSearch/TableSearch.tsx b/frontend/src/component/common/Table/TableSearch/TableSearch.tsx index d47faef67b..3fe2969511 100644 --- a/frontend/src/component/common/Table/TableSearch/TableSearch.tsx +++ b/frontend/src/component/common/Table/TableSearch/TableSearch.tsx @@ -11,6 +11,9 @@ interface ITableSearchProps { getSearchContext?: () => IGetSearchContextOutput; } +/** + * @deprecated use `Search` instead. + */ export const TableSearch: FC = ({ initialValue, onChange = () => {}, diff --git a/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx b/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx index bfe6658e3d..31e992d011 100644 --- a/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx +++ b/frontend/src/component/common/Table/TableSearch/TableSearchField/TableSearchField.tsx @@ -17,6 +17,9 @@ interface ITableSearchFieldProps { getSearchContext?: () => IGetSearchContextOutput; } +/** + * @deprecated use `Search` instead. + */ export const TableSearchField = ({ value = '', onChange, @@ -28,16 +31,20 @@ export const TableSearchField = ({ const ref = useRef(); const { classes: styles } = useStyles(); const [showSuggestions, setShowSuggestions] = useState(false); + const hotkey = useKeyboardShortcut( { modifiers: ['ctrl'], key: 'k', preventDefault: true }, () => { - ref.current?.focus(); - setShowSuggestions(true); + if (document.activeElement === ref.current) { + ref.current?.blur(); + } else { + ref.current?.focus(); + } } ); useKeyboardShortcut({ key: 'Escape' }, () => { if (document.activeElement === ref.current) { - setShowSuggestions(suggestions => !suggestions); + ref.current?.blur(); } }); const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; diff --git a/frontend/src/component/context/ContextList/ContextList.tsx b/frontend/src/component/context/ContextList/ContextList.tsx index c90633f327..7a263e6b21 100644 --- a/frontend/src/component/context/ContextList/ContextList.tsx +++ b/frontend/src/component/context/ContextList/ContextList.tsx @@ -7,7 +7,6 @@ import { TableCell, TableRow, TablePlaceholder, - TableSearch, } from 'component/common/Table'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -24,6 +23,7 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell'; import { Adjust } from '@mui/icons-material'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; +import { Search } from 'component/common/Search/Search'; const ContextList: VFC = () => { const [showDelDialogue, setShowDelDialogue] = useState(false); @@ -164,7 +164,7 @@ const ContextList: VFC = () => { title="Context fields" actions={ <> - diff --git a/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx b/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx index ec10b1daba..e58eb81189 100644 --- a/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx +++ b/frontend/src/component/environments/EnvironmentTable/EnvironmentTable.tsx @@ -4,7 +4,6 @@ import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironmen import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton'; import { useTable, useGlobalFilter } from 'react-table'; import { - TableSearch, SortableTableHeader, Table, TablePlaceholder, @@ -24,6 +23,7 @@ import useEnvironmentApi, { } from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import { formatUnknownError } from 'utils/formatUnknownError'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Search } from 'component/common/Search/Search'; const StyledAlert = styled(Alert)(({ theme }) => ({ marginBottom: theme.spacing(4), @@ -71,7 +71,7 @@ export const EnvironmentTable = () => { ); const headerSearch = ( - + ); const headerActions = ( diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 578d4f1b63..cb156e04be 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -9,7 +9,6 @@ import { TableCell, TableRow, TablePlaceholder, - TableSearch, } from 'component/common/Table'; import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; @@ -29,6 +28,7 @@ import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton' import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; import { useStyles } from './styles'; import { useSearch } from 'hooks/useSearch'; +import { Search } from 'component/common/Search/Search'; const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ name: 'Name of the feature', @@ -208,7 +208,7 @@ export const FeatureToggleListTable: VFC = () => { condition={!isSmallScreen} show={ <> - { - setSearchValue(value) - } + onChange={setSearchValue} hasFilters getSearchContext={getSearchContext} /> @@ -454,7 +452,7 @@ export const ProjectFeatureToggles = ({ ({ fontFamily: theme.typography.fontFamily, pointer: 'cursor', }, - searchBarContainer: { - marginBottom: '2rem', - display: 'flex', - gap: '1rem', - justifyContent: 'space-between', - alignItems: 'center', - [theme.breakpoints.down('sm')]: { - display: 'block', - }, - }, - searchBar: { - minWidth: 450, - [theme.breakpoints.down('sm')]: { - minWidth: '100%', - }, - }, })); diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index 69ea110e4a..f635212353 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -1,5 +1,5 @@ -import { useContext, useMemo, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { useContext, useEffect, useMemo, useState } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { mutate } from 'swr'; import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; @@ -17,8 +17,12 @@ import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { Add } from '@mui/icons-material'; import ApiError from 'component/common/ApiError/ApiError'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { SearchField } from 'component/common/SearchField/SearchField'; -import classnames from 'classnames'; +import { TablePlaceholder } from 'component/common/Table'; +import { useMediaQuery } from '@mui/material'; +import theme from 'themes/theme'; +import { Search } from 'component/common/Search/Search'; + +type PageQueryType = Partial>; type projectMap = { [index: string]: boolean; @@ -51,14 +55,30 @@ export const ProjectListNew = () => { const [fetchedProjects, setFetchedProjects] = useState({}); const ref = useLoading(loading); const { isOss } = useUiConfig(); - const [filter, setFilter] = useState(''); + + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); + const [searchParams, setSearchParams] = useSearchParams(); + const [searchValue, setSearchValue] = useState( + searchParams.get('search') || '' + ); + + useEffect(() => { + const tableState: PageQueryType = {}; + if (searchValue) { + tableState.search = searchValue; + } + + setSearchParams(tableState, { + replace: true, + }); + }, [searchValue, setSearchParams]); const filteredProjects = useMemo(() => { - const regExp = new RegExp(filter, 'i'); - return filter + const regExp = new RegExp(searchValue, 'i'); + return searchValue ? projects.filter(project => regExp.test(project.name)) : projects; - }, [projects, filter]); + }, [projects, searchValue]); const handleHover = (projectId: string) => { if (fetchedProjects[projectId]) { @@ -129,39 +149,69 @@ export const ProjectListNew = () => { return (
-
- -
navigate('/projects/create')} - maxWidth="700px" - permission={CREATE_PROJECT} - disabled={createButtonData.disabled} - > - New project - + <> + + + + + } + /> + navigate('/projects/create')} + maxWidth="700px" + permission={CREATE_PROJECT} + disabled={createButtonData.disabled} + > + New project + + } - /> + > + + } + /> + } >
No projects available.
} + show={ + 0} + show={ + + No projects found matching “ + {searchValue} + ” + + } + elseShow={ + + No projects available. + + } + /> + } elseShow={renderProjects()} />
diff --git a/frontend/src/component/segments/SegmentTable/SegmentTable.tsx b/frontend/src/component/segments/SegmentTable/SegmentTable.tsx index 246cb01019..e6e0b5eb38 100644 --- a/frontend/src/component/segments/SegmentTable/SegmentTable.tsx +++ b/frontend/src/component/segments/SegmentTable/SegmentTable.tsx @@ -1,7 +1,6 @@ import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { - TableSearch, SortableTableHeader, TableCell, TablePlaceholder, @@ -25,6 +24,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import theme from 'themes/theme'; import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Search } from 'component/common/Search/Search'; export const SegmentTable = () => { const { segments, loading } = useSegments(); @@ -87,7 +87,7 @@ export const SegmentTable = () => { title="Segments" actions={ <> - diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx index 150124bdf9..cad1b6eeeb 100644 --- a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx +++ b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx @@ -19,7 +19,6 @@ import { TableCell, TableRow, TablePlaceholder, - TableSearch, } from 'component/common/Table'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; @@ -38,6 +37,7 @@ import { sortTypes } from 'utils/sortTypes'; import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton'; import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; +import { Search } from 'component/common/Search/Search'; interface IDialogueMetaData { show: boolean; @@ -357,7 +357,7 @@ export const StrategiesList = () => { title="Strategies" actions={ <> - diff --git a/frontend/src/component/tags/TagTypeList/TagTypeList.tsx b/frontend/src/component/tags/TagTypeList/TagTypeList.tsx index 5bd880b27d..c468360913 100644 --- a/frontend/src/component/tags/TagTypeList/TagTypeList.tsx +++ b/frontend/src/component/tags/TagTypeList/TagTypeList.tsx @@ -8,7 +8,6 @@ import { TableCell, TableRow, TablePlaceholder, - TableSearch, } from 'component/common/Table'; import { Delete, Edit, Label } from '@mui/icons-material'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -29,6 +28,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { sortTypes } from 'utils/sortTypes'; import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton'; +import { Search } from 'component/common/Search/Search'; export const TagTypeList = () => { const [deletion, setDeletion] = useState<{ @@ -192,7 +192,7 @@ export const TagTypeList = () => { title="Tag types" actions={ <> -