mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
refactor: remove deprecated get archive featured by project endpoint (#9938)
https://linear.app/unleash/issue/2-3367/remove-get-apiadminarchivefeaturesprojectid-deprecated-in-4110 Removes GET `/api/admin/archive/features/{projectId}` which was deprecated in v4.11. Also cleans up related code. I leveraged our search features endpoint where needed, since that's what we're using now in our UI (to see archived features you filter by archived=true). Builds on top of https://github.com/Unleash/unleash/pull/9924 — Since they're both related.
This commit is contained in:
parent
42b6fc810e
commit
0429c1985a
@ -7,7 +7,6 @@ import {
|
||||
UPDATE_FEATURE,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
|
||||
import { ArchivedFeatureDeleteConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { ArchivedFeatureReviveConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureReviveConfirm/ArchivedFeatureReviveConfirm';
|
||||
@ -23,7 +22,6 @@ export const ArchiveBatchActions: FC<IArchiveBatchActionsProps> = ({
|
||||
projectId,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const { refetchArchived } = useFeaturesArchive(projectId);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [reviveModalOpen, setReviveModalOpen] = useState(false);
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
@ -70,7 +68,6 @@ export const ArchiveBatchActions: FC<IArchiveBatchActionsProps> = ({
|
||||
open={deleteModalOpen}
|
||||
setOpen={setDeleteModalOpen}
|
||||
refetch={() => {
|
||||
refetchArchived();
|
||||
onConfirm?.();
|
||||
trackEvent('batch_operations', {
|
||||
props: {
|
||||
@ -85,7 +82,6 @@ export const ArchiveBatchActions: FC<IArchiveBatchActionsProps> = ({
|
||||
open={reviveModalOpen}
|
||||
setOpen={setReviveModalOpen}
|
||||
refetch={() => {
|
||||
refetchArchived();
|
||||
onConfirm?.();
|
||||
trackEvent('batch_operations', {
|
||||
props: {
|
||||
|
@ -1,160 +0,0 @@
|
||||
import { ArchiveTable } from './ArchiveTable';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { useState } from 'react';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
|
||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||
|
||||
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',
|
||||
archived: true,
|
||||
},
|
||||
{
|
||||
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',
|
||||
archived: true,
|
||||
},
|
||||
];
|
||||
|
||||
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}
|
||||
projectId='default'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 confirm dialog when batch reviving flag', async () => {
|
||||
setupApi();
|
||||
render(
|
||||
<>
|
||||
<ToastRenderer />
|
||||
<Component />
|
||||
</>,
|
||||
{
|
||||
permissions: [
|
||||
{ permission: UPDATE_FEATURE, project: 'default' },
|
||||
{ permission: DELETE_FEATURE, project: 'default' },
|
||||
],
|
||||
},
|
||||
);
|
||||
await screen.findByText('someFeature');
|
||||
|
||||
const selectAll = await screen.findByTestId('select_all_rows');
|
||||
fireEvent.click(selectAll.firstChild!);
|
||||
const batchReviveButton = await screen.findByText(/Revive/i);
|
||||
await userEvent.click(batchReviveButton!);
|
||||
|
||||
await screen.findByText('Revive feature flags?');
|
||||
|
||||
const reviveTogglesButton = screen.getByRole('button', {
|
||||
name: /Revive feature flags/i,
|
||||
});
|
||||
fireEvent.click(reviveTogglesButton);
|
||||
|
||||
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',
|
||||
);
|
||||
});
|
@ -1,320 +0,0 @@
|
||||
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 { Checkbox, 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 { ArchivedFeatureActionCell } from 'component/archive/ArchiveTable/ArchivedFeatureActionCell/ArchivedFeatureActionCell';
|
||||
import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
||||
import theme from 'themes/theme';
|
||||
import type { ArchivedFeatureSchema } from 'openapi';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { FeatureArchivedCell } from './FeatureArchivedCell/FeatureArchivedCell';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { ArchivedFeatureDeleteConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||
import { RowSelectCell } from '../../project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||
import { BatchSelectionActionsBar } from '../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||
import { ArchiveBatchActions } from './ArchiveBatchActions';
|
||||
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import { ArchivedFeatureReviveConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureReviveConfirm/ArchivedFeatureReviveConfirm';
|
||||
|
||||
export interface IFeaturesArchiveTableProps {
|
||||
archivedFeatures: ArchivedFeatureSchema[];
|
||||
title: string;
|
||||
refetch: () => void;
|
||||
loading: boolean;
|
||||
storedParams: SortingRule<string>;
|
||||
setStoredParams: (
|
||||
newValue:
|
||||
| SortingRule<string>
|
||||
| ((prev: SortingRule<string>) => SortingRule<string>),
|
||||
) => SortingRule<string>;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const ArchiveTable = ({
|
||||
archivedFeatures = [],
|
||||
loading,
|
||||
refetch,
|
||||
storedParams,
|
||||
setStoredParams,
|
||||
title,
|
||||
projectId,
|
||||
}: 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(
|
||||
() => [
|
||||
{
|
||||
id: 'Select',
|
||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
||||
<Checkbox
|
||||
data-testid='select_all_rows'
|
||||
{...getToggleAllRowsSelectedProps()}
|
||||
/>
|
||||
),
|
||||
Cell: ({ row }: any) => (
|
||||
<RowSelectCell {...row?.getToggleRowSelectedProps?.()} />
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
{
|
||||
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: '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, selectedRowIds },
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
toggleAllRowsSelected,
|
||||
} = 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={projectId}
|
||||
open={deleteModalOpen}
|
||||
setOpen={setDeleteModalOpen}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<ArchivedFeatureReviveConfirm
|
||||
revivedFeatures={[revivedFeature?.name!]}
|
||||
projectId={projectId}
|
||||
open={reviveModalOpen}
|
||||
setOpen={setReviveModalOpen}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</PageContent>
|
||||
<BatchSelectionActionsBar
|
||||
count={Object.keys(selectedRowIds).length}
|
||||
>
|
||||
<ArchiveBatchActions
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
projectId={projectId}
|
||||
onConfirm={() => toggleAllRowsSelected(false)}
|
||||
/>
|
||||
</BatchSelectionActionsBar>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,35 +0,0 @@
|
||||
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
|
||||
import type { FC } from 'react';
|
||||
import type { SortingRule } from 'react-table';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { ArchiveTable } from './ArchiveTable/ArchiveTable';
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'archivedAt' };
|
||||
|
||||
interface IProjectFeaturesTable {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const ProjectFeaturesArchiveTable: FC<IProjectFeaturesTable> = ({
|
||||
projectId,
|
||||
}) => {
|
||||
const { archivedFeatures, loading, refetchArchived } =
|
||||
useFeaturesArchive(projectId);
|
||||
|
||||
const { value, setValue } = createLocalStorage(
|
||||
`${projectId}:ProjectFeaturesArchiveTable`,
|
||||
defaultSort,
|
||||
);
|
||||
|
||||
return (
|
||||
<ArchiveTable
|
||||
title='Archived flags'
|
||||
archivedFeatures={archivedFeatures || []}
|
||||
loading={loading}
|
||||
storedParams={value}
|
||||
setStoredParams={setValue}
|
||||
refetch={refetchArchived}
|
||||
projectId={projectId}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
const RedirectArchive = () => {
|
||||
return <Navigate to='/archive' replace />;
|
||||
};
|
||||
|
||||
export default RedirectArchive;
|
@ -1,8 +1,8 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getCreateTogglePath } from 'utils/routePathHelpers';
|
||||
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { styled } from '@mui/material';
|
||||
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
|
||||
const StyledFeatureId = styled('strong')({
|
||||
wordBreak: 'break-all',
|
||||
@ -11,7 +11,10 @@ const StyledFeatureId = styled('strong')({
|
||||
export const FeatureNotFound = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { archivedFeatures } = useFeaturesArchive(projectId);
|
||||
const { features: archivedFeatures } = useFeatureSearch({
|
||||
project: `IS:${projectId}`,
|
||||
archived: 'IS:true',
|
||||
});
|
||||
|
||||
const createFeatureTogglePath = getCreateTogglePath(projectId, {
|
||||
name: featureId,
|
||||
|
@ -35,14 +35,6 @@ exports[`returns all baseRoutes 1`] = `
|
||||
"title": "Create",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"menu": {},
|
||||
"parent": "/archive",
|
||||
"path": "/projects/:projectId/archived",
|
||||
"title": ":projectId",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"menu": {},
|
||||
|
@ -10,7 +10,6 @@ import ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
||||
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
|
||||
import { ProjectList } from 'component/project/ProjectList/ProjectList';
|
||||
import { ArchiveProjectList } from 'component/project/ProjectList/ArchiveProjectList';
|
||||
import RedirectArchive from 'component/archive/RedirectArchive';
|
||||
import CreateEnvironment from 'component/environments/CreateEnvironment/CreateEnvironment';
|
||||
import EditEnvironment from 'component/environments/EditEnvironment/EditEnvironment';
|
||||
import { EditContext } from 'component/context/EditContext/EditContext';
|
||||
@ -80,14 +79,6 @@ export const routes: IRoute[] = [
|
||||
enterprise: true,
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/archived',
|
||||
title: ':projectId',
|
||||
parent: '/archive',
|
||||
component: RedirectArchive,
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/features/:featureId/copy',
|
||||
parent: '/projects/:projectId/features/:featureId/',
|
||||
|
@ -27,7 +27,6 @@ import useToast from 'hooks/useToast';
|
||||
import useQueryParams from 'hooks/useQueryParams';
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
||||
import { ProjectFeaturesArchive } from './ProjectFeaturesArchive/ProjectFeaturesArchive';
|
||||
import ProjectFlags from './ProjectFlags';
|
||||
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
@ -387,7 +386,6 @@ export const Project = () => {
|
||||
}
|
||||
/>
|
||||
<Route path='environments' element={<ProjectEnvironment />} />
|
||||
<Route path='archive' element={<ProjectFeaturesArchive />} />
|
||||
<Route path='insights' element={<ProjectInsights />} />
|
||||
<Route path='logs' element={<ProjectLog />} />
|
||||
<Route
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { ProjectFeaturesArchiveTable } from 'component/archive/ProjectFeaturesArchiveTable';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||
|
||||
export const ProjectFeaturesArchive = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const projectName = useProjectOverviewNameOrId(projectId);
|
||||
usePageTitle(`Project archived flags – ${projectName}`);
|
||||
|
||||
return <ProjectFeaturesArchiveTable projectId={projectId} />;
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import useSWR from 'swr';
|
||||
import type { ArchivedFeaturesSchema } from 'openapi';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Feature flag archive'))
|
||||
.then((res) => res.json());
|
||||
};
|
||||
|
||||
export const useFeaturesArchive = (projectId: string) => {
|
||||
const { data, error, mutate, isLoading } = useSWR<ArchivedFeaturesSchema>(
|
||||
formatApiPath(`/api/admin/archive/features/${projectId}`),
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 15 * 1000, // ms
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
archivedFeatures: data?.features,
|
||||
refetchArchived: mutate,
|
||||
loading: isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { ArchivedFeatureSchemaEnvironmentsItem } from './archivedFeatureSchemaEnvironmentsItem';
|
||||
|
||||
/**
|
||||
* An archived project feature flag definition
|
||||
*/
|
||||
export interface ArchivedFeatureSchema {
|
||||
/** The date the feature was archived */
|
||||
archivedAt?: string;
|
||||
/** The date the feature was created */
|
||||
createdAt?: string;
|
||||
/**
|
||||
* Detailed description of the feature
|
||||
* @nullable
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* The list of environments where the feature can be used
|
||||
* @deprecated
|
||||
*/
|
||||
environments?: ArchivedFeatureSchemaEnvironmentsItem[];
|
||||
/** `true` if the impression data collection is enabled for the feature, otherwise `false`. */
|
||||
impressionData?: boolean;
|
||||
/**
|
||||
* The date when metrics where last collected for the feature. This field was deprecated in v5, use the one in featureEnvironmentSchema
|
||||
* @deprecated
|
||||
* @nullable
|
||||
*/
|
||||
lastSeenAt?: string | null;
|
||||
/** Unique feature name */
|
||||
name: string;
|
||||
/** Name of the project the feature belongs to */
|
||||
project: string;
|
||||
/** `true` if the feature is stale based on the age and feature type, otherwise `false`. */
|
||||
stale?: boolean;
|
||||
/** Type of the flag e.g. experiment, kill-switch, release, operational, permission */
|
||||
type?: string;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type ArchivedFeatureSchemaEnvironmentsItem = {
|
||||
/** `true` if the feature is enabled for the environment, otherwise `false`. */
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* The date when metrics where last collected for the feature environment
|
||||
* @nullable
|
||||
*/
|
||||
lastSeenAt?: string | null;
|
||||
/** The name of the environment */
|
||||
name?: string;
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { ArchivedFeatureSchema } from './archivedFeatureSchema';
|
||||
|
||||
/**
|
||||
* A list of archived features
|
||||
*/
|
||||
export interface ArchivedFeaturesSchema {
|
||||
/** A list of features */
|
||||
features: ArchivedFeatureSchema[];
|
||||
/** The version of the feature's schema */
|
||||
version: number;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetArchivedFeatures401 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetArchivedFeatures403 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetArchivedFeaturesByProjectId401 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetArchivedFeaturesByProjectId403 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -124,9 +124,6 @@ export * from './archiveProject401';
|
||||
export * from './archiveProject403';
|
||||
export * from './archiveReleasePlanTemplate401';
|
||||
export * from './archiveReleasePlanTemplate403';
|
||||
export * from './archivedFeatureSchema';
|
||||
export * from './archivedFeatureSchemaEnvironmentsItem';
|
||||
export * from './archivedFeaturesSchema';
|
||||
export * from './bannerSchema';
|
||||
export * from './bannersSchema';
|
||||
export * from './batchFeaturesSchema';
|
||||
@ -710,10 +707,6 @@ export * from './getApplication404';
|
||||
export * from './getApplicationEnvironmentInstances404';
|
||||
export * from './getApplicationOverview404';
|
||||
export * from './getApplicationsParams';
|
||||
export * from './getArchivedFeatures401';
|
||||
export * from './getArchivedFeatures403';
|
||||
export * from './getArchivedFeaturesByProjectId401';
|
||||
export * from './getArchivedFeaturesByProjectId403';
|
||||
export * from './getBanners401';
|
||||
export * from './getBaseUsersAndGroups401';
|
||||
export * from './getChangeRequest404';
|
||||
|
@ -223,7 +223,9 @@ const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
|
||||
};
|
||||
const getProjectArchive = async (projectId = 'default', expectedCode = 200) => {
|
||||
return app.request
|
||||
.get(`/api/admin/archive/features/${projectId}`)
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${projectId}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect(expectedCode);
|
||||
};
|
||||
|
||||
|
@ -1,23 +1,17 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Response } from 'express';
|
||||
import type { IUnleashConfig } from '../../types/option';
|
||||
import type { IUnleashServices } from '../../types';
|
||||
import Controller from '../../routes/controller';
|
||||
import { extractUsername } from '../../util/extract-user';
|
||||
import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions';
|
||||
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../types/permissions';
|
||||
import type FeatureToggleService from './feature-toggle-service';
|
||||
import type { IAuthRequest } from '../../routes/unleash-types';
|
||||
import { serializeDates } from '../../types/serialize-dates';
|
||||
import type { OpenApiService } from '../../services/openapi-service';
|
||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||
import {
|
||||
emptyResponse,
|
||||
getStandardResponses,
|
||||
} from '../../openapi/util/standard-responses';
|
||||
import type { WithTransactional } from '../../db/transaction';
|
||||
import {
|
||||
archivedFeaturesSchema,
|
||||
type ArchivedFeaturesSchema,
|
||||
} from '../../openapi';
|
||||
|
||||
export default class ArchiveController extends Controller {
|
||||
private featureService: FeatureToggleService;
|
||||
@ -43,28 +37,6 @@ export default class ArchiveController extends Controller {
|
||||
this.transactionalFeatureToggleService =
|
||||
transactionalFeatureToggleService;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/features/:projectId',
|
||||
handler: this.getArchivedFeaturesByProjectId,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Archive'],
|
||||
operationId: 'getArchivedFeaturesByProjectId',
|
||||
summary: 'Get archived features in project',
|
||||
description:
|
||||
'Retrieves a list of archived features that belong to the provided project.',
|
||||
responses: {
|
||||
200: createResponseSchema('archivedFeaturesSchema'),
|
||||
...getStandardResponses(401, 403),
|
||||
},
|
||||
|
||||
deprecated: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'delete',
|
||||
path: '/:featureName',
|
||||
@ -108,21 +80,6 @@ export default class ArchiveController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
async getArchivedFeaturesByProjectId(
|
||||
req: Request<{ projectId: string }, any, any, any>,
|
||||
res: Response<ArchivedFeaturesSchema>,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
const features =
|
||||
await this.featureService.getArchivedFeaturesByProjectId(projectId);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
archivedFeaturesSchema.$id,
|
||||
{ version: 2, features: serializeDates(features) },
|
||||
);
|
||||
}
|
||||
|
||||
async deleteFeature(
|
||||
req: IAuthRequest<{ featureName: string }>,
|
||||
res: Response<void>,
|
||||
|
@ -220,30 +220,4 @@ export class FeatureToggleRowConverter {
|
||||
|
||||
return this.formatToggles(result);
|
||||
};
|
||||
|
||||
buildArchivedFeatureToggleListFromRows = (
|
||||
rows: any[],
|
||||
): IFeatureToggleListItem[] => {
|
||||
const result = rows.reduce((acc, row) => {
|
||||
const feature: PartialDeep<IFeatureToggleListItem> =
|
||||
acc[row.name] ?? {};
|
||||
|
||||
feature.name = row.name;
|
||||
feature.description = row.description;
|
||||
feature.type = row.type;
|
||||
feature.project = row.project;
|
||||
feature.stale = row.stale;
|
||||
feature.createdAt = row.created_at;
|
||||
feature.impressionData = row.impression_data;
|
||||
feature.lastSeenAt = row.last_seen_at;
|
||||
feature.archivedAt = row.archived_at;
|
||||
|
||||
this.addLastSeenByEnvironment(feature, row);
|
||||
|
||||
acc[row.name] = feature;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.values(result);
|
||||
};
|
||||
}
|
||||
|
@ -178,10 +178,6 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
return this.features.filter((feature) => feature.archived !== archived);
|
||||
}
|
||||
|
||||
async getArchivedFeatures(project: string): Promise<FeatureToggle[]> {
|
||||
return this.features.filter((feature) => feature.archived === true);
|
||||
}
|
||||
|
||||
async getPlaygroundFeatures(
|
||||
query?: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
|
@ -2162,12 +2162,6 @@ class FeatureToggleService {
|
||||
);
|
||||
}
|
||||
|
||||
async getArchivedFeaturesByProjectId(
|
||||
project: string,
|
||||
): Promise<FeatureToggle[]> {
|
||||
return this.featureToggleStore.getArchivedFeatures(project);
|
||||
}
|
||||
|
||||
async getProjectId(name: string): Promise<string | undefined> {
|
||||
return this.featureToggleStore.getProjectId(name);
|
||||
}
|
||||
|
@ -258,32 +258,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
return rows.map(this.rowToFeature);
|
||||
}
|
||||
|
||||
async getArchivedFeatures(project: string): Promise<FeatureToggle[]> {
|
||||
const builder = new FeatureToggleListBuilder(this.db, [
|
||||
...commonSelectColumns,
|
||||
'features.archived_at as archived_at',
|
||||
]);
|
||||
|
||||
const archived = true;
|
||||
builder.query('features').withLastSeenByEnvironment(archived);
|
||||
|
||||
builder.addSelectColumn(
|
||||
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||
);
|
||||
builder.addSelectColumn(
|
||||
'last_seen_at_metrics.environment as last_seen_at_env',
|
||||
);
|
||||
|
||||
const rows = await builder.internalQuery
|
||||
.select(builder.getSelectColumns())
|
||||
.where({ project })
|
||||
.whereNotNull('archived_at');
|
||||
|
||||
return this.featureToggleRowConverter.buildArchivedFeatureToggleListFromRows(
|
||||
rows,
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureTypeCounts({
|
||||
projectId,
|
||||
archived,
|
||||
|
@ -54,37 +54,42 @@ test('Should be allowed to reuse deleted toggle name', async () => {
|
||||
test('Should get archived toggles via project', async () => {
|
||||
await db.stores.featureToggleStore.deleteAll();
|
||||
|
||||
const proj1 = 'proj-1';
|
||||
const proj2 = 'proj-2';
|
||||
|
||||
await db.stores.projectStore.create({
|
||||
id: 'proj-1',
|
||||
name: 'proj-1',
|
||||
id: proj1,
|
||||
name: proj1,
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await db.stores.projectStore.create({
|
||||
id: 'proj-2',
|
||||
name: 'proj-2',
|
||||
id: proj2,
|
||||
name: proj2,
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
|
||||
await db.stores.featureToggleStore.create('proj-1', {
|
||||
await db.stores.featureToggleStore.create(proj1, {
|
||||
name: 'feat-proj-1',
|
||||
archived: true,
|
||||
createdByUserId: 9999,
|
||||
});
|
||||
await db.stores.featureToggleStore.create('proj-2', {
|
||||
await db.stores.featureToggleStore.create(proj2, {
|
||||
name: 'feat-proj-2',
|
||||
archived: true,
|
||||
createdByUserId: 9999,
|
||||
});
|
||||
await db.stores.featureToggleStore.create('proj-2', {
|
||||
await db.stores.featureToggleStore.create(proj2, {
|
||||
name: 'feat-proj-2-2',
|
||||
archived: true,
|
||||
createdByUserId: 9999,
|
||||
});
|
||||
|
||||
await app.request
|
||||
.get('/api/admin/archive/features/proj-1')
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${proj1}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
@ -92,7 +97,9 @@ test('Should get archived toggles via project', async () => {
|
||||
});
|
||||
|
||||
await app.request
|
||||
.get('/api/admin/archive/features/proj-2')
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${proj2}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
|
@ -80,28 +80,6 @@ test('response should include last seen at per environment for multiple environm
|
||||
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||
});
|
||||
|
||||
test('response should include last seen at per environment for multiple environments in /api/admin/archive/features/:projectId', async () => {
|
||||
const featureName = 'multiple-environment-last-seen-at-archived-project';
|
||||
await setupLastSeenAtTest(featureName);
|
||||
|
||||
await app.request
|
||||
.delete(`/api/admin/projects/default/features/${featureName}`)
|
||||
.expect(202);
|
||||
|
||||
const { body } = await app.request.get(
|
||||
`/api/admin/archive/features/default`,
|
||||
);
|
||||
|
||||
const featureEnvironments = body.features[0].environments;
|
||||
const [development, production] = featureEnvironments;
|
||||
|
||||
expect(development.name).toBe('development');
|
||||
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||
|
||||
expect(production.name).toBe('production');
|
||||
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||
});
|
||||
|
||||
test('response should include last seen at per environment correctly for a single toggle /api/admin/project/:projectId/features/:featureName', async () => {
|
||||
const featureName = 'multiple-environment-last-seen-at-single-toggle';
|
||||
await app.createFeature(featureName);
|
||||
|
@ -1047,7 +1047,9 @@ test('Should archive feature flag', async () => {
|
||||
|
||||
await app.request.get(`${url}/${name}`).expect(404);
|
||||
const { body } = await app.request
|
||||
.get(`/api/admin/archive/features/${projectId}`)
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${projectId}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const flag = body.features.find((f) => f.name === name);
|
||||
|
@ -54,8 +54,6 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||
archived?: boolean,
|
||||
): Promise<FeatureToggle[]>;
|
||||
|
||||
getArchivedFeatures(project: string): Promise<FeatureToggle[]>;
|
||||
|
||||
getPlaygroundFeatures(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]>;
|
||||
|
@ -1,102 +0,0 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const archivedFeatureSchema = {
|
||||
$id: '#/components/schemas/archivedFeatureSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'project'],
|
||||
description: 'An archived project feature flag definition',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
example: 'disable-comments',
|
||||
description: 'Unique feature name',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
example: 'kill-switch',
|
||||
description:
|
||||
'Type of the flag e.g. experiment, kill-switch, release, operational, permission',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
example:
|
||||
'Controls disabling of the comments section in case of an incident',
|
||||
description: 'Detailed description of the feature',
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
example: 'dx-squad',
|
||||
description: 'Name of the project the feature belongs to',
|
||||
},
|
||||
stale: {
|
||||
type: 'boolean',
|
||||
example: false,
|
||||
description:
|
||||
'`true` if the feature is stale based on the age and feature type, otherwise `false`.',
|
||||
},
|
||||
impressionData: {
|
||||
type: 'boolean',
|
||||
example: false,
|
||||
description:
|
||||
'`true` if the impression data collection is enabled for the feature, otherwise `false`.',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
example: '2023-01-28T15:21:39.975Z',
|
||||
description: 'The date the feature was created',
|
||||
},
|
||||
archivedAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
example: '2023-01-29T15:21:39.975Z',
|
||||
description: 'The date the feature was archived',
|
||||
},
|
||||
lastSeenAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
nullable: true,
|
||||
deprecated: true,
|
||||
example: '2023-01-28T16:21:39.975Z',
|
||||
description:
|
||||
'The date when metrics where last collected for the feature. This field was deprecated in v5, use the one in featureEnvironmentSchema',
|
||||
},
|
||||
environments: {
|
||||
type: 'array',
|
||||
deprecated: true,
|
||||
description:
|
||||
'The list of environments where the feature can be used',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
example: 'my-dev-env',
|
||||
description: 'The name of the environment',
|
||||
},
|
||||
lastSeenAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
nullable: true,
|
||||
example: '2023-01-28T16:21:39.975Z',
|
||||
description:
|
||||
'The date when metrics where last collected for the feature environment',
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
example: true,
|
||||
description:
|
||||
'`true` if the feature is enabled for the environment, otherwise `false`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ArchivedFeatureSchema = FromSchema<typeof archivedFeatureSchema>;
|
@ -1,30 +0,0 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
import { archivedFeatureSchema } from './archived-feature-schema';
|
||||
|
||||
export const archivedFeaturesSchema = {
|
||||
$id: '#/components/schemas/archivedFeaturesSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['version', 'features'],
|
||||
description: 'A list of archived features',
|
||||
properties: {
|
||||
version: {
|
||||
type: 'integer',
|
||||
description: "The version of the feature's schema",
|
||||
},
|
||||
features: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/archivedFeatureSchema',
|
||||
},
|
||||
description: 'A list of features',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
archivedFeatureSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ArchivedFeaturesSchema = FromSchema<typeof archivedFeaturesSchema>;
|
@ -24,8 +24,6 @@ export * from './application-overview-schema';
|
||||
export * from './application-schema';
|
||||
export * from './application-usage-schema';
|
||||
export * from './applications-schema';
|
||||
export * from './archived-feature-schema';
|
||||
export * from './archived-features-schema';
|
||||
export * from './batch-features-schema';
|
||||
export * from './batch-stale-schema';
|
||||
export * from './bulk-metrics-schema';
|
||||
|
@ -68,7 +68,9 @@ afterAll(async () => {
|
||||
test('returns three archived flags', async () => {
|
||||
expect.assertions(1);
|
||||
return app.request
|
||||
.get(`/api/admin/archive/features/${DEFAULT_PROJECT}`)
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${DEFAULT_PROJECT}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
@ -79,7 +81,9 @@ test('returns three archived flags', async () => {
|
||||
test('returns three archived flags with archivedAt', async () => {
|
||||
expect.assertions(2);
|
||||
return app.request
|
||||
.get(`/api/admin/archive/features/${DEFAULT_PROJECT}`)
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${DEFAULT_PROJECT}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
@ -238,7 +242,9 @@ test('Should be able to bulk archive features', async () => {
|
||||
.expect(202);
|
||||
|
||||
const { body } = await app.request
|
||||
.get(`/api/admin/archive/features/${DEFAULT_PROJECT}`)
|
||||
.get(
|
||||
`/api/admin/search/features?project=IS%3A${DEFAULT_PROJECT}&archived=IS%3Atrue`,
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const archivedFeatures = body.features.filter(
|
||||
|
Loading…
Reference in New Issue
Block a user