1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: new search for feature toggle list table (#5454)

Filtering of feature toggles list with backend
This commit is contained in:
Tymoteusz Czech 2023-11-29 10:42:35 +01:00 committed by GitHub
parent bb03253681
commit f690fe86da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 616 additions and 127 deletions

View File

@ -13,12 +13,17 @@ import { FilterItemChip } from './FilterItemChip/FilterItemChip';
interface IFilterItemProps {
label: string;
options: Array<{ label: string; value: string }>;
onChange?: (value: string) => void;
}
const singularOperators = ['IS', 'IS_NOT'];
const pluralOperators = ['IS_IN', 'IS_NOT_IN'];
const pluralOperators = ['IS_ANY_OF', 'IS_NOT_ANY_OF'];
export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
export const FilterItem: FC<IFilterItemProps> = ({
label,
options,
onChange,
}) => {
const ref = useRef<HTMLDivElement>(null);
const [selectedOptions, setSelectedOptions] = useState<typeof options>([]);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
@ -35,8 +40,28 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
setAnchorEl(null);
};
const handleOnChange = (
op: typeof operator,
values: typeof selectedOptions,
) => {
const value = values.length
? `${op}:${values?.map((option) => option.value).join(', ')}`
: '';
onChange?.(value);
};
const handleOperatorChange = (value: string) => {
setOperator(value);
handleOnChange(value, selectedOptions);
};
const handleOptionsChange = (values: typeof selectedOptions) => {
setSelectedOptions(values);
handleOnChange(operator, values);
};
const onDelete = () => {
setSelectedOptions([]);
handleOptionsChange([]);
onClose();
};
@ -46,19 +71,19 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
(selectedOption) => selectedOption.value === value,
)
) {
setSelectedOptions(
selectedOptions?.filter(
(selectedOption) => selectedOption.value !== value,
),
const newOptions = selectedOptions?.filter(
(selectedOption) => selectedOption.value !== value,
);
handleOptionsChange(newOptions);
} else {
setSelectedOptions([
const newOptions = [
...(selectedOptions ?? []),
options.find((option) => option.value === value) ?? {
label: '',
value: '',
},
]);
];
handleOptionsChange(newOptions);
}
};
@ -80,7 +105,7 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
onClick={onClick}
operator={operator}
operatorOptions={currentOperators}
onChangeOperator={setOperator}
onChangeOperator={handleOperatorChange}
/>
</Box>
<StyledPopover

View File

