Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
19151d1
fix: fixed Add Metrics to Tree Chart (#29158)
SBIN2010 Oct 22, 2024
35aaeb6
fix: clear modal after CSS templates is added
SBIN2010 Oct 22, 2024
db3afda
Revert "fix: clear modal after CSS templates is added"
SBIN2010 Oct 23, 2024
9eea2e6
fix: tree metrics exclude validator
SBIN2010 Nov 25, 2024
3930c95
Merge branch 'apache:master' into master
SBIN2010 Mar 11, 2025
b355212
Merge branch 'apache:master' into master
SBIN2010 Mar 18, 2025
7200d6c
Merge branch 'apache:master' into master
SBIN2010 Mar 20, 2025
3049a3d
Merge branch 'apache:master' into master
SBIN2010 Mar 25, 2025
247aef8
Merge branch 'apache:master' into master
SBIN2010 Apr 2, 2025
b4678ec
Merge branch 'apache:master' into master
SBIN2010 Apr 4, 2025
0ae844d
Merge branch 'apache:master' into master
SBIN2010 Jul 1, 2025
49fc529
Merge branch 'apache:master' into master
SBIN2010 Jul 10, 2025
601dde2
fix: revert #30679
SBIN2010 Mar 25, 2025
66823a5
Merge branch 'apache:master' into master
SBIN2010 Jul 12, 2025
4f59629
Merge branch 'apache:master' into master
SBIN2010 Jul 16, 2025
a7ce4e9
Merge branch 'apache:master' into master
SBIN2010 Jul 20, 2025
1bf4fdd
fix
SBIN2010 Jul 20, 2025
1241867
Merge branch 'apache:master' into master
SBIN2010 Jul 31, 2025
045cf6b
Merge branch 'apache:master' into master
SBIN2010 Aug 2, 2025
acdba8a
Merge branch 'apache:master' into master
SBIN2010 Aug 10, 2025
4fb89c2
Merge branch 'apache:master' into master
SBIN2010 Aug 15, 2025
f47d28b
Merge branch 'apache:master' into master
SBIN2010 Aug 18, 2025
3bdf337
Merge branch 'apache:master' into master
SBIN2010 Aug 19, 2025
780a5d6
Merge branch 'apache:master' into master
SBIN2010 Aug 24, 2025
3c339df
Merge branch 'apache:master' into master
SBIN2010 Aug 25, 2025
798b7a6
Merge branch 'apache:master' into master
SBIN2010 Aug 30, 2025
2db564e
Merge branch 'apache:master' into master
SBIN2010 Sep 3, 2025
a58cfad
Merge branch 'apache:master' into master
SBIN2010 Sep 5, 2025
9ea0d2f
Merge branch 'apache:master' into master
SBIN2010 Sep 8, 2025
c6bbf53
Merge branch 'apache:master' into master
SBIN2010 Sep 9, 2025
5214e7a
Merge branch 'apache:master' into master
SBIN2010 Sep 15, 2025
d98105c
Merge branch 'apache:master' into master
SBIN2010 Sep 17, 2025
30b1576
Merge branch 'apache:master' into master
SBIN2010 Sep 19, 2025
4aaa27c
Merge branch 'apache:master' into master
SBIN2010 Sep 23, 2025
25c1987
Merge branch 'apache:master' into master
SBIN2010 Sep 24, 2025
c3854cb
Merge branch 'apache:master' into master
SBIN2010 Oct 1, 2025
e1c4909
Merge branch 'apache:master' into master
SBIN2010 Oct 1, 2025
c60bda8
Merge branch 'apache:master' into master
SBIN2010 Oct 2, 2025
964307e
Merge branch 'apache:master' into master
SBIN2010 Oct 14, 2025
df97cb7
Merge branch 'apache:master' into master
SBIN2010 Oct 22, 2025
6ef82fa
Merge branch 'apache:master' into master
SBIN2010 Oct 27, 2025
d0ae96c
Merge branch 'apache:master' into master
SBIN2010 Oct 29, 2025
725039c
Merge branch 'apache:master' into master
SBIN2010 Oct 29, 2025
727c59f
Merge branch 'apache:master' into master
SBIN2010 Nov 2, 2025
eeb04da
Merge branch 'apache:master' into master
SBIN2010 Nov 7, 2025
ca12644
Merge branch 'apache:master' into master
SBIN2010 Nov 10, 2025
45c1a1a
Merge branch 'apache:master' into master
SBIN2010 Nov 13, 2025
44708bf
Merge branch 'apache:master' into master
SBIN2010 Nov 14, 2025
e3336eb
feat: step 1 add front table
SBIN2010 Nov 20, 2025
8a7e2c9
feat: add test and changed go to new url
SBIN2010 Nov 28, 2025
b3d39d8
fix: remarks in PR
SBIN2010 Nov 28, 2025
5de507e
fix: return delete blok
SBIN2010 Nov 28, 2025
f56c2c1
fix: selectedTabId add
SBIN2010 Nov 29, 2025
bc0c095
Merge branch 'apache:master' into feature/add_tab_select_with_save_chart
SBIN2010 Nov 29, 2025
3a82246
Merge branch 'apache:master' into feature/add_tab_select_with_save_chart
SBIN2010 Nov 30, 2025
a339bd9
fix: any changed to type
SBIN2010 Nov 30, 2025
9967088
Update superset-frontend/src/explore/components/SaveModal.test.jsx
rusackas Dec 1, 2025
2444e7e
Update superset-frontend/src/explore/components/SaveModal.tsx
rusackas Dec 1, 2025
ccdf86b
Merge branch 'apache:master' into feature/add_tab_select_with_save_chart
SBIN2010 Dec 1, 2025
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
71 changes: 71 additions & 0 deletions superset-frontend/src/explore/components/SaveModal.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ jest.mock('@superset-ui/core/components/Select', () => ({
),
}));

