diff --git a/docs/docs/using-superset/recently-archived.mdx b/docs/docs/using-superset/recently-archived.mdx index 777e4efa56f..ebca8c6c416 100644 --- a/docs/docs/using-superset/recently-archived.mdx +++ b/docs/docs/using-superset/recently-archived.mdx @@ -26,7 +26,8 @@ page and its menu entry are hidden, and deletes are permanent as before. ## Finding archived objects Open **Recently Archived** and pick a type — **Chart**, **Dashboard**, or -**Dataset** — from the Type selector. The view shows one type at a time; each +**Dataset** (shown as **Datasource** when semantic layers are enabled) — from +the Type selector. The view shows one type at a time; each type is read from its own list endpoint, so the same row-level access rules that govern the normal lists apply here. diff --git a/superset-frontend/src/pages/ArchivedList/ArchivedList.test.tsx b/superset-frontend/src/pages/ArchivedList/ArchivedList.test.tsx index 360f682898b..849e3164b9b 100644 --- a/superset-frontend/src/pages/ArchivedList/ArchivedList.test.tsx +++ b/superset-frontend/src/pages/ArchivedList/ArchivedList.test.tsx @@ -25,8 +25,10 @@ import { fireEvent, userEvent, waitFor, + within, selectOption, } from 'spec/helpers/testing-library'; +import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core'; import { MemoryRouter } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; @@ -88,6 +90,15 @@ const mockCharts = [ // list so `_info` requests resolve to it rather than the broader list glob. // withToasts injects the toast callbacks as props; the harness renders no // toast container, so the spy is the only way to pin what the user is told. +// The type label for the dataset concept is flag-aware (SEMANTIC_LAYERS → +// "Datasource"); mock the flag reader so tests can exercise both states. The +// default (false for every flag) matches the real test environment, where no +// bootstrap flags are set. +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + isFeatureEnabled: jest.fn(() => false), +})); + const mockAddDangerToast = jest.fn(); jest.mock('src/components/MessageToasts/withToasts', () => ({ __esModule: true, @@ -144,6 +155,13 @@ beforeEach(() => { mockAddDangerToast.mockClear(); }); +afterEach(() => { + // The flag mock is shared module state; restore the environment default so a + // flag-flipping test that dies mid-body (e.g. by Jest timeout) cannot leak + // SEMANTIC_LAYERS into whichever test runs next. + (isFeatureEnabled as jest.Mock).mockImplementation(() => false); +}); + test('renders archived rows with Name and Type columns', async () => { mockRoutes(); renderArchivedList(); @@ -573,3 +591,57 @@ test('a viewer who can read none of the types gets an empty state, not three 403 // No list fetch was ever issued. expect(fetchMock.callHistory.calls(/chart\/\?q/)).toHaveLength(0); }); + +test('labels the dataset type "Datasource" when semantic layers is enabled', async () => { + (isFeatureEnabled as jest.Mock).mockImplementation( + (flag: FeatureFlag) => flag === FeatureFlag.SemanticLayers, + ); + mockRoutes(); + renderArchivedList(); + await screen.findByText('Deleted Chart One'); + + userEvent.click(screen.getByRole('combobox', { name: 'Type' })); + expect( + await screen.findByRole('option', { name: 'Datasource' }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('option', { name: 'Dataset' }), + ).not.toBeInTheDocument(); + + // Selecting the renamed option still drives the dataset resource — + // the underlying type value is flag-independent. + await selectOption('Datasource', 'Type'); + await screen.findByText('deleted_table_one'); + expect( + fetchMock.callHistory.calls(datasetListEndpoint).length, + ).toBeGreaterThan(0); + // Pin the Type COLUMN cell, not just the Select's own rendered value. + const datasetRow = screen.getByText('deleted_table_one').closest('tr'); + expect( + within(datasetRow as HTMLElement).getByText('Datasource'), + ).toBeInTheDocument(); +}); + +test('labels the dataset type "Dataset" when semantic layers is disabled', async () => { + mockRoutes(); + renderArchivedList(); + await screen.findByText('Deleted Chart One'); + + userEvent.click(screen.getByRole('combobox', { name: 'Type' })); + expect( + await screen.findByRole('option', { name: 'Dataset' }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('option', { name: 'Datasource' }), + ).not.toBeInTheDocument(); + + await selectOption('Dataset', 'Type'); + await screen.findByText('deleted_table_one'); + expect( + fetchMock.callHistory.calls(datasetListEndpoint).length, + ).toBeGreaterThan(0); + const datasetRow = screen.getByText('deleted_table_one').closest('tr'); + expect( + within(datasetRow as HTMLElement).getByText('Dataset'), + ).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/pages/ArchivedList/index.tsx b/superset-frontend/src/pages/ArchivedList/index.tsx index fffa83b83ec..c6d5f811c5d 100644 --- a/superset-frontend/src/pages/ArchivedList/index.tsx +++ b/superset-frontend/src/pages/ArchivedList/index.tsx @@ -37,6 +37,7 @@ import { type ListViewFilters, } from 'src/components'; import SubMenu from 'src/features/home/SubMenu'; +import { datasetLabel } from 'src/features/semanticLayers/label'; import withToasts from 'src/components/MessageToasts/withToasts'; import { recoveredToast } from 'src/utils/softDeleteCopy'; import { findPermission } from 'src/utils/findPermission'; @@ -82,10 +83,12 @@ const EmptyStateRow = styled.div` `} `; -const TYPE_LABELS: Record = { - chart: t('Chart'), - dashboard: t('Dashboard'), - dataset: t('Dataset'), +// Getters, not strings: the dataset label follows the SEMANTIC_LAYERS flag +// ("Dataset" / "Datasource"), read at render time via the shared naming module. +const TYPE_LABELS: Record string> = { + chart: () => t('Chart'), + dashboard: () => t('Dashboard'), + dataset: datasetLabel, }; interface ToastProps { @@ -166,7 +169,7 @@ function ArchivedListBody({ refreshData, } = useListViewResource( config.resource, - TYPE_LABELS[type], + TYPE_LABELS[type](), addDangerToast, true, [], @@ -247,7 +250,7 @@ function ArchivedListBody({ name => { const { text, options } = recoveredToast( name, - TYPE_LABELS[type], + TYPE_LABELS[type](), item.url ?? item.explore_url, ); addSuccessToast(text, options); @@ -306,7 +309,7 @@ function ArchivedListBody({ id: config.nameField, }, { - Cell: () => TYPE_LABELS[type], + Cell: () => TYPE_LABELS[type](), Header: t('Type'), id: 'type', disableSortBy: true, @@ -539,7 +542,7 @@ function ArchivedList({ addDangerToast, addSuccessToast }: ToastProps) { onChange={handleTypeChange} options={availableTypes.map(option => ({ value: option, - label: TYPE_LABELS[option], + label: TYPE_LABELS[option](), }))} />