1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00

refactor: remove deprecated GET archive features endpoint (#9924)

https://linear.app/unleash/issue/2-3366/remove-get-apiadminarchivefeatures-deprecated-in-4100

Removes GET `/api/admin/archive/features` which was deprecated in v4.10.
Also cleans up related code.

May include some slight scouting.

**P.S.** Should we merge this into main, or is there a `v7` branch we
should be targeting instead?
This commit is contained in:
Nuno Góis 2025-05-13 11:45:03 +01:00 committed by GitHub
parent 410142cb42
commit 42b6fc810e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 46 additions and 302 deletions

View File

@ -17,7 +17,6 @@ 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';
@ -32,7 +31,6 @@ import { RowSelectCell } from '../../project/Project/ProjectFeatureToggles/RowSe
import { BatchSelectionActionsBar } from '../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
import { ArchiveBatchActions } from './ArchiveBatchActions';
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ArchivedFeatureReviveConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureReviveConfirm/ArchivedFeatureReviveConfirm';
export interface IFeaturesArchiveTableProps {
@ -46,7 +44,7 @@ export interface IFeaturesArchiveTableProps {
| SortingRule<string>
| ((prev: SortingRule<string>) => SortingRule<string>),
) => SortingRule<string>;
projectId?: string;
projectId: string;
}
export const ArchiveTable = ({
@ -72,35 +70,27 @@ export const ArchiveTable = ({
searchParams.get('search') || '',
);
const { uiConfig } = useUiConfig();
const columns = useMemo(
() => [
...(projectId
? [
{
id: 'Select',
Header: ({ getToggleAllRowsSelectedProps }: any) => (
<Checkbox
data-testid='select_all_rows'
{...getToggleAllRowsSelectedProps()}
/>
),
Cell: ({ row }: any) => (
<RowSelectCell
{...row?.getToggleRowSelectedProps?.()}
/>
),
maxWidth: 50,
disableSortBy: true,
hideInMenu: true,
},
]
: []),
{
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: ({ value, row: { original: feature } }: any) => {
Cell: ({ row: { original: feature } }: any) => {
return <FeatureEnvironmentSeenCell feature={feature} />;
},
align: 'center',
@ -139,24 +129,6 @@ export const ArchiveTable = ({
width: 150,
Cell: FeatureArchivedCell,
},
...(!projectId
? [
{
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',
@ -184,8 +156,7 @@ export const ArchiveTable = ({
searchable: true,
},
],
//eslint-disable-next-line
[projectId],
[],
);
const {
@ -270,7 +241,7 @@ export const ArchiveTable = ({
replace: true,
});
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
}, [loading, sortBy, searchValue]);
return (
<>
@ -322,33 +293,28 @@ export const ArchiveTable = ({
/>
<ArchivedFeatureDeleteConfirm
deletedFeatures={[deletedFeature?.name!]}
projectId={projectId ?? deletedFeature?.project!}
projectId={projectId}
open={deleteModalOpen}
setOpen={setDeleteModalOpen}
refetch={refetch}
/>
<ArchivedFeatureReviveConfirm
revivedFeatures={[revivedFeature?.name!]}
projectId={projectId ?? revivedFeature?.project!}
projectId={projectId}
open={reviveModalOpen}
setOpen={setReviveModalOpen}
refetch={refetch}
/>
</PageContent>
<ConditionallyRender
condition={Boolean(projectId)}
show={
<BatchSelectionActionsBar
count={Object.keys(selectedRowIds).length}
>
<ArchiveBatchActions
selectedIds={Object.keys(selectedRowIds)}
projectId={projectId!}
onConfirm={() => toggleAllRowsSelected(false)}
/>
</BatchSelectionActionsBar>
}
/>
<BatchSelectionActionsBar
count={Object.keys(selectedRowIds).length}
>
<ArchiveBatchActions
selectedIds={Object.keys(selectedRowIds)}
projectId={projectId}
onConfirm={() => toggleAllRowsSelected(false)}
/>
</BatchSelectionActionsBar>
</>
);
};

View File

@ -1,32 +0,0 @@
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
import { ArchiveTable } from './ArchiveTable/ArchiveTable';
import type { SortingRule } from 'react-table';
import { usePageTitle } from 'hooks/usePageTitle';
import { createLocalStorage } from 'utils/createLocalStorage';
const defaultSort: SortingRule<string> = { id: 'createdAt' };
const { value, setValue } = createLocalStorage(
'FeaturesArchiveTable:v1',
defaultSort,
);
export const FeaturesArchiveTable = () => {
usePageTitle('Archive');
const {
archivedFeatures = [],
loading,
refetchArchived,
} = useFeaturesArchive();
return (
<ArchiveTable
title='Archive'
archivedFeatures={archivedFeatures}
loading={loading}
storedParams={value}
setStoredParams={setValue}
refetch={refetchArchived}
/>
);
};

View File

@ -11,7 +11,7 @@ const StyledFeatureId = styled('strong')({
export const FeatureNotFound = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { archivedFeatures } = useFeaturesArchive();
const { archivedFeatures } = useFeaturesArchive(projectId);
const createFeatureTogglePath = getCreateTogglePath(projectId, {
name: featureId,

View File

@ -440,13 +440,6 @@ exports[`returns all baseRoutes 1`] = `
"title": "Login history",
"type": "protected",
},
{
"component": [Function],
"menu": {},
"path": "/archive",
"title": "Archived flags",
"type": "protected",
},
{
"component": {
"$$typeof": Symbol(react.lazy),

View File

@ -31,7 +31,6 @@ import { EditSegment } from 'component/segments/EditSegment/EditSegment';
import type { INavigationMenuItem, IRoute } from 'interfaces/route';
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
import { SegmentTable } from '../segments/SegmentTable/SegmentTable';
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
import { Profile } from 'component/user/Profile/Profile';
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
@ -456,15 +455,6 @@ export const routes: IRoute[] = [
menu: { adminSettings: true },
},
// Archive
{
path: '/archive',
title: 'Archived flags',
component: FeaturesArchiveTable,
type: 'protected',
menu: {},
},
// Admin
{
path: '/admin/*',

View File

@ -9,13 +9,9 @@ const fetcher = (path: string) => {
.then((res) => res.json());
};
export const useFeaturesArchive = (projectId?: string) => {
export const useFeaturesArchive = (projectId: string) => {
const { data, error, mutate, isLoading } = useSWR<ArchivedFeaturesSchema>(
formatApiPath(
projectId
? `/api/admin/archive/features/${projectId}`
: 'api/admin/archive/features',
),
formatApiPath(`/api/admin/archive/features/${projectId}`),
fetcher,
{
refreshInterval: 15 * 1000, // ms

View File

@ -2,10 +2,7 @@ import type { Request, Response } from 'express';
import type { IUnleashConfig } from '../../types/option';
import type { IUnleashServices } from '../../types';
import Controller from '../../routes/controller';
import {
extractUserIdFromUser,
extractUsername,
} from '../../util/extract-user';
import { extractUsername } from '../../util/extract-user';
import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions';
import type FeatureToggleService from './feature-toggle-service';
import type { IAuthRequest } from '../../routes/unleash-types';
@ -46,28 +43,6 @@ export default class ArchiveController extends Controller {
this.transactionalFeatureToggleService =
transactionalFeatureToggleService;
this.route({
method: 'get',
path: '/features',
handler: this.getArchivedFeatures,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Archive'],
summary: 'Get archived features',
description:
'Retrieve a list of all [archived feature flags](https://docs.getunleash.io/reference/feature-toggles#archive-a-feature-flag).',
operationId: 'getArchivedFeatures',
responses: {
200: createResponseSchema('archivedFeaturesSchema'),
...getStandardResponses(401, 403),
},
deprecated: true,
}),
],
});
this.route({
method: 'get',
path: '/features/:projectId',
@ -133,45 +108,13 @@ export default class ArchiveController extends Controller {
});
}
async getArchivedFeatures(
req: IAuthRequest,
res: Response<ArchivedFeaturesSchema>,
): Promise<void> {
const { user } = req;
const features = await this.featureService.getAllArchivedFeatures(
true,
extractUserIdFromUser(user),
);
this.openApiService.respondWithValidation(
200,
res,
archivedFeaturesSchema.$id,
{
version: 2,
features: serializeDates(
features.map((feature) => {
return {
...feature,
stale: feature.stale || false,
archivedAt: feature.archivedAt!,
};
}),
),
},
);
}
async getArchivedFeaturesByProjectId(
req: Request<{ projectId: string }, any, any, any>,
res: Response<ArchivedFeaturesSchema>,
): Promise<void> {
const { projectId } = req.params;
const features =
await this.featureService.getArchivedFeaturesByProjectId(
true,
projectId,
);
await this.featureService.getArchivedFeaturesByProjectId(projectId);
this.openApiService.respondWithValidation(
200,
res,

View File

@ -2162,25 +2162,7 @@ class FeatureToggleService {
);
}
async getAllArchivedFeatures(
archived: boolean,
userId: number,
): Promise<FeatureToggle[]> {
const features = await this.featureToggleStore.getArchivedFeatures();
const projectAccess =
await this.privateProjectChecker.getUserAccessibleProjects(userId);
if (projectAccess.mode === 'all') {
return features;
} else {
return features.filter((f) =>
projectAccess.projects.includes(f.project),
);
}
}
async getArchivedFeaturesByProjectId(
archived: boolean,
project: string,
): Promise<FeatureToggle[]> {
return this.featureToggleStore.getArchivedFeatures(project);

View File

@ -258,7 +258,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
return rows.map(this.rowToFeature);
}
async getArchivedFeatures(project?: string): Promise<FeatureToggle[]> {
async getArchivedFeatures(project: string): Promise<FeatureToggle[]> {
const builder = new FeatureToggleListBuilder(this.db, [
...commonSelectColumns,
'features.archived_at as archived_at',
@ -274,18 +274,10 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
'last_seen_at_metrics.environment as last_seen_at_env',
);
let rows: any[];
if (project) {
rows = await builder.internalQuery
.select(builder.getSelectColumns())
.where({ project })
.whereNotNull('archived_at');
} else {
rows = await builder.internalQuery
.select(builder.getSelectColumns())
.whereNotNull('archived_at');
}
const rows = await builder.internalQuery
.select(builder.getSelectColumns())
.where({ project })
.whereNotNull('archived_at');
return this.featureToggleRowConverter.buildArchivedFeatureToggleListFromRows(
rows,

View File

@ -32,16 +32,6 @@ afterAll(async () => {
await db.destroy();
});
test('Should get empty features via admin', async () => {
await app.request
.get('/api/admin/archive/features')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.features).toHaveLength(0);
});
});
test('Should be allowed to reuse deleted toggle name', async () => {
await app.request
.post('/api/admin/projects/default/features')
@ -61,30 +51,6 @@ test('Should be allowed to reuse deleted toggle name', async () => {
.expect(200);
});
test('Should get archived toggles via admin', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'archived.test.1',
archived: true,
})
.expect(201);
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'archived.test.2',
archived: true,
})
.expect(201);
await app.request
.get('/api/admin/archive/features')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.features).toHaveLength(2);
});
});
test('Should get archived toggles via project', async () => {
await db.stores.featureToggleStore.deleteAll();
@ -132,14 +98,6 @@ test('Should get archived toggles via project', async () => {
.expect((res) => {
expect(res.body.features).toHaveLength(2);
});
await app.request
.get('/api/admin/archive/features')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.features).toHaveLength(3);
});
});
test('Should be able to revive toggle', async () => {

View File

@ -80,26 +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', async () => {
const featureName = 'multiple-environment-last-seen-at-archived';
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`);
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 for multiple environments in /api/admin/archive/features/:projectId', async () => {
const featureName = 'multiple-environment-last-seen-at-archived-project';
await setupLastSeenAtTest(featureName);

View File

@ -1036,7 +1036,8 @@ test('Patching feature flags to active (turning stale to false) should trigger F
});
test('Should archive feature flag', async () => {
const url = '/api/admin/projects/default/features';
const projectId = 'default';
const url = `/api/admin/projects/${projectId}/features`;
const name = 'new.flag.archive';
await app.request
.post(url)
@ -1046,7 +1047,7 @@ test('Should archive feature flag', async () => {
await app.request.get(`${url}/${name}`).expect(404);
const { body } = await app.request
.get(`/api/admin/archive/features`)
.get(`/api/admin/archive/features/${projectId}`)
.expect(200);
const flag = body.features.find((f) => f.name === name);

View File

@ -54,7 +54,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
archived?: boolean,
): Promise<FeatureToggle[]>;
getArchivedFeatures(project?: string): Promise<FeatureToggle[]>;
getArchivedFeatures(project: string): Promise<FeatureToggle[]>;
getPlaygroundFeatures(
featureQuery?: IFeatureToggleQuery,

View File

@ -68,7 +68,7 @@ afterAll(async () => {
test('returns three archived flags', async () => {
expect.assertions(1);
return app.request
.get('/api/admin/archive/features')
.get(`/api/admin/archive/features/${DEFAULT_PROJECT}`)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
@ -79,7 +79,7 @@ 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')
.get(`/api/admin/archive/features/${DEFAULT_PROJECT}`)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {

View File

@ -4,31 +4,6 @@ title: /api/admin/archive
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### Fetch archived flags {#fetch-archived-toggles}
`GET http://unleash.host.com/api/admin/archive/features`
Used to fetch list of archived feature flags
**Example response:**
```json
{
"version": 1,
"features": [
{
"name": "Feature.A",
"description": "lorem ipsum",
"type": "release",
"stale": false,
"variants": [],
"tags": [],
"strategy": "default",
"parameters": {}
}
]
}
```
### Revive feature flag {#revive-feature-toggle}