jest.mock('@superset-ui/core/components/TreeSelect', () => ({
TreeSelect: ({ onChange, disabled }) => {
return (
<input
data-test="mock-tree-select"
disabled={disabled}
onChange={({ target: { value } }) => onChange(value)}
/>
);
},
}));

const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const initialState = {
Expand Down Expand Up @@ -345,3 +357,62 @@ test('removes form_data_key from URL parameters after save', () => {
expect(result.get('slice_id')).toEqual('1');
expect(result.get('save_action')).toEqual('overwrite');
});

test('disables tab selector when no dashboard selected', () => {
const { getByRole, getByTestId } = setup();
fireEvent.click(getByRole('radio', { name: 'Save as...' }));
const tabSelector = getByTestId('mock-tree-select');
expect(tabSelector).toBeInTheDocument();
expect(tabSelector).toBeDisabled();
});

test('renders tab selector when saving as', async () => {
const { getByRole, getByTestId } = setup();
fireEvent.click(getByRole('radio', { name: 'Save as...' }));
const selection = getByTestId('mock-async-select');
fireEvent.change(selection, { target: { value: '1' } });
const tabSelector = getByTestId('mock-tree-select');
expect(tabSelector).toBeInTheDocument();
expect(tabSelector).toBeDisabled();
});

test('onDashboardChange triggers tabs load for existing dashboard', async () => {
const dashboardId = mockEvent.value;

fetchMock.get(`glob:*/api/v1/dashboard/${dashboardId}/tabs`, {
json: {
result: {
tab_tree: [{ value: 'tab1', title: 'Main Tab' }],
},
},
});
const component = new PureSaveModal(defaultProps);
const loadTabsMock = jest
.fn()
.mockResolvedValue([{ value: 'tab1', title: 'Main Tab' }]);
component.loadTabs = loadTabsMock;
await component.onDashboardChange({
value: dashboardId,
label: 'Test Dashboard',
});
expect(loadTabsMock).toHaveBeenCalledWith(dashboardId);
});

test('onTabChange updates selectedTab state', () => {
const component = new PureSaveModal(defaultProps);

const setStateMock = jest.fn();
component.setState = setStateMock;

component.state.tabsData = [
{ value: 'tab1', title: 'Main Tab', key: 'tab1' },
{ value: 'tab2', title: 'Analytics Tab', key: 'tab2' },
];
Comment on lines +407 to +410
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Use setState instead of directly modifying component state.

Suggested change
component.state.tabsData = [
{ value: 'tab1', title: 'Main Tab', key: 'tab1' },
{ value: 'tab2', title: 'Analytics Tab', key: 'tab2' },
];
component.setState({
tabsData: [
{ value: 'tab1', title: 'Main Tab', key: 'tab1' },
{ value: 'tab2', title: 'Analytics Tab', key: 'tab2' },
],
});

Copilot uses AI. Check for mistakes.
component.onTabChange('tab2');
expect(setStateMock).toHaveBeenCalledWith({
selectedTab: {
value: 'tab2',
label: 'Analytics Tab',
},
});
});
210 changes: 205 additions & 5 deletions superset-frontend/src/explore/components/SaveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
Input,
Loading,
Divider,
TreeSelect,
} from '@superset-ui/core/components';
import {
DatasourceType,
Expand All @@ -42,11 +43,13 @@ import {
} from '@superset-ui/core';
import { css, styled, Alert } from '@apache-superset/core/ui';
import { Radio } from '@superset-ui/core/components/Radio';
import { Layout } from 'src/dashboard/types';
import { canUserEditDashboard } from 'src/dashboard/util/permissionUtils';
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
import { SaveActionType } from 'src/explore/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { Dashboard } from 'src/types/Dashboard';
import { TabNode, TreeDataNode } from '../types';

// Session storage key for recent dashboard
const SK_DASHBOARD_ID = 'save_chart_recent_dashboard';
Expand All @@ -72,6 +75,8 @@ type SaveModalState = {
isLoading: boolean;
saveStatus?: string | null;
dashboard?: { label: string; value: string | number };
selectedTab?: { label: string; value: string | number };
tabsData: Array<{ value: string; title: string; key: string }>;
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Type inconsistency: tabsData state is typed as Array<{ value: string; title: string; key: string }> (line 79), but it's actually populated with TreeDataNode[] which has a children property (lines 465-474). The state type should be TreeDataNode[] to match the actual data structure.

Suggested change
tabsData: Array<{ value: string; title: string; key: string }>;
tabsData: TreeDataNode[];

Copilot uses AI. Check for mistakes.
};

export const StyledModal = styled(Modal)`
Expand All @@ -94,6 +99,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
action: this.canOverwriteSlice() ? 'overwrite' : 'saveas',
isLoading: false,
dashboard: undefined,
tabsData: [],
};
this.onDashboardChange = this.onDashboardChange.bind(this);
this.onSliceNameChange = this.onSliceNameChange.bind(this);
Expand Down Expand Up @@ -152,10 +158,20 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
this.setState({ newSliceName: event.target.value });
}

onDashboardChange(dashboard: { label: string; value: string | number }) {
this.setState({ dashboard });
}
onDashboardChange = async (dashboard: {
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Inconsistent method binding: The class uses arrow functions for onDashboardChange (line 161) and onTabChange (line 486), but uses explicit binding in the constructor for older methods (lines 104-107). For consistency, either use arrow functions for all event handlers or bind them all in the constructor. Arrow functions are the modern React pattern and eliminate the need for explicit binding.

Copilot uses AI. Check for mistakes.
label: string;
value: string | number;
}) => {
this.setState({
dashboard,
tabsData: [],
selectedTab: undefined,
});

if (typeof dashboard.value === 'number') {
await this.loadTabs(dashboard.value);
}
};
Comment on lines +161 to +174
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Potential race condition: If a user quickly changes the dashboard selection multiple times, multiple loadTabs calls could be in flight simultaneously. The last completed call will set the state, but it may not correspond to the currently selected dashboard. Consider using an AbortController or tracking the latest request to ignore stale responses:

private currentLoadTabsRequest = 0;

onDashboardChange = async (dashboard) => {
  const requestId = ++this.currentLoadTabsRequest;
  // ... existing code ...
  if (typeof dashboard.value === 'number') {
    const tabs = await this.loadTabs(dashboard.value);
    if (requestId === this.currentLoadTabsRequest) {
      this.setState({ tabsData: tabs });
    }
  }
};

Copilot uses AI. Check for mistakes.
changeAction(action: SaveActionType) {
this.setState({ action });
}
Expand Down Expand Up @@ -210,6 +226,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
delete formData.url_params;

let dashboard: DashboardGetResponse | null = null;
let selectedTabId: string | undefined;
if (this.state.dashboard) {
let validId = this.state.dashboard.value;
if (this.isNewDashboard()) {
Expand All @@ -231,6 +248,9 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
? sliceDashboards
: [...sliceDashboards, dashboard.id];
formData.dashboards = sliceDashboards;
if (this.state.action === 'saveas') {
selectedTabId = this.state.selectedTab?.value as string;
Comment on lines +251 to +252
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The condition on line 251 only adds the chart to the selected tab when action === 'saveas', but line 586 shows that the tab selector is only displayed for 'saveas' action. This means charts saved with the 'overwrite' action will be added to the dashboard but not to any specific tab. Consider if this is the intended behavior, or if the condition should check this.state.selectedTab instead.

Suggested change
if (this.state.action === 'saveas') {
selectedTabId = this.state.selectedTab?.value as string;
if (this.state.selectedTab) {
selectedTabId = this.state.selectedTab.value as string;

Copilot uses AI. Check for mistakes.
}
}
}

Expand Down Expand Up @@ -262,6 +282,20 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
}
: null,
);
if (dashboard && selectedTabId) {
try {
await this.addChartToDashboardTab(
dashboard.id,
value.id,
selectedTabId,
);
} catch (error) {
logging.error('Error adding chart to dashboard tab:', error);
this.props.addDangerToast(
t('Chart was saved but could not be added to the selected tab.'),
);
}
}
Comment on lines +285 to +298
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing test coverage for integration between save action and tab placement. Tests should verify:

  • Chart is added to selected tab when saving to a dashboard
  • URL includes tab hash when navigating to dashboard
  • Error toast is shown when tab placement fails but save succeeds
  • selectedTabId is correctly extracted and passed to addChartToDashboardTab

Copilot uses AI. Check for mistakes.
}

try {
Expand All @@ -276,10 +310,13 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {

// Go to new dashboard url
if (gotodash && dashboard) {
this.props.history.push(dashboard.url);
let url = dashboard.url;
if (this.state.selectedTab?.value) {
url += `#${this.state.selectedTab.value}`;
}
this.props.history.push(url);
return;
}

