1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

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 <fredrik.no@gmail.com>
This commit is contained in:
Nuno Góis 2022-06-10 14:23:12 +01:00 committed by GitHub
parent 63852441dd
commit 4761847ce5
23 changed files with 602 additions and 85 deletions

View File

@ -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) => {
<PageHeader
title="Overview"
actions={
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>

View File

@ -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 = (
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
<Search initialValue={globalFilter} onChange={setGlobalFilter} />
);
const headerActions = (

View File

@ -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={
<>
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>

View File

@ -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={
<>
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>

View File

@ -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<Record<'search', string>>;
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 (
<>
<div className={themeStyles.searchField}>
<SearchField initialValue={filter} updateValue={setFilter} />
</div>
<PageContent header={<PageHeader title="Applications" />}>
<PageContent
header={
<PageHeader
title="Applications"
actions={
<Search
initialValue={searchValue}
onChange={setSearchValue}
/>
}
/>
}
>
<div className={themeStyles.fullwidth}>
<ConditionallyRender
condition={filteredApplications.length > 0}

View File

@ -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%',
},
}));

View File

@ -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<HTMLInputElement>();
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 (
<div className={styles.container}>
<div
className={classnames(
styles.search,
className,
'search-container'
)}
>
<SearchIcon
className={classnames(styles.searchIcon, 'search-icon')}
/>
<InputBase
inputRef={ref}
placeholder={placeholder}
classes={{
root: classnames(styles.inputRoot, 'input-container'),
}}
inputProps={{ 'aria-label': placeholder }}
value={value}
onChange={e => onSearchChange(e.target.value)}
onFocus={() => setShowSuggestions(true)}
onBlur={() => setShowSuggestions(false)}
/>
<div
className={classnames(
styles.clearContainer,
'clear-container'
)}
>
<ConditionallyRender
condition={Boolean(value)}
show={
<Tooltip title="Clear search query" arrow>
<IconButton
size="small"
onClick={() => {
onChange('');
ref.current?.focus();
}}
>
<Close className={styles.clearIcon} />
</IconButton>
</Tooltip>
}
/>
</div>
</div>
<ConditionallyRender
condition={Boolean(hasFilters) && showSuggestions}
show={
<SearchSuggestions getSearchContext={getSearchContext!} />
}
/>
</div>
);
};

View File

@ -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<ISearchDescriptionProps> = ({
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 (
<>
<ConditionallyRender
condition={Boolean(searchText)}
show={
<>
<StyledHeader>Searching for:</StyledHeader>
<p>
<StyledCode>{searchText}</StyledCode>{' '}
{searchableColumnsString
? ` in ${searchableColumnsString}`
: ''}
</p>
</>
}
/>
<ConditionallyRender
condition={searchFilters.length > 0}
show={
<>
<StyledHeader>Filtering by:</StyledHeader>
{searchFilters.map(filter => (
<p key={filter.name}>
<StyledCode>
{filter.values.join(',')}
</StyledCode>{' '}
in {filter.header}. Options:{' '}
{filter.options.join(', ')}
</p>
))}
</>
}
/>
</>
);
};

View File

@ -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<ISearchInstructionsProps> = ({
filters,
getSearchContext,
searchableColumnsString,
}) => {
return (
<>
<StyledHeader>
{filters.length > 0
? 'Filter your search with operators like:'
: `Start typing to search${
searchableColumnsString
? ` in ${searchableColumnsString}`
: '...'
}`}
</StyledHeader>
{filters.map(filter => (
<p key={filter.name}>
Filter by {filter.header}:{' '}
<StyledCode>
{filter.name}:{filter.options[0]}
</StyledCode>
<ConditionallyRender
condition={filter.options.length > 1}
show={
<>
{' or '}
<StyledCode>
{filter.name}:
{filter.options.slice(0, 2).join(',')}
</StyledCode>
</>
}
/>
</p>
))}
</>
);
};

View File

@ -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<SearchSuggestionsProps> = ({
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 (
<StyledPaper>
<StyledBox>
<StyledFilterList />
<Box>
<ConditionallyRender
condition={Boolean(searchContext.searchValue)}
show={
<SearchDescription
filters={filters}
getSearchContext={getSearchContext}
searchableColumnsString={
searchableColumnsString
}
/>
}
elseShow={
<SearchInstructions
filters={filters}
getSearchContext={getSearchContext}
searchableColumnsString={
searchableColumnsString
}
/>
}
/>
</Box>
</StyledBox>
<StyledDivider />
<ConditionallyRender
condition={filters.length > 0}
show="Combine filters and search."
/>
<p>
Example:{' '}
<StyledCode>
{filters.map(filter => (
<span key={filter.name}>
{filter.name}:{filter.suggestedOption}{' '}
</span>
))}
<span>{suggestedTextSearch}</span>
</StyledCode>
</p>
</StyledPaper>
);
};

View File

@ -13,6 +13,9 @@ interface ISearchFieldProps {
showValueChip?: boolean;
}
/**
* @deprecated use `Search` instead.
*/
export const SearchField: VFC<ISearchFieldProps> = ({
updateValue,
initialValue = '',

View File

@ -9,5 +9,6 @@ export const useStyles = makeStyles()(theme => ({
justifyContent: 'center',
alignItems: 'center',
marginTop: theme.spacing(2),
width: '100%',
},
}));

View File

@ -11,6 +11,9 @@ interface ITableSearchProps {
getSearchContext?: () => IGetSearchContextOutput;
}
/**
* @deprecated use `Search` instead.
*/
export const TableSearch: FC<ITableSearchProps> = ({
initialValue,
onChange = () => {},

View File

@ -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<HTMLInputElement>();
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})`;

View File

@ -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={
<>
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>

View File

@ -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 = (
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
<Search initialValue={globalFilter} onChange={setGlobalFilter} />
);
const headerActions = (

View File

@ -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={
<>
<TableSearch
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters
@ -236,7 +236,7 @@ export const FeatureToggleListTable: VFC = () => {
<ConditionallyRender
condition={isSmallScreen}
show={
<TableSearch
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters

View File

@ -25,7 +25,6 @@ import {
TableCell,
TableRow,
TablePlaceholder,
TableSearch,
} from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import useProject from 'hooks/api/getters/useProject/useProject';
@ -44,6 +43,7 @@ import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureS
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { useSearch } from 'hooks/useSearch';
import { useMediaQuery } from '@mui/material';
import { Search } from 'component/common/Search/Search';
interface IProjectFeatureTogglesProps {
features: IProject['features'];
@ -412,11 +412,9 @@ export const ProjectFeatureToggles = ({
<ConditionallyRender
condition={!isSmallScreen}
show={
<TableSearch
<Search
initialValue={searchValue}
onChange={value =>
setSearchValue(value)
}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
@ -454,7 +452,7 @@ export const ProjectFeatureToggles = ({
<ConditionallyRender
condition={isSmallScreen}
show={
<TableSearch
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters

View File

@ -21,20 +21,4 @@ export const useStyles = makeStyles()(theme => ({
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%',
},
},
}));

View File

@ -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<Record<'search', string>>;
type projectMap = {
[index: string]: boolean;
@ -51,14 +55,30 @@ export const ProjectListNew = () => {
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
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 (
<div ref={ref}>
<div className={styles.searchBarContainer}>
<SearchField
initialValue={filter}
updateValue={setFilter}
showValueChip
className={classnames(styles.searchBar, {
skeleton: loading,
})}
/>
</div>
<PageContent
header={
<PageHeader
title="Projects"
actions={
<ResponsiveButton
Icon={Add}
onClick={() => navigate('/projects/create')}
maxWidth="700px"
permission={CREATE_PROJECT}
disabled={createButtonData.disabled}
>
New project
</ResponsiveButton>
<>
<ConditionallyRender
condition={!isSmallScreen}
show={
<>
<Search
initialValue={searchValue}
onChange={setSearchValue}
/>
<PageHeader.Divider />
</>
}
/>
<ResponsiveButton
Icon={Add}
onClick={() => navigate('/projects/create')}
maxWidth="700px"
permission={CREATE_PROJECT}
disabled={createButtonData.disabled}
>
New project
</ResponsiveButton>
</>
}
/>
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={searchValue}
onChange={setSearchValue}
/>
}
/>
</PageHeader>
}
>
<ConditionallyRender condition={error} show={renderError()} />
<div className={styles.container}>
<ConditionallyRender
condition={filteredProjects.length < 1 && !loading}
show={<div>No projects available.</div>}
show={
<ConditionallyRender
condition={searchValue?.length > 0}
show={
<TablePlaceholder>
No projects found matching &ldquo;
{searchValue}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No projects available.
</TablePlaceholder>
}
/>
}
elseShow={renderProjects()}
/>
</div>

View File

@ -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={
<>
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>

View File

@ -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={
<>
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>

View File

@ -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={
<>
<TableSearch
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>