fix: removed dashboard from main page in "All" tab, refreshes dashboard list (#35945)

This commit is contained in:
SBIN2010
2025-12-17 02:45:58 +03:00
committed by Joe Li
parent cffaffd769
commit 8a92092d7e
3 changed files with 389 additions and 224 deletions

View File

@@ -28,18 +28,35 @@ import { Router } from 'react-router-dom';
import { configureStore } from '@reduxjs/toolkit';
import fetchMock from 'fetch-mock';
import * as hooks from 'src/views/CRUD/hooks';
import handleResourceExport from 'src/utils/export';
import { handleDashboardDelete } from 'src/views/CRUD/utils';
import DashboardTable from './DashboardTable';
jest.mock('src/views/CRUD/utils', () => ({
...jest.requireActual('src/views/CRUD/utils'),
handleDashboardDelete: jest
.fn()
.mockImplementation((dashboard, refreshData) => {
refreshData();
return Promise.resolve();
}),
const mockHandleDashboardDelete = handleDashboardDelete as jest.Mock;
// Mock the export module
jest.mock('src/utils/export', () => ({
__esModule: true,
default: jest.fn(),
}));
const mockExport = handleResourceExport as jest.MockedFunction<
typeof handleResourceExport
>;
jest.mock('src/views/CRUD/utils', () => {
const actual = jest.requireActual('src/views/CRUD/utils');
return {
...actual,
handleDashboardDelete: jest
.fn()
.mockImplementation((dashboard, refreshData) => {
refreshData();
return Promise.resolve();
}),
};
});
// Mock the CRUD hooks
jest.mock('src/views/CRUD/hooks', () => ({
...jest.requireActual('src/views/CRUD/hooks'),
@@ -94,256 +111,400 @@ const defaultProps = {
otherTabTitle: 'Examples',
};
describe('DashboardTable', () => {
const history = createMemoryHistory();
const store = configureStore({
reducer: {
dashboards: (state = { dashboards: [] }) => state,
const history = createMemoryHistory();
const store = configureStore({
reducer: {
dashboards: (state = { dashboards: [] }) => state,
},
preloadedState: {
dashboards: {
dashboards: mockDashboards,
},
preloadedState: {
dashboards: {
dashboards: mockDashboards,
},
},
});
},
});
beforeEach(() => {
jest.spyOn(SupersetClient, 'get').mockImplementation(() =>
Promise.resolve({
json: {
result: mockDashboards[0],
},
response: new Response(),
}),
);
fetchMock.get(
'glob:*/api/v1/dashboard/*',
{
beforeEach(() => {
jest.spyOn(SupersetClient, 'get').mockImplementation(() =>
Promise.resolve({
json: {
result: mockDashboards[0],
},
{ overwriteRoutes: true },
); // Add overwriteRoutes option
response: new Response(),
}),
);
// Mock loading state for first render
jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({
state: {
loading: true,
resourceCollection: [],
resourceCount: 0,
bulkSelectEnabled: false,
lastFetched: undefined,
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
fetchMock.get(
'glob:*/api/v1/dashboard/*',
{
result: mockDashboards[0],
},
{ overwriteRoutes: true },
); // Add overwriteRoutes option
// Mock loading state for first render
jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({
state: {
loading: true,
resourceCollection: [],
resourceCount: 0,
bulkSelectEnabled: false,
lastFetched: undefined,
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
});
test('renders loading state initially', () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
});
test('renders empty state when no dashboards', async () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
await waitFor(() => {
expect(screen.getByText('No results')).toBeInTheDocument();
});
});
it('renders loading state initially', () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
});
test('renders dashboard cards when data is loaded', async () => {
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
it('renders empty state when no dashboards', async () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
render(
<Router history={history}>
<DashboardTable {...defaultProps} mine={mockDashboards} />
</Router>,
{ store },
);
await waitFor(() => {
expect(screen.getByText('No results')).toBeInTheDocument();
await waitFor(() => {
mockDashboards.forEach(dashboard => {
expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
});
});
});
it('renders dashboard cards when data is loaded', async () => {
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
test('switches to Mine tab correctly', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
render(
<Router history={history}>
<DashboardTable {...defaultProps} mine={mockDashboards} />
</Router>,
{ store },
);
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
await waitFor(() => {
mockDashboards.forEach(dashboard => {
expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
});
});
const mineTab = screen.getByRole('menuitem', { name: /mine/i });
await userEvent.click(mineTab);
await waitFor(() => {
expect(mineTab).toHaveClass('ant-menu-item-selected');
});
});
test('handles create dashboard button click', async () => {
const assignMock = jest.fn();
Object.defineProperty(window, 'location', {
value: { assign: assignMock },
writable: true,
});
it('switches to Mine tab correctly', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const createButton = screen.getByRole('button', { name: /dashboard$/i });
await userEvent.click(createButton);
expect(assignMock).toHaveBeenCalledWith('/dashboard/new');
});
const mineTab = screen.getByRole('menuitem', { name: /mine/i });
await userEvent.click(mineTab);
await waitFor(() => {
expect(mineTab).toHaveClass('ant-menu-item-selected');
});
test('switches to Other tab when available', async () => {
const props = {
...defaultProps,
otherTabData: mockDashboards,
otherTabTitle: 'Examples',
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const otherTab = screen.getByRole('tab', { name: 'Examples' });
await userEvent.click(otherTab);
expect(otherTab).toHaveClass('active');
});
test('handles bulk dashboard export with correct ID and shows spinner', async () => {
// Mock export to take some time before calling the done callback
mockExport.mockImplementation(
(resource: string, ids: number[], done: () => void) =>
new Promise<void>(resolve => {
setTimeout(() => {
done();
resolve();
}, 100);
}),
);
const props = {
...defaultProps,
mine: mockDashboards,
};
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const moreOptionsButton = screen.getAllByRole('img', {
name: 'more',
})[0];
await userEvent.click(moreOptionsButton);
// Wait for dropdown menu to appear
await waitFor(() => {
expect(screen.getByText('Export')).toBeInTheDocument();
});
it('handles create dashboard button click', async () => {
const assignMock = jest.fn();
Object.defineProperty(window, 'location', {
value: { assign: assignMock },
writable: true,
});
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
const createButton = screen.getByRole('button', { name: /dashboard$/i });
await userEvent.click(createButton);
expect(assignMock).toHaveBeenCalledWith('/dashboard/new');
});
it('switches to Other tab when available', async () => {
const props = {
...defaultProps,
otherTabData: mockDashboards,
otherTabTitle: 'Examples',
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const otherTab = screen.getByRole('tab', { name: 'Examples' });
await userEvent.click(otherTab);
expect(otherTab).toHaveClass('active');
});
it('handles bulk dashboard export', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const moreOptionsButton = screen.getAllByRole('img', {
name: 'more',
})[0];
await userEvent.click(moreOptionsButton);
// Wait for dropdown menu to appear
await waitFor(() => {
expect(screen.getByText('Export')).toBeInTheDocument();
});
const exportOption = screen.getByText('Export');
await userEvent.click(exportOption);
const exportOption = screen.getByText('Export');
await userEvent.click(exportOption);
// Verify spinner shows up during export
await waitFor(() => {
expect(screen.getByRole('status')).toBeInTheDocument();
});
it('handles dashboard deletion confirmation', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
// Verify the export was called with correct parameters
expect(mockExport).toHaveBeenCalledWith(
'dashboard',
[1],
expect.any(Function),
);
const refreshDataMock = jest.fn();
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: refreshDataMock,
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
// Wait for export to complete and spinner to disappear
await waitFor(
() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
test('handles dashboard deletion confirmation', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
const moreOptionsButton = screen.getAllByLabelText('more')[0];
await userEvent.click(moreOptionsButton);
const refreshDataMock = jest.fn();
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: refreshDataMock,
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}));
await waitFor(() => {
expect(screen.getByText('Delete')).toBeInTheDocument();
});
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const deleteOption = screen.getByText('Delete');
await userEvent.click(deleteOption);
const moreOptionsButton = screen.getAllByLabelText('more')[0];
await userEvent.click(moreOptionsButton);
// Verify Delete button is initially disabled
const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
expect(confirmDeleteButton).toBeDisabled();
await waitFor(() => {
expect(screen.getByText('Delete')).toBeInTheDocument();
});
// Type DELETE in the confirmation input
const deleteInput = screen.getByTestId('delete-modal-input');
await userEvent.type(deleteInput, 'DELETE');
const deleteOption = screen.getByText('Delete');
await userEvent.click(deleteOption);
// Verify Delete button becomes enabled
await waitFor(() => {
expect(confirmDeleteButton).toBeEnabled();
});
// Verify Delete button is initially disabled
const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
expect(confirmDeleteButton).toBeDisabled();
// Click the now-enabled Delete button
await userEvent.click(confirmDeleteButton);
// Type DELETE in the confirmation input
const deleteInput = screen.getByTestId('delete-modal-input');
await userEvent.type(deleteInput, 'DELETE');
await waitFor(
() => {
expect(refreshDataMock).toHaveBeenCalled();
},
{ timeout: 3000 },
);
// Verify Delete button becomes enabled
await waitFor(() => {
expect(confirmDeleteButton).toBeEnabled();
});
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Click the now-enabled Delete button
await userEvent.click(confirmDeleteButton);
await waitFor(
() => {
expect(refreshDataMock).toHaveBeenCalled();
},
{ timeout: 3000 },
);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
test('passes correct parameters to handleDashboardDelete for Other tab', async () => {
mockHandleDashboardDelete.mockClear();
const refreshDataMock = jest.fn();
const fetchDataMock = jest.fn().mockName('getData');
jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({
state: {
loading: false,
resourceCollection: mockDashboards,
resourceCount: mockDashboards.length,
bulkSelectEnabled: false,
lastFetched: new Date().toISOString(),
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: refreshDataMock,
fetchData: fetchDataMock,
toggleBulkSelect: jest.fn(),
}));
const props = {
...defaultProps,
otherTabData: mockDashboards,
otherTabTitle: 'All',
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
await waitFor(() => {
expect(screen.getByText('Test Dashboard 1')).toBeInTheDocument();
});
const otherTab = screen.getByRole('tab', { name: 'All' });
await userEvent.click(otherTab);
await waitFor(() => {
expect(screen.getByText('Test Dashboard 1')).toBeInTheDocument();
});
const moreOptionsButtons = screen.getAllByLabelText(/more|options/i);
expect(moreOptionsButtons.length).toBeGreaterThan(0);
await userEvent.click(moreOptionsButtons[0]);
await waitFor(() => {
expect(screen.getByText('Delete')).toBeInTheDocument();
});
const deleteOption = screen.getByText('Delete');
await userEvent.click(deleteOption);
await waitFor(() => {
expect(screen.getByText('Please confirm')).toBeInTheDocument();
expect(screen.getByTestId('delete-modal-input')).toBeInTheDocument();
});
const deleteInput = screen.getByTestId('delete-modal-input');
await userEvent.type(deleteInput, 'DELETE');
const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
await waitFor(() => {
expect(confirmDeleteButton).toBeEnabled();
});
await userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(mockHandleDashboardDelete).toHaveBeenCalled();
});
expect(mockHandleDashboardDelete).toHaveBeenCalledWith(
expect.objectContaining({
id: 1,
dashboard_title: 'Test Dashboard 1',
}),
expect.any(Function),
expect.any(Function),
expect.any(Function),
'Other',
mockUser.userId,
expect.any(Function),
);
const lastCall = mockHandleDashboardDelete.mock.calls[0];
const getDataParam = lastCall[6];
getDataParam('Other');
expect(fetchDataMock).toHaveBeenCalledWith({
filters: [],
pageIndex: 0,
pageSize: 5,
sortBy: [{ desc: true, id: 'changed_on_delta_humanized' }],
});
});

View File

@@ -237,6 +237,7 @@ function DashboardTable({
addDangerToast,
activeTab,
user?.userId,
getData,
);
setDashboardToDelete(null);
}}

View File

@@ -310,6 +310,7 @@ export function handleDashboardDelete(
addDangerToast: (arg0: string) => void,
dashboardFilter?: string,
userId?: string | number,
getData?: (tab: TableTab) => void,
) {
return SupersetClient.delete({
endpoint: `/api/v1/dashboard/${id}`,
@@ -333,6 +334,8 @@ export function handleDashboardDelete(
],
};
if (dashboardFilter === 'Mine') refreshData(filters);
else if (dashboardFilter === 'Other' && getData)
getData(dashboardFilter as TableTab);
else refreshData();
addSuccessToast(t('Deleted: %s', dashboardTitle));
},