const searchParams = this.handleRedirect(window.location.search, value);
this.props.history.replace(`/explore/?${searchParams.toString()}`);

Expand All @@ -289,6 +326,90 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
this.setState({ isLoading: false });
}
}
addChartToDashboardTab = async (
dashboardId: number,
chartId: number,
tabId: string,
) => {
Comment on lines +329 to +333
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing documentation for the addChartToDashboardTab method. This complex method modifies dashboard layout structure and should include JSDoc comments explaining:

  • The purpose of the method
  • Parameters and their meanings
  • The return value
  • The layout structure it creates (parents hierarchy, row/chart relationship)
  • Potential errors and when they might occur

Copilot uses AI. Check for mistakes.
try {
const dashboardResponse = await SupersetClient.get({
endpoint: `/api/v1/dashboard/${dashboardId}`,
});

const dashboard = dashboardResponse.json.result;

let positionJson = dashboard.position_json;
if (typeof positionJson === 'string') {
positionJson = JSON.parse(positionJson);
}
positionJson = positionJson || {};

const updatedPositionJson = JSON.parse(JSON.stringify(positionJson));
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Unnecessary deep clone: JSON.parse(JSON.stringify(positionJson)) creates a deep copy even though you're directly mutating the cloned object afterward. Consider directly working with the positionJson object or use structured clone if available. This improves performance and readability.

Copilot uses AI. Check for mistakes.

const chartKey = `CHART-${chartId}`;
const rowIndex = this.findNextRowPosition(updatedPositionJson);
const rowKey = `ROW-${rowIndex}`;

updatedPositionJson[chartKey] = {
type: 'CHART',
id: chartKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', rowKey],
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The parents array is inconsistent with the actual hierarchy. If this chart is being added to a specific tab, the parents should be ['ROOT_ID', tabId], not ['ROOT_ID', 'GRID_ID', rowKey]. The current structure suggests GRID_ID is a parent, which may not align with the tab-based layout structure.

Suggested change
parents: ['ROOT_ID', 'GRID_ID', rowKey],
parents: ['ROOT_ID', tabId],

Copilot uses AI. Check for mistakes.
meta: {
width: 4,
height: 50,
Comment on lines +359 to +360
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded chart dimensions (width: 4, height: 50) may not be appropriate for all chart types or dashboard layouts. Consider making these configurable or deriving them from the dashboard's grid settings or chart metadata.

Copilot uses AI. Check for mistakes.
chartId: chartId,
sliceName: `Chart ${chartId}`,
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The meta object is missing the uuid property which is required according to the LayoutItemMeta type definition (line 217 in dashboard/types.ts shows uuid is required). This will cause type inconsistencies and potential runtime errors. Add a uuid property:

meta: {
  width: 4,
  height: 50,
  chartId: chartId,
  sliceName: `Chart ${chartId}`,
  uuid: `${chartKey}-${Date.now()}`, // or use a proper UUID generator
},
Suggested change
sliceName: `Chart ${chartId}`,
sliceName: `Chart ${chartId}`,
uuid: `${chartKey}-${Date.now()}`,

Copilot uses AI. Check for mistakes.
},
};

updatedPositionJson[rowKey] = {
type: 'ROW',
id: rowKey,
children: [chartKey],
parents: ['ROOT_ID', 'GRID_ID', tabId],
meta: {
background: 'BACKGROUND_TRANSPARENT',
},
};

if (updatedPositionJson[tabId]) {
if (!updatedPositionJson[tabId].children) {
updatedPositionJson[tabId].children = [];
}
updatedPositionJson[tabId].children.push(rowKey);
} else {
throw new Error(`Tab ${tabId} not found in positionJson`);
}

const response = await SupersetClient.put({
endpoint: `/api/v1/dashboard/${dashboardId}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
position_json: JSON.stringify(updatedPositionJson),
}),
});

return response;
} catch (error) {
throw new Error('Error adding chart to dashboard tab:', error);
}
};

findNextRowPosition = (layout: Layout): number => {
const rowIndices: number[] = [];

Object.keys(layout).forEach(key => {
if (key.startsWith('ROW-')) {
const rest = key.substring(4);
if (/^\d+$/.test(rest)) {
rowIndices.push(parseInt(rest, 10));
}
}
});

return rowIndices.length > 0 ? Math.max(...rowIndices) + 1 : 0;
};

loadDashboard = async (id: number) => {
const response = await SupersetClient.get({
Expand Down Expand Up @@ -331,6 +452,64 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
totalCount: count,
};
};
loadTabs = async (dashboardId: number) => {
try {
const response = await SupersetClient.get({
endpoint: `/api/v1/dashboard/${dashboardId}/tabs`,
});

const { result } = response.json;
const tabTree = result.tab_tree || [];
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing error handling: If the API endpoint /api/v1/dashboard/${dashboardId}/tabs doesn't exist or returns an unexpected response structure, the code assumes result.tab_tree will be available. While there's a catch block, it would be safer to validate the response structure before accessing result.tab_tree:

const { result } = response.json;
if (!result || !Array.isArray(result.tab_tree)) {
  logging.warn('Invalid tabs response format');
  this.setState({ tabsData: [] });
  return [];
}
const tabTree = result.tab_tree;
Suggested change
const tabTree = result.tab_tree || [];
if (!result || !Array.isArray(result.tab_tree)) {
logging.warn('Invalid tabs response format');
this.setState({ tabsData: [] });
return [];
}
const tabTree = result.tab_tree;

Copilot uses AI. Check for mistakes.

const convertToTreeData = (nodes: TabNode[]): TreeDataNode[] =>
nodes.map(node => ({
value: node.value,
title: node.title,
key: node.value,
children:
node.children && node.children.length > 0
? convertToTreeData(node.children)
: undefined,
}));

const treeData = convertToTreeData(tabTree);
this.setState({ tabsData: treeData });
return treeData;
} catch (error) {
logging.error('Error loading tabs:', error);
this.setState({ tabsData: [] });
return [];
}
};
Comment on lines +455 to +483
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing test coverage for the loadTabs method. This method handles API calls and data transformation. Tests should verify:

  • Successful loading and conversion of tab data
  • Error handling when API call fails
  • Proper conversion from TabNode to TreeDataNode structure
  • State updates with correct data

Copilot uses AI. Check for mistakes.

onTabChange = (value: string) => {
if (value) {
const findTabInTree = (data: TabNode[]): TabNode | null => {
for (const item of data) {
if (item.value === value) {
return item;
}
if (item.children) {
const found = findTabInTree(item.children);
if (found) return found;
}
}
return null;
};

const selectedTab = findTabInTree(this.state.tabsData);
Comment on lines +487 to +500
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Type mismatch in findTabInTree: the function is defined to accept TabNode[] but is being called with this.state.tabsData which is typed as Array<{ value: string; title: string; key: string }> (and should actually be TreeDataNode[]). The parameter type should match the actual state type: (data: TreeDataNode[]) or change state type to use TabNode[].

Copilot uses AI. Check for mistakes.
if (selectedTab) {
this.setState({
selectedTab: {
value: selectedTab.value,
label: selectedTab.title,
},
});
}
} else {
this.setState({ selectedTab: undefined });
}
};

renderSaveChartModal = () => {
const info = this.info();
Expand Down Expand Up @@ -403,6 +582,27 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
}
/>
</FormItem>
{this.state.action === 'saveas' && (
<FormItem
label={t('Add to tabs')}
data-test="save-chart-modal-select-tabs-form"
>
<TreeSelect
showSearch
allowClear
treeDefaultExpandAll
treeData={this.state.tabsData}
onChange={this.onTabChange}
value={this.state.selectedTab?.value}
disabled={
!this.state.dashboard ||
typeof this.state.dashboard.value === 'string' ||
this.state.tabsData.length === 0
}
placeholder={t('Select a tab')}
/>
</FormItem>
)}
{info && <Alert type="info" message={info} closable={false} />}
{this.props.alert && (
<Alert
Expand Down
Loading
Loading