@ -176,7 +176,9 @@ export const Search = ({
</StyledSearch>
<ConditionallyRender
condition={Boolean(hasFilters) && showSuggestions}
condition={
Boolean(hasFilters && getSearchContext) && showSuggestions
}
show={
<SearchSuggestions
onSuggestion={(suggestion) => {

View File

@ -0,0 +1,41 @@
import { VFC } from 'react';
import { Box } from '@mui/material';
import { FilterItem } from 'component/common/FilterItem/FilterItem';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useTableState } from 'hooks/useTableState';
export type FeatureTogglesListFilters = {
projectId?: string;
};
interface IFeatureToggleFiltersProps {
state: FeatureTogglesListFilters;
onChange: (value: FeatureTogglesListFilters) => void;
}
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
state,
onChange,
}) => {
const { projects } = useProjects();
const projectsOptions = (projects || []).map((project) => ({
label: project.name,
value: project.id,
}));
return (
<Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}>
<ConditionallyRender
condition={projectsOptions.length > 1}
show={() => (
<FilterItem
label='Project'
options={projectsOptions}
onChange={(value) => onChange({ projectId: value })}
/>
)}
/>
</Box>
);
};

View File

@ -1,37 +1,30 @@
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
import {
Box,
IconButton,
Link,
Tooltip,
useMediaQuery,
useTheme,
} from '@mui/material';
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { Link as RouterLink } from 'react-router-dom';
import { useFlexLayout, usePagination, useSortBy, useTable } from 'react-table';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { createLocalStorage } from 'utils/createLocalStorage';
import { FeatureSchema } from 'openapi';
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
import { useSearch } from 'hooks/useSearch';
import { Search } from 'component/common/Search/Search';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import FileDownload from '@mui/icons-material/FileDownload';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
@ -40,8 +33,20 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { focusable } from 'themes/themeStyles';
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useToast from 'hooks/useToast';
import { FilterItem } from 'component/common/FilterItem/FilterItem';
import { useUiFlag } from 'hooks/useUiFlag';
import { sortTypes } from 'utils/sortTypes';
import {
FeatureToggleFilters,
FeatureTogglesListFilters,
} from './FeatureToggleFilters/FeatureToggleFilters';
import {
DEFAULT_PAGE_LIMIT,
useFeatureSearch,
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
import {
defaultQueryKeys,
defaultStoredKeys,
useTableState,
} from 'hooks/useTableState';
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
name: 'Name of the feature',
@ -55,12 +60,15 @@ export type PageQueryType = Partial<
Record<'sort' | 'order' | 'search' | 'favorites', string>
>;
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
'FeatureToggleListTable:v1',
defaultSort,
);
type FeatureToggleListState = {
page: string;
pageSize: string;
sortBy?: string;
sortOrder?: string;
projectId?: string;
search?: string;
favorites?: string;
} & FeatureTogglesListFilters;
export const FeatureToggleListTable: VFC = () => {
const theme = useTheme();
@ -71,36 +79,54 @@ export const FeatureToggleListTable: VFC = () => {
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const [showExportDialog, setShowExportDialog] = useState(false);
const { features = [], loading, refetchFeatures } = useFeatures();
const [searchParams, setSearchParams] = useSearchParams();
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
const [tableState, setTableState] = useTableState<FeatureToggleListState>(
{
page: '1',
pageSize: `${DEFAULT_PAGE_LIMIT}`,
sortBy: 'createdAt',
sortOrder: 'desc',
projectId: '',
search: '',
favorites: 'true',
},
'featureToggleList',
[...defaultQueryKeys, 'projectId'],
[...defaultStoredKeys, 'projectId'],
);
const offset = (Number(tableState.page) - 1) * Number(tableState?.pageSize);
const {
features = [],
loading,
refetch: refetchFeatures,
} = useFeatureSearch(
offset,
Number(tableState.pageSize),
{
sortBy: tableState.sortBy || 'createdAt',
sortOrder: tableState.sortOrder || 'desc',
favoritesFirst: tableState.favorites === 'true',
},
tableState.projectId || undefined,
tableState.search || '',
);
const [initialState] = useState(() => ({
sortBy: [
{
id: searchParams.get('sort') || storedParams.id,
desc: searchParams.has('order')
? searchParams.get('order') === 'desc'
: storedParams.desc,
id: tableState.sortBy || 'createdAt',
desc: tableState.sortOrder === 'desc',
},
],
hiddenColumns: ['description'],
globalFilter: searchParams.get('search') || '',
pageSize: Number(tableState.pageSize),
pageIndex: Number(tableState.page) - 1,
}));
const { value: globalStore, setValue: setGlobalStore } =
useGlobalLocalStorage();
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
usePinnedFavorites(
searchParams.has('favorites')
? searchParams.get('favorites') === 'true'
: globalStore.favorites,
);
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
const { favorite, unfavorite } = useFavoriteFeaturesApi();
const onFavorite = useCallback(
async (feature: any) => {
// FIXME: projectId is missing
try {
if (feature?.favorite) {
await unfavorite(feature.project, feature.name);
@ -122,8 +148,15 @@ export const FeatureToggleListTable: VFC = () => {
{
Header: (
<FavoriteIconHeader
isActive={isFavoritesPinned}
onClick={onChangeIsFavoritePinned}
isActive={tableState.favorites === 'true'}
onClick={() =>
setTableState({
favorites:
tableState.favorites === 'true'
? 'false'
: 'true',
})
}
/>
),
accessor: 'favorite',
@ -194,38 +227,22 @@ export const FeatureToggleListTable: VFC = () => {
Cell: FeatureStaleCell,
sortType: 'boolean',
maxWidth: 120,
filterName: 'state',
filterParsing: (value: any) => (value ? 'stale' : 'active'),
},
// Always hidden -- for search
{
accessor: 'description',
Header: 'Description',
searchable: true,
},
],
[isFavoritesPinned],
[tableState.favorites],
);
const {
data: searchedData,
getSearchText,
getSearchContext,
} = useSearch(columns, searchValue, features);
const data = useMemo(
() =>
searchedData?.length === 0 && loading
? featuresPlaceholder
: searchedData,
[searchedData, loading],
features?.length === 0 && loading ? featuresPlaceholder : features,
[features, loading],
);
const {
headerGroups,
rows,
prepareRow,
state: { sortBy },
state: { pageIndex, pageSize, sortBy },
setHiddenColumns,
} = useTable(
{
@ -237,11 +254,23 @@ export const FeatureToggleListTable: VFC = () => {
autoResetSortBy: false,
disableSortRemove: true,
disableMultiSort: true,
manualSortBy: true,
manualPagination: true,
},
useSortBy,
useFlexLayout,
usePagination,
);
useEffect(() => {
setTableState({
page: `${pageIndex + 1}`,
pageSize: `${pageSize}`,
sortBy: sortBy[0]?.id || 'createdAt',
sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
});
}, [pageIndex, pageSize, sortBy]);
useConditionallyHiddenColumns(
[
{
@ -260,32 +289,7 @@ export const FeatureToggleListTable: VFC = () => {
setHiddenColumns,
columns,
);
useEffect(() => {
const tableState: PageQueryType = {};
tableState.sort = sortBy[0].id;
if (sortBy[0].desc) {
tableState.order = 'desc';
}
if (searchValue) {
tableState.search = searchValue;
}
if (isFavoritesPinned) {
tableState.favorites = 'true';
}
setSearchParams(tableState, {
replace: true,
});
setStoredParams({
id: sortBy[0].id,
desc: sortBy[0].desc || false,
});
setGlobalStore((params) => ({
...params,
favorites: Boolean(isFavoritesPinned),
}));
}, [sortBy, searchValue, setSearchParams, isFavoritesPinned]);
const setSearchValue = (search = '') => setTableState({ search });
if (!(environments.length > 0)) {
return null;
@ -308,12 +312,10 @@ export const FeatureToggleListTable: VFC = () => {
show={
<>
<Search
placeholder='Search and Filter'
placeholder='Search'
expandable
initialValue={searchValue}
initialValue={tableState.search}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
<PageHeader.Divider />
</>
@ -361,38 +363,16 @@ export const FeatureToggleListTable: VFC = () => {
condition={isSmallScreen}
show={
<Search
initialValue={searchValue}
initialValue={tableState.search}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
}
/>
</PageHeader>
}
>
{featureSearchFrontend && (
<Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}>
<FilterItem
label='Project'
options={[
{
label: 'Project 1',
value: '1',
},
{
label: 'Test',
value: '2',
},
{
label: 'Default',
value: '3',
},
]}
/>
</Box>
)}
<SearchHighlightProvider value={getSearchText(searchValue)}>
<FeatureToggleFilters state={tableState} onChange={setTableState} />
<SearchHighlightProvider value={tableState.search || ''}>
<VirtualizedTable
rows={rows}
headerGroups={headerGroups}
@ -403,11 +383,11 @@ export const FeatureToggleListTable: VFC = () => {
condition={rows.length === 0}
show={
<ConditionallyRender
condition={searchValue?.length > 0}
condition={(tableState.search || '')?.length > 0}
show={
<TablePlaceholder>
No feature toggles found matching &ldquo;
{searchValue}
{tableState.search}
&rdquo;
</TablePlaceholder>
}

View File

@ -0,0 +1,414 @@
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
import {
Box,
IconButton,
Link,
Tooltip,
useMediaQuery,
useTheme,
} from '@mui/material';
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { createLocalStorage } from 'utils/createLocalStorage';
import { FeatureSchema } from 'openapi';
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
import { useSearch } from 'hooks/useSearch';
import { Search } from 'component/common/Search/Search';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import FileDownload from '@mui/icons-material/FileDownload';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { ExportDialog } from './ExportDialog';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { focusable } from 'themes/themeStyles';
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useToast from 'hooks/useToast';
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
name: 'Name of the feature',
description: 'Short description of the feature',
type: '-',
createdAt: new Date(2022, 1, 1),
project: 'projectID',
});
export type PageQueryType = Partial<
Record<'sort' | 'order' | 'search' | 'favorites', string>
>;
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
'FeatureToggleListTable:v1',
defaultSort,
);
/**
* @deprecated remove with flag `featureSearchFrontend`
*/
export const FeatureToggleListTable: VFC = () => {
const theme = useTheme();
const { environments } = useEnvironments();
const enabledEnvironments = environments
.filter((env) => env.enabled)
.map((env) => env.name);
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const [showExportDialog, setShowExportDialog] = useState(false);
const { features = [], loading, refetchFeatures } = useFeatures();
const [searchParams, setSearchParams] = useSearchParams();
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const [initialState] = useState(() => ({
sortBy: [
{
id: searchParams.get('sort') || storedParams.id,
desc: searchParams.has('order')
? searchParams.get('order') === 'desc'
: storedParams.desc,
},
],
hiddenColumns: ['description'],
globalFilter: searchParams.get('search') || '',
}));
const { value: globalStore, setValue: setGlobalStore } =
useGlobalLocalStorage();
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
usePinnedFavorites(
searchParams.has('favorites')
? searchParams.get('favorites') === 'true'
: globalStore.favorites,
);
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
const { favorite, unfavorite } = useFavoriteFeaturesApi();
const onFavorite = useCallback(
async (feature: any) => {
try {
if (feature?.favorite) {
await unfavorite(feature.project, feature.name);
} else {
await favorite(feature.project, feature.name);
}
refetchFeatures();
} catch (error) {
setToastApiError(
'Something went wrong, could not update favorite',
);
}
},
[favorite, refetchFeatures, unfavorite, setToastApiError],
);
const columns = useMemo(
() => [
{
Header: (
<FavoriteIconHeader
isActive={isFavoritesPinned}
onClick={onChangeIsFavoritePinned}
/>
),
accessor: 'favorite',
Cell: ({ row: { original: feature } }: any) => (
<FavoriteIconCell
value={feature?.favorite}
onClick={() => onFavorite(feature)}
/>
),
maxWidth: 50,
disableSortBy: true,
},
{
Header: 'Seen',
accessor: 'lastSeenAt',
Cell: ({ value, row: { original: feature } }: any) => {
return <FeatureEnvironmentSeenCell feature={feature} />;
},
align: 'center',
maxWidth: 80,
},
{
Header: 'Type',
accessor: 'type',
Cell: FeatureTypeCell,
align: 'center',
maxWidth: 85,
},
{
Header: 'Name',
accessor: 'name',
minWidth: 150,
Cell: FeatureNameCell,
sortType: 'alphanumeric',
searchable: true,
},
{
id: 'tags',
Header: 'Tags',
accessor: (row: FeatureSchema) =>
row.tags
?.map(({ type, value }) => `${type}:${value}`)
.join('\n') || '',
Cell: FeatureTagCell,
width: 80,
searchable: true,
},
{
Header: 'Created',
accessor: 'createdAt',
Cell: DateCell,
maxWidth: 150,
},
{
Header: 'Project ID',
accessor: 'project',
Cell: ({ value }: { value: string }) => (
<LinkCell title={value} to={`/projects/${value}`} />
),
sortType: 'alphanumeric',
maxWidth: 150,
filterName: 'project',
searchable: true,
},
{
Header: 'State',
accessor: 'stale',
Cell: FeatureStaleCell,
sortType: 'boolean',
maxWidth: 120,
filterName: 'state',
filterParsing: (value: any) => (value ? 'stale' : 'active'),
},
// Always hidden -- for search
{
accessor: 'description',
Header: 'Description',
searchable: true,
},
],
[isFavoritesPinned],
);
const {
data: searchedData,
getSearchText,
getSearchContext,
} = useSearch(columns, searchValue, features);
const data = useMemo(
() =>
searchedData?.length === 0 && loading
? featuresPlaceholder
: searchedData,
[searchedData, loading],
);
const {
headerGroups,
rows,
prepareRow,
state: { sortBy },
setHiddenColumns,
} = useTable(
{
columns: columns as any[],
data,
initialState,
sortTypes,
autoResetHiddenColumns: false,
autoResetSortBy: false,
disableSortRemove: true,
disableMultiSort: true,
},
useSortBy,
useFlexLayout,
);
useConditionallyHiddenColumns(
[
{
condition: !features.some(({ tags }) => tags?.length),
columns: ['tags'],
},
{
condition: isSmallScreen,
columns: ['type', 'createdAt', 'tags'],
},
{
condition: isMediumScreen,
columns: ['lastSeenAt', 'stale'],
},
],
setHiddenColumns,
columns,
);
useEffect(() => {
const tableState: PageQueryType = {};
tableState.sort = sortBy[0].id;
if (sortBy[0].desc) {
tableState.order = 'desc';
}
if (searchValue) {
tableState.search = searchValue;
}
if (isFavoritesPinned) {
tableState.favorites = 'true';
}
setSearchParams(tableState, {
replace: true,
});
setStoredParams({
id: sortBy[0].id,
desc: sortBy[0].desc || false,
});
setGlobalStore((params) => ({
...params,
favorites: Boolean(isFavoritesPinned),
}));
}, [sortBy, searchValue, setSearchParams, isFavoritesPinned]);
if (!(environments.length > 0)) {
return null;
}
return (
<PageContent
isLoading={loading}
header={
<PageHeader
title={`Feature toggles (${
rows.length < data.length
? `${rows.length} of ${data.length}`
: data.length
})`}
actions={
<>
<ConditionallyRender
condition={!isSmallScreen}
show={
<>
<Search
placeholder='Search and Filter'
expandable
initialValue={searchValue}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
<PageHeader.Divider />
</>
}
/>
<Link
component={RouterLink}
to='/archive'
underline='always'
sx={{ marginRight: 2, ...focusable(theme) }}
>
View archive
</Link>
<ConditionallyRender
condition={Boolean(
uiConfig?.flags?.featuresExportImport,
)}
show={
<Tooltip
title='Export current selection'
arrow
>
<IconButton
onClick={() =>
setShowExportDialog(true)
}
sx={(theme) => ({
marginRight: theme.spacing(2),
})}
>
<FileDownload />
</IconButton>
</Tooltip>
}
/>
<CreateFeatureButton
loading={false}
filter={{ query: '', project: 'default' }}
/>
</>
}
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={searchValue}
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
/>
}
/>
</PageHeader>
}
>
<SearchHighlightProvider value={getSearchText(searchValue)}>
<VirtualizedTable
rows={rows}
headerGroups={headerGroups}
prepareRow={prepareRow}
/>
</SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={
<ConditionallyRender
condition={searchValue?.length > 0}
show={
<TablePlaceholder>
No feature toggles found matching &ldquo;
{searchValue}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No feature toggles available. Get started by
adding a new feature toggle.
</TablePlaceholder>
}
/>
}
/>
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
show={
<ExportDialog
showExportDialog={showExportDialog}
data={data}
onClose={() => setShowExportDialog(false)}
environments={enabledEnvironments}
/>
}
/>
</PageContent>
);
};

View File

@ -115,6 +115,7 @@ const StyledIconButton = styled(IconButton)<{
}));
const Header: VFC = () => {
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
const { onSetThemeMode, themeMode } = useThemeMode();
const theme = useTheme();
const adminId = useId();
@ -191,7 +192,15 @@ const Header: VFC = () => {
<StyledNav>
<StyledLinks>
<StyledLink to='/projects'>Projects</StyledLink>
<StyledLink to='/features'>Feature toggles</StyledLink>
<StyledLink
to={
featureSearchFrontend
? '/features-new'
: '/features'
}
>
Feature toggles
</StyledLink>
<StyledLink to='/playground'>Playground</StyledLink>
<StyledAdvancedNavButton
onClick={(e) => setConfigRef(e.currentTarget)}

View File

@ -123,6 +123,16 @@ exports[`returns all baseRoutes 1`] = `
"title": "Feature toggles",
"type": "protected",
},
{
"component": [Function],
"flag": "featureSearchFrontend",
"menu": {
"mobile": true,
},
"path": "/features-new",
"title": "Feature toggles",
"type": "protected",
},
{
"component": {
"$$typeof": Symbol(react.lazy),

View File

@ -1,4 +1,5 @@
import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from 'component/feature/FeatureToggleList/LegacyFeatureToggleListTable';
import { StrategyView } from 'component/strategies/StrategyView/StrategyView';
import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList';
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
@ -144,9 +145,17 @@ export const routes: IRoute[] = [
{
path: '/features',
title: 'Feature toggles',
component: LegacyFeatureToggleListTable,
type: 'protected',
menu: { mobile: true },
},
{
path: '/features-new',
title: 'Feature toggles',
component: FeatureToggleListTable,
type: 'protected',
menu: { mobile: true },
flag: 'featureSearchFrontend',
},
// Playground

View File

@ -128,7 +128,6 @@ const getFeatureSearchFetcher = (
const searchQueryParams = translateToQueryParams(searchValue);
const sortQueryParams = translateToSortQueryParams(sortingRules);
const project = projectId ? `projectId=${projectId}&` : '';
const KEY = `api/admin/search/features?${project}offset=${offset}&limit=${limit}&${searchQueryParams}&${sortQueryParams}`;
const fetcher = () => {
const path = formatApiPath(KEY);