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={
<>
-