mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-29 01:15:48 +02:00
Merge branch 'main' into archive_table
This commit is contained in:
commit
3040256047
@ -1,6 +1,5 @@
|
|||||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||||
import {
|
import {
|
||||||
TableSearch,
|
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
@ -26,6 +25,7 @@ import { formatExpiredAt } from 'component/Reporting/ReportExpiredCell/formatExp
|
|||||||
import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
interface IReportTableProps {
|
interface IReportTableProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -95,7 +95,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Overview"
|
title="Overview"
|
||||||
actions={
|
actions={
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -3,7 +3,6 @@ import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import {
|
import {
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
TableSearch,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
} from 'component/common/Table';
|
} 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 { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
export const ApiTokenTable = () => {
|
export const ApiTokenTable = () => {
|
||||||
const { tokens, loading } = useApiTokens();
|
const { tokens, loading } = useApiTokens();
|
||||||
@ -57,7 +57,7 @@ export const ApiTokenTable = () => {
|
|||||||
}, [setHiddenColumns, hiddenColumns]);
|
}, [setHiddenColumns, hiddenColumns]);
|
||||||
|
|
||||||
const headerSearch = (
|
const headerSearch = (
|
||||||
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
|
<Search initialValue={globalFilter} onChange={setGlobalFilter} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const headerActions = (
|
const headerActions = (
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
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 { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
const ROOTROLE = 'root';
|
const ROOTROLE = 'root';
|
||||||
const BUILTIN_ROLE_TYPE = 'project';
|
const BUILTIN_ROLE_TYPE = 'project';
|
||||||
@ -190,7 +190,7 @@ const ProjectRoleList = () => {
|
|||||||
title="Project roles"
|
title="Project roles"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import ChangePassword from './ChangePassword/ChangePassword';
|
import ChangePassword from './ChangePassword/ChangePassword';
|
||||||
import DeleteUser from './DeleteUser/DeleteUser';
|
import DeleteUser from './DeleteUser/DeleteUser';
|
||||||
@ -34,6 +33,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell';
|
import { UsersActionsCell } from './UsersActionsCell/UsersActionsCell';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
||||||
width: theme.spacing(4),
|
width: theme.spacing(4),
|
||||||
@ -248,7 +248,7 @@ const UsersList = () => {
|
|||||||
title="Users"
|
title="Users"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -1,23 +1,40 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { Warning } from '@mui/icons-material';
|
import { Warning } from '@mui/icons-material';
|
||||||
import { AppsLinkList, styles as themeStyles } from 'component/common';
|
import { AppsLinkList, styles as themeStyles } from 'component/common';
|
||||||
import { SearchField } from 'component/common/SearchField/SearchField';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 = () => {
|
export const ApplicationList = () => {
|
||||||
const { applications, loading } = useApplications();
|
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 filteredApplications = useMemo(() => {
|
||||||
const regExp = new RegExp(filter, 'i');
|
const regExp = new RegExp(searchValue, 'i');
|
||||||
return filter
|
return searchValue
|
||||||
? applications?.filter(a => regExp.test(a.appName))
|
? applications?.filter(a => regExp.test(a.appName))
|
||||||
: applications;
|
: applications;
|
||||||
}, [applications, filter]);
|
}, [applications, searchValue]);
|
||||||
|
|
||||||
const renderNoApplications = () => (
|
const renderNoApplications = () => (
|
||||||
<>
|
<>
|
||||||
@ -44,10 +61,19 @@ export const ApplicationList = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={themeStyles.searchField}>
|
<PageContent
|
||||||
<SearchField initialValue={filter} updateValue={setFilter} />
|
header={
|
||||||
</div>
|
<PageHeader
|
||||||
<PageContent header={<PageHeader title="Applications" />}>
|
title="Applications"
|
||||||
|
actions={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className={themeStyles.fullwidth}>
|
<div className={themeStyles.fullwidth}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={filteredApplications.length > 0}
|
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;
|
showValueChip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `Search` instead.
|
||||||
|
*/
|
||||||
export const SearchField: VFC<ISearchFieldProps> = ({
|
export const SearchField: VFC<ISearchFieldProps> = ({
|
||||||
updateValue,
|
updateValue,
|
||||||
initialValue = '',
|
initialValue = '',
|
||||||
|
@ -9,5 +9,6 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -11,6 +11,9 @@ interface ITableSearchProps {
|
|||||||
getSearchContext?: () => IGetSearchContextOutput;
|
getSearchContext?: () => IGetSearchContextOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `Search` instead.
|
||||||
|
*/
|
||||||
export const TableSearch: FC<ITableSearchProps> = ({
|
export const TableSearch: FC<ITableSearchProps> = ({
|
||||||
initialValue,
|
initialValue,
|
||||||
onChange = () => {},
|
onChange = () => {},
|
||||||
|
@ -17,6 +17,9 @@ interface ITableSearchFieldProps {
|
|||||||
getSearchContext?: () => IGetSearchContextOutput;
|
getSearchContext?: () => IGetSearchContextOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `Search` instead.
|
||||||
|
*/
|
||||||
export const TableSearchField = ({
|
export const TableSearchField = ({
|
||||||
value = '',
|
value = '',
|
||||||
onChange,
|
onChange,
|
||||||
@ -28,16 +31,20 @@ export const TableSearchField = ({
|
|||||||
const ref = useRef<HTMLInputElement>();
|
const ref = useRef<HTMLInputElement>();
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
|
||||||
const hotkey = useKeyboardShortcut(
|
const hotkey = useKeyboardShortcut(
|
||||||
{ modifiers: ['ctrl'], key: 'k', preventDefault: true },
|
{ modifiers: ['ctrl'], key: 'k', preventDefault: true },
|
||||||
() => {
|
() => {
|
||||||
|
if (document.activeElement === ref.current) {
|
||||||
|
ref.current?.blur();
|
||||||
|
} else {
|
||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
setShowSuggestions(true);
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
useKeyboardShortcut({ key: 'Escape' }, () => {
|
useKeyboardShortcut({ key: 'Escape' }, () => {
|
||||||
if (document.activeElement === ref.current) {
|
if (document.activeElement === ref.current) {
|
||||||
setShowSuggestions(suggestions => !suggestions);
|
ref.current?.blur();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
|
const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
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 { ContextActionsCell } from './ContextActionsCell/ContextActionsCell';
|
||||||
import { Adjust } from '@mui/icons-material';
|
import { Adjust } from '@mui/icons-material';
|
||||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
const ContextList: VFC = () => {
|
const ContextList: VFC = () => {
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
@ -164,7 +164,7 @@ const ContextList: VFC = () => {
|
|||||||
title="Context fields"
|
title="Context fields"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -4,7 +4,6 @@ import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironmen
|
|||||||
import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton';
|
import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton';
|
||||||
import { useTable, useGlobalFilter } from 'react-table';
|
import { useTable, useGlobalFilter } from 'react-table';
|
||||||
import {
|
import {
|
||||||
TableSearch,
|
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
Table,
|
Table,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
@ -24,6 +23,7 @@ import useEnvironmentApi, {
|
|||||||
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
@ -71,7 +71,7 @@ export const EnvironmentTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const headerSearch = (
|
const headerSearch = (
|
||||||
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
|
<Search initialValue={globalFilter} onChange={setGlobalFilter} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const headerActions = (
|
const headerActions = (
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
@ -29,6 +28,7 @@ import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'
|
|||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||||
name: 'Name of the feature',
|
name: 'Name of the feature',
|
||||||
@ -210,7 +210,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
condition={!isSmallScreen}
|
condition={!isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={searchValue}
|
||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
hasFilters
|
hasFilters
|
||||||
@ -238,7 +238,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={isSmallScreen}
|
condition={isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={searchValue}
|
||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
hasFilters
|
hasFilters
|
||||||
|
@ -25,7 +25,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
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 { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
interface IProjectFeatureTogglesProps {
|
||||||
features: IProject['features'];
|
features: IProject['features'];
|
||||||
@ -412,11 +412,9 @@ export const ProjectFeatureToggles = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!isSmallScreen}
|
condition={!isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={searchValue}
|
||||||
onChange={value =>
|
onChange={setSearchValue}
|
||||||
setSearchValue(value)
|
|
||||||
}
|
|
||||||
hasFilters
|
hasFilters
|
||||||
getSearchContext={getSearchContext}
|
getSearchContext={getSearchContext}
|
||||||
/>
|
/>
|
||||||
@ -454,7 +452,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={isSmallScreen}
|
condition={isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={searchValue}
|
||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
hasFilters
|
hasFilters
|
||||||
|
@ -21,20 +21,4 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
fontFamily: theme.typography.fontFamily,
|
fontFamily: theme.typography.fontFamily,
|
||||||
pointer: 'cursor',
|
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 { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr';
|
||||||
import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher';
|
import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
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 { Add } from '@mui/icons-material';
|
||||||
import ApiError from 'component/common/ApiError/ApiError';
|
import ApiError from 'component/common/ApiError/ApiError';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { SearchField } from 'component/common/SearchField/SearchField';
|
import { TablePlaceholder } from 'component/common/Table';
|
||||||
import classnames from 'classnames';
|
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 = {
|
type projectMap = {
|
||||||
[index: string]: boolean;
|
[index: string]: boolean;
|
||||||
@ -51,14 +55,30 @@ export const ProjectListNew = () => {
|
|||||||
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
const { isOss } = useUiConfig();
|
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 filteredProjects = useMemo(() => {
|
||||||
const regExp = new RegExp(filter, 'i');
|
const regExp = new RegExp(searchValue, 'i');
|
||||||
return filter
|
return searchValue
|
||||||
? projects.filter(project => regExp.test(project.name))
|
? projects.filter(project => regExp.test(project.name))
|
||||||
: projects;
|
: projects;
|
||||||
}, [projects, filter]);
|
}, [projects, searchValue]);
|
||||||
|
|
||||||
const handleHover = (projectId: string) => {
|
const handleHover = (projectId: string) => {
|
||||||
if (fetchedProjects[projectId]) {
|
if (fetchedProjects[projectId]) {
|
||||||
@ -129,21 +149,24 @@ export const ProjectListNew = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<div className={styles.searchBarContainer}>
|
|
||||||
<SearchField
|
|
||||||
initialValue={filter}
|
|
||||||
updateValue={setFilter}
|
|
||||||
showValueChip
|
|
||||||
className={classnames(styles.searchBar, {
|
|
||||||
skeleton: loading,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<PageContent
|
<PageContent
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Projects"
|
title="Projects"
|
||||||
actions={
|
actions={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
<PageHeader.Divider />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
Icon={Add}
|
Icon={Add}
|
||||||
onClick={() => navigate('/projects/create')}
|
onClick={() => navigate('/projects/create')}
|
||||||
@ -153,15 +176,42 @@ export const ProjectListNew = () => {
|
|||||||
>
|
>
|
||||||
New project
|
New project
|
||||||
</ResponsiveButton>
|
</ResponsiveButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</PageHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ConditionallyRender condition={error} show={renderError()} />
|
<ConditionallyRender condition={error} show={renderError()} />
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={filteredProjects.length < 1 && !loading}
|
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()}
|
elseShow={renderProjects()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import {
|
import {
|
||||||
TableSearch,
|
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
@ -25,6 +24,7 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
|
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
export const SegmentTable = () => {
|
export const SegmentTable = () => {
|
||||||
const { segments, loading } = useSegments();
|
const { segments, loading } = useSegments();
|
||||||
@ -87,7 +87,7 @@ export const SegmentTable = () => {
|
|||||||
title="Segments"
|
title="Segments"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
@ -38,6 +37,7 @@ import { sortTypes } from 'utils/sortTypes';
|
|||||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||||
import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton';
|
import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton';
|
||||||
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
|
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
interface IDialogueMetaData {
|
interface IDialogueMetaData {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -357,7 +357,7 @@ export const StrategiesList = () => {
|
|||||||
title="Strategies"
|
title="Strategies"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
TableSearch,
|
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { Delete, Edit, Label } from '@mui/icons-material';
|
import { Delete, Edit, Label } from '@mui/icons-material';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
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 { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton';
|
import { AddTagTypeButton } from './AddTagTypeButton/AddTagTypeButton';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
|
||||||
export const TagTypeList = () => {
|
export const TagTypeList = () => {
|
||||||
const [deletion, setDeletion] = useState<{
|
const [deletion, setDeletion] = useState<{
|
||||||
@ -192,7 +192,7 @@ export const TagTypeList = () => {
|
|||||||
title="Tag types"
|
title="Tag types"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<Search
|
||||||
initialValue={globalFilter}
|
initialValue={globalFilter}
|
||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user