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:
parent
63852441dd
commit
4761847ce5
@ -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}
|
||||
/>
|
||||
|
@ -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 = (
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
46
frontend/src/component/common/Search/Search.styles.ts
Normal file
46
frontend/src/component/common/Search/Search.styles.ts
Normal 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%',
|
||||
},
|
||||
}));
|
115
frontend/src/component/common/Search/Search.tsx
Normal file
115
frontend/src/component/common/Search/Search.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -13,6 +13,9 @@ interface ISearchFieldProps {
|
||||
showValueChip?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Search` instead.
|
||||
*/
|
||||
export const SearchField: VFC<ISearchFieldProps> = ({
|
||||
updateValue,
|
||||
initialValue = '',
|
||||
|
@ -9,5 +9,6 @@ export const useStyles = makeStyles()(theme => ({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: theme.spacing(2),
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
@ -11,6 +11,9 @@ interface ITableSearchProps {
|
||||
getSearchContext?: () => IGetSearchContextOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Search` instead.
|
||||
*/
|
||||
export const TableSearch: FC<ITableSearchProps> = ({
|
||||
initialValue,
|
||||
onChange = () => {},
|
||||
|
@ -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})`;
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 = (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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%',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
@ -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 “
|
||||
{searchValue}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No projects available.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
elseShow={renderProjects()}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user