Repository: Unleash/unleash
Pull request #10001: chore: revive archive page
Author: login: nunogois
Created: 2025-05-15T10:34:36Z | Merged: 2025-05-15T10:54:52Z

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.


Diff:
diff --git a/frontend/src/component/archive/ArchiveTable/ArchiveTable.test.tsx b/frontend/src/component/archive/ArchiveTable/ArchiveTable.test.tsx
new file mode 100644
index 0000000000..0faf8935fb
--- /dev/null
+++ b/frontend/src/component/archive/ArchiveTable/ArchiveTable.test.tsx
@@ -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',
+    );
+});
diff --git a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx
new file mode 100644
index 0000000000..d55c3d24b7
--- /dev/null
+++ b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx
@@ -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 &ldquo;
+                                    {searchValue}&rdquo;
+                                </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>
+        </>
+    );
+};
diff --git a/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx b/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx
new file mode 100644
index 0000000000..16f97c3e27
--- /dev/null
+++ b/frontend/src/component/archive/ArchiveTable/FeatureArchivedCell/FeatureArchivedCell.tsx
@@ -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>
+    );
+};
diff --git a/frontend/src/component/archive/FeaturesArchiveTable.tsx b/frontend/src/component/archive/FeaturesArchiveTable.tsx
new file mode 100644
index 0000000000..a4a4cfcbb6
--- /dev/null
+++ b/frontend/src/component/archive/FeaturesArchiveTable.tsx
@@ -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}
+        />
+    );
+};
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
index c538c2267b..61093db32c 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
@@ -432,6 +432,13 @@ 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),
diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts
index 42fa885e82..08ee8ab298 100644
--- a/frontend/src/component/menu/routes.ts
+++ b/frontend/src/component/menu/routes.ts
@@ -30,6 +30,7 @@ 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.jsx';
+import { FeaturesArchiveTable } from 'component/archive/FeaturesArchiveTable';
 import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
 import { Profile } from 'component/user/Profile/Profile';
 import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
@@ -446,6 +447,15 @@ export const routes: IRoute[] = [
         menu: { adminSettings: true },
     },
 
+    // Archive
+    {
+        path: '/archive',
+        title: 'Archived flags',
+        component: FeaturesArchiveTable,
+        type: 'protected',
+        menu: {},
+    },
+
     // Admin
     {
         path: '/admin/*',


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR revives the flag archive page by reintroducing the archive table using the feature flag search endpoint. Key changes include adding a new route for the archive page, implementing the FeaturesArchiveTable component and its dependent components, and adding tests for archive table functionality.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                              | Description                                                       |
| ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| frontend/src/component/menu/routes.ts                             | Added a new route for the archive page.                           |
| frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx | Updated snapshot reflecting new archive route.                    |
| frontend/src/component/archive/FeaturesArchiveTable.tsx            | Created a new component to display archived feature flags.        |
| frontend/src/component/archive/FeatureArchivedCell/FeatureArchivedCell.tsx | Added a cell component for displaying archive information.        |
| frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx         | Implemented the archive table with search, sorting and action logic.|
| frontend/src/component/archive/ArchiveTable/ArchiveTable.test.tsx    | Added tests covering the archive table functionality.             |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T10:35:12Z
user: login: kwasniew
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T10:45:55Z
user: login: nunogois
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T10:47:39Z
user: login: nunogois
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T10:47:44Z

Review comments:
diff_hunk: @@ -0,0 +1,303 @@
+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, selectedRowIds },
+        prepareRow,
+        setHiddenColumns,
+        toggleAllRowsSelected,
+    } = useTable(
+        {
+            columns: columns as any[], // TODO: fix after `react-table` v8 update
path: frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx
user: login: Copilot
body: Consider replacing 'any[]' with properly typed column definitions to improve type safety, especially given the upgrade path for react-table.
```suggestion
            columns: columns as Column<IFeatureToggle>[], // Updated to use proper type
```
created_at: 2025-05-15T10:35:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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({});
path: frontend/src/component/archive/ArchiveTable/ArchiveTable.test.tsx
user: login: Copilot
body: Initialize storedParams with a value matching the SortingRule<string> type (e.g., { id: 'createdAt', desc: false }) to ensure consistency with component prop types.
created_at: 2025-05-15T10:35:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,303 @@
+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, selectedRowIds },
+        prepareRow,
+        setHiddenColumns,
+        toggleAllRowsSelected,
+    } = useTable(
+        {
+            columns: columns as any[], // TODO: fix after `react-table` v8 update
path: frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx
user: login: nunogois
body: Not super relevant for this PR, was like this before.
created_at: 2025-05-15T10:47:39Z
author_association: MEMBER
diff_hunk: @@ -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({});
path: frontend/src/component/archive/ArchiveTable/ArchiveTable.test.tsx
user: login: nunogois
body: Not super relevant for this PR, was like this before.
created_at: 2025-05-15T10:47:44Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-05-15T10:34:41Z
body: [vc]: #i+KpIInoy9/3k3lUG6hTh+1q/9Co0fojQVAC15U1J2E=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ1bmxlYXNoLWRvY3MiLCJyb290RGlyZWN0b3J5Ijoid2Vic2l0ZSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3VubGVhc2gtdGVhbS91bmxlYXNoLWRvY3MvQndpYjdNdjVKU3lXTmlwa1ZNa0NqMjR5bWhOVyIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6InVubGVhc2gtZG9jcy1naXQtY2hvcmUtcmV2aXZlLWFyY2hpdmUtcGFnZS11bmxlYXNoLXRlYW0udmVyY2VsLmFwcCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **unleash-docs** | ⬜️ Ignored ([Inspect](https://vercel.com/unleash-team/unleash-docs/Bwib7Mv5JSyWNipkVMkCj24ymhNW)) | [Visit Preview](https://unleash-docs-git-chore-revive-archive-page-unleash-team.vercel.app) |  | May 15, 2025 10:46am |
</details>


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T10:34:52Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>Scanned Files</h2>
None

<!-- dependency-review-pr-comment-marker -->
author_association: CONTRIBUTOR