mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
feat: new search for feature toggle list table (#5454)
Filtering of feature toggles list with backend
This commit is contained in:
parent
bb03253681
commit
f690fe86da
@ -13,12 +13,17 @@ import { FilterItemChip } from './FilterItemChip/FilterItemChip';
|
|||||||
interface IFilterItemProps {
|
interface IFilterItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
options: Array<{ label: string; value: string }>;
|
options: Array<{ label: string; value: string }>;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const singularOperators = ['IS', 'IS_NOT'];
|
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 ref = useRef<HTMLDivElement>(null);
|
||||||
const [selectedOptions, setSelectedOptions] = useState<typeof options>([]);
|
const [selectedOptions, setSelectedOptions] = useState<typeof options>([]);
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||||
@ -35,8 +40,28 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
|
|||||||
setAnchorEl(null);
|
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 = () => {
|
const onDelete = () => {
|
||||||
setSelectedOptions([]);
|
handleOptionsChange([]);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,19 +71,19 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
|
|||||||
(selectedOption) => selectedOption.value === value,
|
(selectedOption) => selectedOption.value === value,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setSelectedOptions(
|
const newOptions = selectedOptions?.filter(
|
||||||
selectedOptions?.filter(
|
|
||||||
(selectedOption) => selectedOption.value !== value,
|
(selectedOption) => selectedOption.value !== value,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
handleOptionsChange(newOptions);
|
||||||
} else {
|
} else {
|
||||||
setSelectedOptions([
|
const newOptions = [
|
||||||
...(selectedOptions ?? []),
|
...(selectedOptions ?? []),
|
||||||
options.find((option) => option.value === value) ?? {
|
options.find((option) => option.value === value) ?? {
|
||||||
label: '',
|
label: '',
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
handleOptionsChange(newOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,7 +105,7 @@ export const FilterItem: FC<IFilterItemProps> = ({ label, options }) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
operator={operator}
|
operator={operator}
|
||||||
operatorOptions={currentOperators}
|
operatorOptions={currentOperators}
|
||||||
onChangeOperator={setOperator}
|
onChangeOperator={handleOperatorChange}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<StyledPopover
|
<StyledPopover
|
||||||
|
@ -176,7 +176,9 @@ export const Search = ({
|
|||||||
</StyledSearch>
|
</StyledSearch>
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(hasFilters) && showSuggestions}
|
condition={
|
||||||
|
Boolean(hasFilters && getSearchContext) && showSuggestions
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<SearchSuggestions
|
<SearchSuggestions
|
||||||
onSuggestion={(suggestion) => {
|
onSuggestion={(suggestion) => {
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,37 +1,30 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
|
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
import { useFlexLayout, usePagination, useSortBy, useTable } from 'react-table';
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/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 { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
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 { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||||
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
||||||
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';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
|
||||||
import { FeatureSchema } from 'openapi';
|
import { FeatureSchema } from 'openapi';
|
||||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
import FileDownload from '@mui/icons-material/FileDownload';
|
import FileDownload from '@mui/icons-material/FileDownload';
|
||||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
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 { focusable } from 'themes/themeStyles';
|
||||||
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { FilterItem } from 'component/common/FilterItem/FilterItem';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
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({
|
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||||
name: 'Name of the feature',
|
name: 'Name of the feature',
|
||||||
@ -55,12 +60,15 @@ export type PageQueryType = Partial<
|
|||||||
Record<'sort' | 'order' | 'search' | 'favorites', string>
|
Record<'sort' | 'order' | 'search' | 'favorites', string>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
|
type FeatureToggleListState = {
|
||||||
|
page: string;
|
||||||
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
pageSize: string;
|
||||||
'FeatureToggleListTable:v1',
|
sortBy?: string;
|
||||||
defaultSort,
|
sortOrder?: string;
|
||||||
);
|
projectId?: string;
|
||||||
|
search?: string;
|
||||||
|
favorites?: string;
|
||||||
|
} & FeatureTogglesListFilters;
|
||||||
|
|
||||||
export const FeatureToggleListTable: VFC = () => {
|
export const FeatureToggleListTable: VFC = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -71,36 +79,54 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
const { features = [], loading, refetchFeatures } = useFeatures();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
const [tableState, setTableState] = useTableState<FeatureToggleListState>(
|
||||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
{
|
||||||
|
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(() => ({
|
const [initialState] = useState(() => ({
|
||||||
sortBy: [
|
sortBy: [
|
||||||
{
|
{
|
||||||
id: searchParams.get('sort') || storedParams.id,
|
id: tableState.sortBy || 'createdAt',
|
||||||
desc: searchParams.has('order')
|
desc: tableState.sortOrder === 'desc',
|
||||||
? searchParams.get('order') === 'desc'
|
|
||||||
: storedParams.desc,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hiddenColumns: ['description'],
|
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 { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||||
const onFavorite = useCallback(
|
const onFavorite = useCallback(
|
||||||
async (feature: any) => {
|
async (feature: any) => {
|
||||||
|
// FIXME: projectId is missing
|
||||||
try {
|
try {
|
||||||
if (feature?.favorite) {
|
if (feature?.favorite) {
|
||||||
await unfavorite(feature.project, feature.name);
|
await unfavorite(feature.project, feature.name);
|
||||||
@ -122,8 +148,15 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
{
|
{
|
||||||
Header: (
|
Header: (
|
||||||
<FavoriteIconHeader
|
<FavoriteIconHeader
|
||||||
isActive={isFavoritesPinned}
|
isActive={tableState.favorites === 'true'}
|
||||||
onClick={onChangeIsFavoritePinned}
|
onClick={() =>
|
||||||
|
setTableState({
|
||||||
|
favorites:
|
||||||
|
tableState.favorites === 'true'
|
||||||
|
? 'false'
|
||||||
|
: 'true',
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
accessor: 'favorite',
|
accessor: 'favorite',
|
||||||
@ -194,38 +227,22 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
Cell: FeatureStaleCell,
|
Cell: FeatureStaleCell,
|
||||||
sortType: 'boolean',
|
sortType: 'boolean',
|
||||||
maxWidth: 120,
|
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(
|
const data = useMemo(
|
||||||
() =>
|
() =>
|
||||||
searchedData?.length === 0 && loading
|
features?.length === 0 && loading ? featuresPlaceholder : features,
|
||||||
? featuresPlaceholder
|
[features, loading],
|
||||||
: searchedData,
|
|
||||||
[searchedData, loading],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
headerGroups,
|
headerGroups,
|
||||||
rows,
|
rows,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
state: { sortBy },
|
state: { pageIndex, pageSize, sortBy },
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
@ -237,11 +254,23 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
disableSortRemove: true,
|
disableSortRemove: true,
|
||||||
disableMultiSort: true,
|
disableMultiSort: true,
|
||||||
|
manualSortBy: true,
|
||||||
|
manualPagination: true,
|
||||||
},
|
},
|
||||||
useSortBy,
|
useSortBy,
|
||||||
useFlexLayout,
|
useFlexLayout,
|
||||||
|
usePagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState({
|
||||||
|
page: `${pageIndex + 1}`,
|
||||||
|
pageSize: `${pageSize}`,
|
||||||
|
sortBy: sortBy[0]?.id || 'createdAt',
|
||||||
|
sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
|
||||||
|
});
|
||||||
|
}, [pageIndex, pageSize, sortBy]);
|
||||||
|
|
||||||
useConditionallyHiddenColumns(
|
useConditionallyHiddenColumns(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -260,32 +289,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
columns,
|
columns,
|
||||||
);
|
);
|
||||||
|
const setSearchValue = (search = '') => setTableState({ search });
|
||||||
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)) {
|
if (!(environments.length > 0)) {
|
||||||
return null;
|
return null;
|
||||||
@ -308,12 +312,10 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<Search
|
<Search
|
||||||
placeholder='Search and Filter'
|
placeholder='Search'
|
||||||
expandable
|
expandable
|
||||||
initialValue={searchValue}
|
initialValue={tableState.search}
|
||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
/>
|
/>
|
||||||
<PageHeader.Divider />
|
<PageHeader.Divider />
|
||||||
</>
|
</>
|
||||||
@ -361,38 +363,16 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
condition={isSmallScreen}
|
condition={isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<Search
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={tableState.search}
|
||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{featureSearchFrontend && (
|
<FeatureToggleFilters state={tableState} onChange={setTableState} />
|
||||||
<Box sx={(theme) => ({ marginBottom: theme.spacing(2) })}>
|
<SearchHighlightProvider value={tableState.search || ''}>
|
||||||
<FilterItem
|
|
||||||
label='Project'
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: 'Project 1',
|
|
||||||
value: '1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Test',
|
|
||||||
value: '2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Default',
|
|
||||||
value: '3',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
|
||||||
<VirtualizedTable
|
<VirtualizedTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
headerGroups={headerGroups}
|
headerGroups={headerGroups}
|
||||||
@ -403,11 +383,11 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
condition={rows.length === 0}
|
condition={rows.length === 0}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={searchValue?.length > 0}
|
condition={(tableState.search || '')?.length > 0}
|
||||||
show={
|
show={
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
No feature toggles found matching “
|
No feature toggles found matching “
|
||||||
{searchValue}
|
{tableState.search}
|
||||||
”
|
”
|
||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
}
|
}
|
||||||
|
@ -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 “
|
||||||
|
{searchValue}
|
||||||
|
”
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
@ -115,6 +115,7 @@ const StyledIconButton = styled(IconButton)<{
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const Header: VFC = () => {
|
const Header: VFC = () => {
|
||||||
|
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
||||||
const { onSetThemeMode, themeMode } = useThemeMode();
|
const { onSetThemeMode, themeMode } = useThemeMode();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const adminId = useId();
|
const adminId = useId();
|
||||||
@ -191,7 +192,15 @@ const Header: VFC = () => {
|
|||||||
<StyledNav>
|
<StyledNav>
|
||||||
<StyledLinks>
|
<StyledLinks>
|
||||||
<StyledLink to='/projects'>Projects</StyledLink>
|
<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>
|
<StyledLink to='/playground'>Playground</StyledLink>
|
||||||
<StyledAdvancedNavButton
|
<StyledAdvancedNavButton
|
||||||
onClick={(e) => setConfigRef(e.currentTarget)}
|
onClick={(e) => setConfigRef(e.currentTarget)}
|
||||||
|
@ -123,6 +123,16 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Feature toggles",
|
"title": "Feature toggles",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "featureSearchFrontend",
|
||||||
|
"menu": {
|
||||||
|
"mobile": true,
|
||||||
|
},
|
||||||
|
"path": "/features-new",
|
||||||
|
"title": "Feature toggles",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": {
|
"component": {
|
||||||
"$$typeof": Symbol(react.lazy),
|
"$$typeof": Symbol(react.lazy),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
||||||
|
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from 'component/feature/FeatureToggleList/LegacyFeatureToggleListTable';
|
||||||
import { StrategyView } from 'component/strategies/StrategyView/StrategyView';
|
import { StrategyView } from 'component/strategies/StrategyView/StrategyView';
|
||||||
import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList';
|
import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList';
|
||||||
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
||||||
@ -144,9 +145,17 @@ export const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
path: '/features',
|
path: '/features',
|
||||||
title: 'Feature toggles',
|
title: 'Feature toggles',
|
||||||
|
component: LegacyFeatureToggleListTable,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { mobile: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/features-new',
|
||||||
|
title: 'Feature toggles',
|
||||||
component: FeatureToggleListTable,
|
component: FeatureToggleListTable,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true },
|
menu: { mobile: true },
|
||||||
|
flag: 'featureSearchFrontend',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Playground
|
// Playground
|
||||||
|
@ -128,7 +128,6 @@ const getFeatureSearchFetcher = (
|
|||||||
const searchQueryParams = translateToQueryParams(searchValue);
|
const searchQueryParams = translateToQueryParams(searchValue);
|
||||||
const sortQueryParams = translateToSortQueryParams(sortingRules);
|
const sortQueryParams = translateToSortQueryParams(sortingRules);
|
||||||
const project = projectId ? `projectId=${projectId}&` : '';
|
const project = projectId ? `projectId=${projectId}&` : '';
|
||||||
|
|
||||||
const KEY = `api/admin/search/features?${project}offset=${offset}&limit=${limit}&${searchQueryParams}&${sortQueryParams}`;
|
const KEY = `api/admin/search/features?${project}offset=${offset}&limit=${limit}&${searchQueryParams}&${sortQueryParams}`;
|
||||||
const fetcher = () => {
|
const fetcher = () => {
|
||||||
const path = formatApiPath(KEY);
|
const path = formatApiPath(KEY);
|
||||||
|
Loading…
Reference in New Issue
Block a user