Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions superset-frontend/src/explore/components/SaveModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,172 @@ test('onTabChange correctly updates selectedTab via forceUpdate', () => {
});
});

const ownerUser = {
userId: 1,
username: 'testuser',
firstName: 'Test',
lastName: 'User',
isActive: true,
isAnonymous: false,
permissions: {},
roles: { Alpha: [['can_write', 'Dashboard']] as [string, string][] },
groups: [],
};

const makeMetadataDashboard = (id: number, title: string) => ({
id,
dashboard_title: title,
owners: [{ id: 1, first_name: 'Test', last_name: 'User' }],
extra_owners: [],
roles: [],
url: `/superset/dashboard/${id}/`,
slug: null,
thumbnail_url: null,
published: true,
changed_by_name: 'Test User',
changed_by: { id: 1, first_name: 'Test', last_name: 'User' },
changed_on: '2024-01-01',
charts: [],
});

test('pre-populates dashboard from metadata.dashboards when dashboardId prop is absent', async () => {
const dashboardId = 5;
const dashboardTitle = 'Chart Dashboard';

const myProps = {
...defaultProps,
dashboardId: null,
metadata: {
dashboards: [{ id: dashboardId, dashboard_title: dashboardTitle }],
owners: ['Test User'],
created_on_humanized: '2 days ago',
changed_on_humanized: '1 day ago',
},
user: ownerUser,
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
dispatch: jest.fn(),
addDangerToast: jest.fn(),
};

const component = new TestSaveModal(myProps);
const mockFull = makeMetadataDashboard(dashboardId, dashboardTitle);

component.loadDashboard = jest.fn().mockResolvedValue(mockFull);
component.loadTabs = jest.fn().mockResolvedValue([]);

const stateUpdates: any[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replace any[] with a specific state update collection type (for example, a typed partial state shape) so the test assertions stay type-safe. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The file is TypeScript (.tsx), and this line uses any[], which directly violates the rule against using any in modified TypeScript/TSX code. A concrete type or a narrower reusable type should be used instead.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/explore/components/SaveModal.test.tsx
**Line:** 671:671
**Comment:**
	*Custom Rule: Replace `any[]` with a specific state update collection type (for example, a typed partial state shape) so the test assertions stay type-safe.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining — line 602 in this same file already uses the identical setState = function (this: any, stateUpdate: any) pattern. Adding specific types only to the new test helper would be inconsistent.

component.setState = jest.fn((update: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Change the update callback parameter from any to a precise union/type that matches the setState updater contract used in this test. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is modified TypeScript code using any in a callback parameter, which matches the custom rule prohibiting any in new or changed TS/TSX code. The suggestion is therefore addressing a real rule violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/explore/components/SaveModal.test.tsx
**Line:** 672:672
**Comment:**
	*Custom Rule: Change the `update` callback parameter from `any` to a precise union/type that matches the `setState` updater contract used in this test.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining — same reason as the adjacent comment: line 602 uses the identical any pattern for setState mocking. Consistent with existing style.

stateUpdates.push(update);
});

try {
sessionStorage.clear();
} catch (_) {
// ignore
}

await component.componentDidMount();

expect(component.loadDashboard).toHaveBeenCalledWith(dashboardId);
expect(stateUpdates).toContainEqual({
dashboard: { label: dashboardTitle, value: dashboardId },
});
expect(component.loadTabs).toHaveBeenCalledWith(dashboardId);
});

test('skips non-editable dashboards and picks the first editable one from metadata', async () => {
const editableId = 7;
const editableTitle = 'Editable Dashboard';

const myProps = {
...defaultProps,
dashboardId: null,
metadata: {
dashboards: [
{ id: 6, dashboard_title: 'Not Mine' },
{ id: editableId, dashboard_title: editableTitle },
],
owners: ['Test User'],
created_on_humanized: '2 days ago',
changed_on_humanized: '1 day ago',
},
user: ownerUser,
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
dispatch: jest.fn(),
addDangerToast: jest.fn(),
};

const component = new TestSaveModal(myProps);

const notMine = makeMetadataDashboard(6, 'Not Mine');
notMine.owners = [{ id: 99, first_name: 'Other', last_name: 'Owner' }];
const editable = makeMetadataDashboard(editableId, editableTitle);

component.loadDashboard = jest
.fn()
.mockImplementation((id: number) =>
Promise.resolve(id === 6 ? notMine : editable),
);
component.loadTabs = jest.fn().mockResolvedValue([]);

const stateUpdates: any[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replace this any[] declaration with a reusable typed alias for state updates to keep test typing strict and consistent. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is another occurrence of any[] in the added test code, so it clearly matches the custom rule to flag TypeScript usage of any.
The suggestion is directly applicable and accurate.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/explore/components/SaveModal.test.tsx
**Line:** 726:726
**Comment:**
	*Custom Rule: Replace this `any[]` declaration with a reusable typed alias for state updates to keep test typing strict and consistent.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

component.setState = jest.fn((update: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Type the mocked setState callback argument with a specific updater type (or Partial state type) instead of any. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The modified test code uses any in the mocked setState callback parameter, which is explicitly disallowed by the rule.
This is a genuine violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/explore/components/SaveModal.test.tsx
**Line:** 727:727
**Comment:**
	*Custom Rule: Type the mocked `setState` callback argument with a specific updater type (or `Partial` state type) instead of `any`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

stateUpdates.push(update);
});

try {
sessionStorage.clear();
} catch (_) {
// ignore
}

await component.componentDidMount();

expect(stateUpdates).toContainEqual({
dashboard: { label: editableTitle, value: editableId },
});
expect(component.loadTabs).toHaveBeenCalledWith(editableId);
});

test('does not use metadata fallback when dashboardId prop is set', async () => {
const propDashboardId = 3;
const propDashboardTitle = 'Prop Dashboard';

const myProps = {
...defaultProps,
dashboardId: propDashboardId,
metadata: {
dashboards: [{ id: 99, dashboard_title: 'Should Not Be Used' }],
owners: ['Test User'],
created_on_humanized: '2 days ago',
changed_on_humanized: '1 day ago',
},
user: ownerUser,
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
dispatch: jest.fn(),
addDangerToast: jest.fn(),
};

const component = new TestSaveModal(myProps);
const mockFull = makeMetadataDashboard(propDashboardId, propDashboardTitle);

component.loadDashboard = jest.fn().mockResolvedValue(mockFull);
component.loadTabs = jest.fn().mockResolvedValue([]);

const stateUpdates: any[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replace this any[] usage with a concrete typed structure that reflects the expected dashboard selection state updates. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The final PR state includes this any[] declaration in newly added TypeScript test code, so it violates the no-any custom rule.
The suggestion correctly identifies the issue.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/explore/components/SaveModal.test.tsx
**Line:** 770:770
**Comment:**
	*Custom Rule: Replace this `any[]` usage with a concrete typed structure that reflects the expected dashboard selection state updates.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

component.setState = jest.fn((update: any) => {
stateUpdates.push(update);
});

await component.componentDidMount();

expect(component.loadDashboard).toHaveBeenCalledWith(propDashboardId);
expect(component.loadDashboard).not.toHaveBeenCalledWith(99);
expect(stateUpdates).toContainEqual({
dashboard: { label: propDashboardTitle, value: propDashboardId },
});
});

test('chart placement logic finds row with available space', () => {
// Test case 1: Row has space (8 + 4 = 12 <= 12)
const positionJson1 = {
Expand Down
38 changes: 37 additions & 1 deletion superset-frontend/src/explore/components/SaveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ import {
isUserAdmin,
} from 'src/dashboard/util/permissionUtils';
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
import { SaveActionType, ChartStatusType } from 'src/explore/types';
import {
SaveActionType,
ChartStatusType,
ExplorePageInitialData,
} from 'src/explore/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import {
removeChartState,
Expand All @@ -81,6 +85,7 @@ interface SaveModalProps extends RouteComponentProps {
isVisible: boolean;
dispatch: Dispatch;
theme: SupersetTheme;
metadata?: ExplorePageInitialData['metadata'];
}

type SaveModalState = {
Expand Down Expand Up @@ -162,6 +167,35 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
t('An error occurred while loading dashboard information.'),
);
}
} else {
const metadataDashboards = this.props.metadata?.dashboards;
if (metadataDashboards?.length) {
// Fallback: the chart is already on one or more dashboards (from Explore API
// metadata). Pre-populate with the first dashboard the user can edit so the
// "Save & go to dashboard" button works out of the box.
try {
let editable: Dashboard | undefined;
for (const { id } of metadataDashboards) {
// eslint-disable-next-line no-await-in-loop
const result = await this.loadDashboard(id).catch(() => null);
if (result && canUserEditDashboard(result, this.props.user)) {
editable = result as Dashboard;
break;
}
}
Comment on lines +178 to +185

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The metadata fallback fetches dashboards sequentially using await inside a loop, which creates one network round-trip per dashboard and can make the modal noticeably slow for charts attached to many dashboards. This also contradicts the intended behavior to load metadata dashboards in parallel. Fetch all candidate dashboards concurrently (e.g., Promise.all) and then pick the first editable one by original metadata order. [performance]

Severity Level: Major ⚠️
- ⚠️ Explore Save chart modal slow when chart on many dashboards.
- ⚠️ "Save & go to dashboard" button stays disabled unnecessarily.
Steps of Reproduction ✅
1. Open an existing chart's Explore page at `/explore/?slice_id=<id>` without a
`dashboard_id` URL param so `dashboardId` is undefined in `hydrateExplore` and hence in
`ExploreViewContainer` (see `src/explore/actions/hydrateExplore.ts:16-21` and
`src/explore/components/ExploreViewContainer/index.tsx:1180-1186` where `dashboardId` is
derived from `explore.form_data.dashboardId`).

2. Ensure the chart belongs to many dashboards so the bootstrap payload includes a
populated `metadata.dashboards` array (shape defined in `src/explore/types.ts:4-20`,
especially `metadata.dashboards?: { id: number; dashboard_title: string }[]`), which
`hydrateExplore` stores on `explore.metadata` and `ExploreViewContainer` passes through as
`metadata` (mapStateToProps around
`src/explore/components/ExploreViewContainer/index.tsx:1188-1204`).

3. In the Explore toolbar, click the Save button so `ExploreChartHeader` dispatches
`setSaveChartModalVisibility(true)` via `useUnsavedChangesPrompt` (see
`src/explore/components/ExploreChartHeader/index.tsx:22-28` where `onSave` calls
`dispatch(setSaveChartModalVisibility(true))`). This sets `saveModal.isVisible` in Redux,
causing `ExploreViewContainer` to render `<SaveModal>` conditionally when
`props.isSaveModalVisible` is true
(`src/explore/components/ExploreViewContainer/index.tsx:1191-1199`).

4. When `<SaveModal>` mounts, its `componentDidMount` runs
(`src/explore/components/SaveModal.tsx:5-61` in the 140–219 block). Because there is no
`dashboardId` and no `SK_DASHBOARD_ID` in `sessionStorage`, execution follows the `else`
branch that reads `const metadataDashboards = this.props.metadata?.dashboards;` (lines
31-33 of that block). The fallback then iterates `for (const { id } of
metadataDashboards)` and performs `await this.loadDashboard(id).catch(() => null);` inside
the loop (`src/explore/components/SaveModal.tsx` PR hunk lines 178-185 / snippet lines
39-46), issuing one `/api/v1/dashboard/<id>` request at a time. With many dashboards,
modal initialization (pre-populating `this.state.dashboard` and loading tabs via
`loadTabs`) is delayed by roughly N×per-request-latency instead of the max of concurrent
latencies, making the Save modal noticeably slower whenever the chart is attached to many
dashboards.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/explore/components/SaveModal.tsx
**Line:** 178:185
**Comment:**
	*Performance: The metadata fallback fetches dashboards sequentially using `await` inside a loop, which creates one network round-trip per dashboard and can make the modal noticeably slow for charts attached to many dashboards. This also contradicts the intended behavior to load metadata dashboards in parallel. Fetch all candidate dashboards concurrently (e.g., `Promise.all`) and then pick the first editable one by original metadata order.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +177 to +185

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sequential loading performance regression

This refactor changes from parallel (Promise.all) to sequential (for...await) loading. The original loads all dashboards concurrently and finds the first editable one; the new code loads one-by-one and stops at the first editable. This is slower when an editable dashboard exists early (n-1 sequential delays) or when none exist (full sequential chain vs parallel). If intentional for resource conservation, add a comment documenting this trade-off.

Code Review Run #c85537


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

if (editable) {
this.setState({
dashboard: {
label: editable.dashboard_title,
value: editable.id,
},
});
await this.loadTabs(editable.id);
}
} catch (error) {
logging.warn(error);
}
}
}
}

Expand Down Expand Up @@ -826,6 +860,7 @@ interface StateProps {
dashboards: any;
alert: any;
isVisible: boolean;
metadata?: ExplorePageInitialData['metadata'];
}

function mapStateToProps({
Expand All @@ -841,6 +876,7 @@ function mapStateToProps({
dashboards: saveModal.dashboards,
alert: saveModal.saveModalAlert,
isVisible: saveModal.isVisible,
metadata: explore.metadata,
};
}

Expand Down
Loading