mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-01 13:47:27 +02:00
chore: revive archive page (#10001)
Related to: - https://linear.app/unleash/issue/2-3366/remove-get-apiadminarchivefeatures-deprecated-in-4100 - https://linear.app/unleash/issue/2-3367/remove-get-apiadminarchivefeaturesprojectid-deprecated-in-4110 Brings back the overall flag archive page and table using the feature flag search endpoint.
This commit is contained in:
parent
0255cf137a
commit
480689e828
@ -0,0 +1,123 @@
|
|||||||
|
import { ArchiveTable } from 'component/archive/ArchiveTable/ArchiveTable';
|
||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
|
||||||
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
|
import type { FeatureSearchResponseSchema } from 'openapi';
|
||||||
|
|
||||||
|
const mockedFeatures = [
|
||||||
|
{
|
||||||
|
name: 'someFeature',
|
||||||
|
description: '',
|
||||||
|
type: 'release',
|
||||||
|
project: 'default',
|
||||||
|
stale: false,
|
||||||
|
createdAt: '2023-08-10T09:28:58.928Z',
|
||||||
|
lastSeenAt: null,
|
||||||
|
impressionData: false,
|
||||||
|
archivedAt: '2023-08-11T10:18:03.429Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'someOtherFeature',
|
||||||
|
description: '',
|
||||||
|
type: 'release',
|
||||||
|
project: 'default',
|
||||||
|
stale: false,
|
||||||
|
createdAt: '2023-08-10T09:28:58.928Z',
|
||||||
|
lastSeenAt: null,
|
||||||
|
impressionData: false,
|
||||||
|
archivedAt: '2023-08-11T10:18:03.429Z',
|
||||||
|
},
|
||||||
|
] as FeatureSearchResponseSchema[];
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const [storedParams, setStoredParams] = useState({});
|
||||||
|
return (
|
||||||
|
<ArchiveTable
|
||||||
|
title='Archived features'
|
||||||
|
archivedFeatures={mockedFeatures}
|
||||||
|
refetch={() => Promise.resolve({})}
|
||||||
|
loading={false}
|
||||||
|
setStoredParams={setStoredParams as any}
|
||||||
|
storedParams={storedParams as any}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = testServerSetup();
|
||||||
|
|
||||||
|
const setupApi = () => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/projects/default/revive',
|
||||||
|
{},
|
||||||
|
'post',
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
|
||||||
|
testServerRoute(server, '/api/admin/projects/default/overview', {
|
||||||
|
environment: 'Open Source',
|
||||||
|
});
|
||||||
|
testServerRoute(server, '/api/admin/ui-config', {
|
||||||
|
archivedAt: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should load the table', async () => {
|
||||||
|
render(<Component />, { permissions: [{ permission: UPDATE_FEATURE }] });
|
||||||
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await screen.findByText('someFeature');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show confirm dialog when reviving flag', async () => {
|
||||||
|
setupApi();
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<ToastRenderer />
|
||||||
|
<Component />
|
||||||
|
</>,
|
||||||
|
{ permissions: [{ permission: UPDATE_FEATURE }] },
|
||||||
|
);
|
||||||
|
await screen.findByText('someFeature');
|
||||||
|
|
||||||
|
const reviveButton = screen.getAllByTestId(
|
||||||
|
'revive-feature-flag-button',
|
||||||
|
)?.[0];
|
||||||
|
fireEvent.click(reviveButton);
|
||||||
|
|
||||||
|
await screen.findByText('Revive feature flag?');
|
||||||
|
const reviveFlagsButton = screen.getByRole('button', {
|
||||||
|
name: /Revive feature flag/i,
|
||||||
|
});
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(reviveFlagsButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
fireEvent.click(reviveFlagsButton);
|
||||||
|
|
||||||
|
await screen.findByText('Feature flags revived');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show info box when disableAllEnvsOnRevive flag is on', async () => {
|
||||||
|
setupApi();
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<ToastRenderer />
|
||||||
|
<Component />
|
||||||
|
</>,
|
||||||
|
{ permissions: [{ permission: UPDATE_FEATURE }] },
|
||||||
|
);
|
||||||
|
await screen.findByText('someFeature');
|
||||||
|
|
||||||
|
const reviveButton = screen.getAllByTestId(
|
||||||
|
'revive-feature-flag-button',
|
||||||
|
)?.[0];
|
||||||
|
fireEvent.click(reviveButton);
|
||||||
|
|
||||||
|
await screen.findByText('Revive feature flag?');
|
||||||
|
await screen.findByText(
|
||||||
|
'Revived feature flags will be automatically disabled in all environments',
|
||||||
|
);
|
||||||
|
});
|
302
frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx
Normal file
302
frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
|
import {
|
||||||
|
type SortingRule,
|
||||||
|
useFlexLayout,
|
||||||
|
useRowSelect,
|
||||||
|
useSortBy,
|
||||||
|
useTable,
|
||||||
|
} from 'react-table';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { useMediaQuery } from '@mui/material';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import { ArchivedFeatureActionCell } from 'component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureActionCell';
|
||||||
|
import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import type { FeatureSearchResponseSchema } from 'openapi';
|
||||||
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import { FeatureArchivedCell } from 'component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { ArchivedFeatureDeleteConfirm } from 'component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm';
|
||||||
|
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
|
import { ArchivedFeatureReviveConfirm } from 'component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureReviveConfirm/ArchivedFeatureReviveConfirm';
|
||||||
|
|
||||||
|
export interface IFeaturesArchiveTableProps {
|
||||||
|
archivedFeatures: FeatureSearchResponseSchema[];
|
||||||
|
title: string;
|
||||||
|
refetch: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
storedParams: SortingRule<string>;
|
||||||
|
setStoredParams: (
|
||||||
|
newValue:
|
||||||
|
| SortingRule<string>
|
||||||
|
| ((prev: SortingRule<string>) => SortingRule<string>),
|
||||||
|
) => SortingRule<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArchiveTable = ({
|
||||||
|
archivedFeatures = [],
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
storedParams,
|
||||||
|
setStoredParams,
|
||||||
|
title,
|
||||||
|
}: IFeaturesArchiveTableProps) => {
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
|
const [deletedFeature, setDeletedFeature] = useState<IFeatureToggle>();
|
||||||
|
|
||||||
|
const [reviveModalOpen, setReviveModalOpen] = useState(false);
|
||||||
|
const [revivedFeature, setRevivedFeature] = useState<IFeatureToggle>();
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState(
|
||||||
|
searchParams.get('search') || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'Seen',
|
||||||
|
accessor: 'lastSeenAt',
|
||||||
|
Cell: ({ row: { original: feature } }: any) => {
|
||||||
|
return <FeatureEnvironmentSeenCell feature={feature} />;
|
||||||
|
},
|
||||||
|
align: 'center',
|
||||||
|
maxWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Type',
|
||||||
|
accessor: 'type',
|
||||||
|
width: 85,
|
||||||
|
canSort: true,
|
||||||
|
Cell: FeatureTypeCell,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
searchable: true,
|
||||||
|
minWidth: 100,
|
||||||
|
Cell: ({ value, row: { original } }: any) => (
|
||||||
|
<HighlightCell
|
||||||
|
value={value}
|
||||||
|
subtitle={original.description}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Created',
|
||||||
|
accessor: 'createdAt',
|
||||||
|
width: 150,
|
||||||
|
Cell: DateCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Archived',
|
||||||
|
accessor: 'archivedAt',
|
||||||
|
width: 150,
|
||||||
|
Cell: FeatureArchivedCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Project ID',
|
||||||
|
accessor: 'project',
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
filterName: 'project',
|
||||||
|
searchable: true,
|
||||||
|
maxWidth: 170,
|
||||||
|
Cell: ({ value }: any) => (
|
||||||
|
<LinkCell title={value} to={`/projects/${value}`} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
maxWidth: 120,
|
||||||
|
canSort: false,
|
||||||
|
Cell: ({ row: { original: feature } }: any) => (
|
||||||
|
<ArchivedFeatureActionCell
|
||||||
|
project={feature.project}
|
||||||
|
onRevive={() => {
|
||||||
|
setRevivedFeature(feature);
|
||||||
|
setReviveModalOpen(true);
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
setDeletedFeature(feature);
|
||||||
|
setDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// Always hidden -- for search
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
header: 'Description',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: searchedData,
|
||||||
|
getSearchText,
|
||||||
|
getSearchContext,
|
||||||
|
} = useSearch(columns, searchValue, archivedFeatures);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() => (loading ? featuresPlaceholder : searchedData),
|
||||||
|
[searchedData, loading],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [initialState] = useState(() => ({
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: searchParams.get('sort') || storedParams.id,
|
||||||
|
desc: searchParams.has('order')
|
||||||
|
? searchParams.get('order') === 'desc'
|
||||||
|
: storedParams.desc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hiddenColumns: ['description'],
|
||||||
|
selectedRowIds: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getRowId = useCallback((row: any) => row.name, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
state: { sortBy },
|
||||||
|
prepareRow,
|
||||||
|
setHiddenColumns,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetHiddenColumns: false,
|
||||||
|
autoResetSelectedRows: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
getRowId,
|
||||||
|
},
|
||||||
|
useFlexLayout,
|
||||||
|
useSortBy,
|
||||||
|
useRowSelect,
|
||||||
|
);
|
||||||
|
|
||||||
|
useConditionallyHiddenColumns(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: isSmallScreen,
|
||||||
|
columns: ['type', 'createdAt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: isMediumScreen,
|
||||||
|
columns: ['lastSeenAt', 'stale'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setHiddenColumns,
|
||||||
|
columns,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tableState: Record<string, string> = {};
|
||||||
|
tableState.sort = sortBy[0].id;
|
||||||
|
if (sortBy[0].desc) {
|
||||||
|
tableState.order = 'desc';
|
||||||
|
}
|
||||||
|
if (searchValue) {
|
||||||
|
tableState.search = searchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(tableState, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||||
|
}, [loading, sortBy, searchValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContent
|
||||||
|
isLoading={loading}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
titleElement={`${title} (${
|
||||||
|
rows.length < data.length
|
||||||
|
? `${rows.length} of ${data.length}`
|
||||||
|
: data.length
|
||||||
|
})`}
|
||||||
|
actions={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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 flags found matching “
|
||||||
|
{searchValue}”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
None of the feature flags were archived yet.
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ArchivedFeatureDeleteConfirm
|
||||||
|
deletedFeatures={[deletedFeature?.name!]}
|
||||||
|
projectId={deletedFeature?.project!}
|
||||||
|
open={deleteModalOpen}
|
||||||
|
setOpen={setDeleteModalOpen}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
<ArchivedFeatureReviveConfirm
|
||||||
|
revivedFeatures={[revivedFeature?.name!]}
|
||||||
|
projectId={revivedFeature?.project!}
|
||||||
|
open={reviveModalOpen}
|
||||||
|
setOpen={setReviveModalOpen}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
|
import { Tooltip, Typography, useTheme } from '@mui/material';
|
||||||
|
import { formatDateYMD } from 'utils/formatDate';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
|
||||||
|
interface IFeatureArchivedCellProps {
|
||||||
|
value?: string | Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureArchivedCell: FC<IFeatureArchivedCellProps> = ({
|
||||||
|
value: archivedAt,
|
||||||
|
}) => {
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (!archivedAt)
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
<Typography
|
||||||
|
variant='body2'
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
not available
|
||||||
|
</Typography>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
<Tooltip
|
||||||
|
title={`Archived on: ${formatDateYMD(
|
||||||
|
archivedAt,
|
||||||
|
locationSettings.locale,
|
||||||
|
)}`}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Typography noWrap variant='body2' data-loading>
|
||||||
|
<TimeAgo date={new Date(archivedAt)} refresh={false} />
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
};
|
34
frontend/src/component/archive/FeaturesArchiveTable.tsx
Normal file
34
frontend/src/component/archive/FeaturesArchiveTable.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ArchiveTable } from 'component/archive/ArchiveTable/ArchiveTable';
|
||||||
|
import type { SortingRule } from 'react-table';
|
||||||
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||||
|
|
||||||
|
const defaultSort: SortingRule<string> = { id: 'createdAt' };
|
||||||
|
const { value, setValue } = createLocalStorage(
|
||||||
|
'FeaturesArchiveTable:v1',
|
||||||
|
defaultSort,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FeaturesArchiveTable = () => {
|
||||||
|
usePageTitle('Archive');
|
||||||
|
|
||||||
|
const {
|
||||||
|
features: archivedFeatures,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
} = useFeatureSearch({
|
||||||
|
archived: 'IS:true',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArchiveTable
|
||||||
|
title='Archive'
|
||||||
|
archivedFeatures={archivedFeatures}
|
||||||
|
loading={loading}
|
||||||
|
storedParams={value}
|
||||||
|
setStoredParams={setValue}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -432,6 +432,13 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Login history",
|
"title": "Login history",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"menu": {},
|
||||||
|
"path": "/archive",
|
||||||
|
"title": "Archived flags",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": {
|
"component": {
|
||||||
"$$typeof": Symbol(react.lazy),
|
"$$typeof": Symbol(react.lazy),
|
||||||
|
@ -30,6 +30,7 @@ import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
|||||||
import type { INavigationMenuItem, IRoute } from 'interfaces/route';
|
import type { INavigationMenuItem, IRoute } from 'interfaces/route';
|
||||||
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
||||||
import { SegmentTable } from '../segments/SegmentTable/SegmentTable.jsx';
|
import { SegmentTable } from '../segments/SegmentTable/SegmentTable.jsx';
|
||||||
|
import { FeaturesArchiveTable } from 'component/archive/FeaturesArchiveTable';
|
||||||
import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
|
import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
|
||||||
import { Profile } from 'component/user/Profile/Profile';
|
import { Profile } from 'component/user/Profile/Profile';
|
||||||
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
||||||
@ -446,6 +447,15 @@ export const routes: IRoute[] = [
|
|||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Archive
|
||||||
|
{
|
||||||
|
path: '/archive',
|
||||||
|
title: 'Archived flags',
|
||||||
|
component: FeaturesArchiveTable,
|
||||||
|
type: 'protected',
|
||||||
|
menu: {},
|
||||||
|
},
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
{
|
{
|
||||||
path: '/admin/*',
|
path: '/admin/*',
|
||||||
|
Loading…
Reference in New Issue
Block a user