diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.ts b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts
index d13205fa183..0829a8a8013 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.ts
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts
@@ -1998,4 +1998,56 @@ describe('async actions', () => {
});
});
});
+
+ // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
+ describe('toggleLeftBar', () => {
+ const activeId = 'active-qe';
+ const makeState = (hideLeftBar: boolean, unsavedOverride?: boolean) => ({
+ sqlLab: {
+ tabHistory: [activeId],
+ queryEditors: [{ id: activeId, hideLeftBar }],
+ unsavedQueryEditor:
+ unsavedOverride !== undefined
+ ? { id: activeId, hideLeftBar: unsavedOverride }
+ : {},
+ },
+ });
+
+ test('dispatches QUERY_EDITOR_TOGGLE_LEFT_BAR when state differs', () => {
+ const store = mockStore(makeState(false));
+ store.dispatch(actions.toggleLeftBar(true));
+ expect(store.getActions()).toEqual([
+ {
+ type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditorId: activeId,
+ hideLeftBar: true,
+ },
+ ]);
+ });
+
+ test('does not dispatch when state is already the same', () => {
+ const store = mockStore(makeState(true));
+ store.dispatch(actions.toggleLeftBar(true));
+ expect(store.getActions()).toHaveLength(0);
+ });
+
+ test('uses unsavedQueryEditor state when available', () => {
+ // qe in queryEditors says false, but unsaved override says true
+ const store = mockStore(makeState(false, true));
+ store.dispatch(actions.toggleLeftBar(true));
+ expect(store.getActions()).toHaveLength(0);
+ });
+
+ test('does not dispatch when there is no active query editor', () => {
+ const store = mockStore({
+ sqlLab: {
+ tabHistory: [],
+ queryEditors: [],
+ unsavedQueryEditor: {},
+ },
+ });
+ store.dispatch(actions.toggleLeftBar(true));
+ expect(store.getActions()).toHaveLength(0);
+ });
+ });
});
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.ts b/superset-frontend/src/SqlLab/actions/sqlLab.ts
index 9c0eb4c38de..290fa78609b 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.ts
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts
@@ -956,12 +956,24 @@ export function setActiveSouthPaneTab(tabId: string): SqlLabAction {
return { type: SET_ACTIVE_SOUTHPANE_TAB, tabId };
}
-export function toggleLeftBar(queryEditor: QueryEditor): SqlLabAction {
- const hideLeftBar = !queryEditor.hideLeftBar;
- return {
- type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
- queryEditor,
- hideLeftBar,
+export function toggleLeftBar(shouldHide: boolean): SqlLabThunkAction {
+ return (dispatch: AppDispatch, getState: GetState) => {
+ const { sqlLab } = getState();
+ const id = sqlLab.tabHistory.slice(-1)[0];
+ if (!id) return;
+ const qe = sqlLab.queryEditors.find(e => e.id === id);
+ const merged =
+ qe && sqlLab.unsavedQueryEditor?.id === id
+ ? { ...qe, ...sqlLab.unsavedQueryEditor }
+ : qe;
+ if (!merged) return;
+ const isCurrentlyHidden = Boolean(merged.hideLeftBar);
+ if (shouldHide === isCurrentlyHidden) return;
+ dispatch({
+ type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditorId: id,
+ hideLeftBar: shouldHide,
+ });
};
}
diff --git a/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx b/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx
index a25738303a3..eaf8ba3d571 100644
--- a/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx
+++ b/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx
@@ -21,6 +21,7 @@ import { render, userEvent, waitFor } from 'spec/helpers/testing-library';
import { initialState } from 'src/SqlLab/fixtures';
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
import { ViewLocations } from 'src/SqlLab/contributions';
+import * as sqlLabActions from 'src/SqlLab/actions/sqlLab';
import {
registerTestView,
cleanupExtensions,
@@ -108,6 +109,46 @@ test('right sidebar is hidden when no extensions registered', () => {
expect(queryByText('Right Sidebar Content')).not.toBeInTheDocument();
});
+test('dispatches toggleLeftBar(true) when sidebar is resized to zero', async () => {
+ const toggleLeftBarSpy = jest
+ .spyOn(sqlLabActions, 'toggleLeftBar')
+ .mockReturnValue({
+ type: sqlLabActions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ } as any);
+ const { getByRole } = render(, {
+ useRedux: true,
+ initialState,
+ });
+ await userEvent.click(getByRole('button', { name: 'Resize to zero' }));
+ await waitFor(() => expect(toggleLeftBarSpy).toHaveBeenCalledWith(true));
+ toggleLeftBarSpy.mockRestore();
+});
+
+test('dispatches toggleLeftBar(false) when sidebar is resized to non-zero', async () => {
+ const collapsedState = {
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ unsavedQueryEditor: {
+ id: initialState.sqlLab.tabHistory[0],
+ hideLeftBar: true,
+ },
+ },
+ };
+ const toggleLeftBarSpy = jest
+ .spyOn(sqlLabActions, 'toggleLeftBar')
+ .mockReturnValue({
+ type: sqlLabActions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ } as any);
+ const { getByRole } = render(, {
+ useRedux: true,
+ initialState: collapsedState,
+ });
+ await userEvent.click(getByRole('button', { name: 'Resize' }));
+ await waitFor(() => expect(toggleLeftBarSpy).toHaveBeenCalledWith(false));
+ toggleLeftBarSpy.mockRestore();
+});
+
test('renders right sidebar when view is contributed at rightSidebar location', () => {
registerTestView(
ViewLocations.sqllab.rightSidebar,
diff --git a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
index 55fdd413f4f..ac61824f3ae 100644
--- a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { noop } from 'lodash-es';
import type { SqlLabRootState } from 'src/SqlLab/types';
import { css, styled } from '@apache-superset/core/theme';
@@ -32,6 +32,7 @@ import {
} from 'src/SqlLab/constants';
import { ViewLocations } from 'src/SqlLab/contributions';
import ViewListExtension from 'src/components/ViewListExtension';
+import { toggleLeftBar } from 'src/SqlLab/actions/sqlLab';
import SqlEditorLeftBar from '../SqlEditorLeftBar';
import StatusBar from '../StatusBar';
@@ -66,6 +67,7 @@ const ContentWrapper = styled.div`
`;
const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
+ const dispatch = useDispatch();
const queryEditorId = useSelector(
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);
@@ -91,6 +93,7 @@ const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const onSidebarChange = (sizes: number[]) => {
const [updatedWidth, _, possibleRightWidth] = sizes;
setLeftWidth(updatedWidth);
+ dispatch(toggleLeftBar(updatedWidth === 0));
if (typeof possibleRightWidth === 'number') {
setRightWidth(possibleRightWidth);
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 07d547a7f22..c85a79735d3 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -121,6 +121,7 @@ import KeyboardShortcutButton, {
KeyboardShortcut,
} from '../KeyboardShortcutButton';
import SqlEditorTopBar from '../SqlEditorTopBar';
+import SqlEditorLeftBar from '../SqlEditorLeftBar';
const bootstrapData = getBootstrapData();
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
@@ -240,34 +241,36 @@ const SqlEditor: FC = ({
const theme = useTheme();
const dispatch = useAppDispatch();
- const { database, latestQuery, currentQueryEditorId, hasSqlStatement } =
- useSelector<
- SqlLabRootState,
- {
- database?: DatabaseObject;
- latestQuery?: QueryResponse;
- hideLeftBar?: boolean;
- currentQueryEditorId: QueryEditor['id'];
- hasSqlStatement: boolean;
- }
- >(({ sqlLab: { unsavedQueryEditor, databases, queries, tabHistory } }) => {
- let { dbId, latestQueryId, hideLeftBar } = queryEditor;
- if (unsavedQueryEditor?.id === queryEditor.id) {
- dbId = unsavedQueryEditor.dbId || dbId;
- latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
- hideLeftBar =
- typeof unsavedQueryEditor.hideLeftBar === 'boolean'
- ? unsavedQueryEditor.hideLeftBar
- : hideLeftBar;
- }
- return {
- hasSqlStatement: Boolean(queryEditor.sql?.trim().length > 0),
- database: databases[dbId || ''],
- latestQuery: queries[latestQueryId || ''],
- hideLeftBar,
- currentQueryEditorId: tabHistory.slice(-1)[0],
- };
- }, shallowEqual);
+ const {
+ database,
+ latestQuery,
+ hideLeftBar,
+ currentQueryEditorId,
+ hasSqlStatement,
+ } = useSelector<
+ SqlLabRootState,
+ {
+ database?: DatabaseObject;
+ latestQuery?: QueryResponse;
+ hideLeftBar?: boolean;
+ currentQueryEditorId: QueryEditor['id'];
+ hasSqlStatement: boolean;
+ }
+ >(({ sqlLab: { unsavedQueryEditor, databases, queries, tabHistory } }) => {
+ let { dbId, latestQueryId, hideLeftBar } = queryEditor;
+ if (unsavedQueryEditor?.id === queryEditor.id) {
+ dbId = unsavedQueryEditor.dbId || dbId;
+ latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
+ hideLeftBar = unsavedQueryEditor.hideLeftBar === true;
+ }
+ return {
+ hasSqlStatement: Boolean(queryEditor.sql?.trim().length > 0),
+ database: databases[dbId || ''],
+ latestQuery: queries[latestQueryId || ''],
+ hideLeftBar,
+ currentQueryEditorId: tabHistory.slice(-1)[0],
+ };
+ }, shallowEqual);
const logAction = useLogAction({ queryEditorId: queryEditor.id });
const isActive = currentQueryEditorId === queryEditor.id;
@@ -975,6 +978,11 @@ const SqlEditor: FC = ({
queryEditorId={queryEditor.id}
defaultPrimaryActions={renderEditorPrimaryAction()}
defaultSecondaryActions={getSecondaryMenuItems()}
+ extra={
+ hideLeftBar && (
+
+ )
+ }
/>
)}
{queryEditor.isDataset && renderDatasetWarning()}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 4031bf1caa6..9071f3a0507 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -39,6 +39,7 @@ import TableExploreTree from '../TableExploreTree';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
+ collapsed?: boolean;
}
const LeftBarStyles = styled.div`
@@ -63,7 +64,10 @@ const StyledDivider = styled.div`
margin: 0 -${({ theme }) => theme.sizeUnit * 2.5}px 0;
`;
-const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
+const SqlEditorLeftBar = ({
+ queryEditorId,
+ collapsed = false,
+}: SqlEditorLeftBarProps) => {
const activeQEId = queryEditorId || EMPTY_STATE_QE_ID;
const dbSelectorProps = useDatabaseSelector(activeQEId);
const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } =
@@ -180,29 +184,38 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
);
+ const dbSelectorTrigger = (
+ !open && closeSelectorModal()}
+ placement="bottomLeft"
+ trigger="click"
+ >
+ {/* Wrap in a span so the Popover can attach a ref without relying
+ on findDOMNode (deprecated in React 18+). */}
+
+ }
+ sqlLabMode
+ compactMode={collapsed}
+ onOpenModal={openSelectorModal}
+ />
+
+
+ );
+
+ if (collapsed) {
+ return dbSelectorTrigger;
+ }
+
return (
- !open && closeSelectorModal()}
- placement="bottomLeft"
- trigger="click"
- >
- {/* Wrap in a span so the Popover can attach a ref without relying
- on findDOMNode (deprecated in React 18+). */}
-
- }
- sqlLabMode
- onOpenModal={openSelectorModal}
- />
-
-
+ {dbSelectorTrigger}
{shouldShowReset && (
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
index 98569bcfc15..b6bb7553632 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
@@ -41,7 +41,6 @@ import {
removeAllOtherQueryEditors,
queryEditorSetTitle,
cloneQueryToNewTab,
- toggleLeftBar,
} from 'src/SqlLab/actions/sqlLab';
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { Icons, type IconType } from '@superset-ui/core/components/Icons';
@@ -105,7 +104,6 @@ const SqlEditorTabHeader: FC = ({ queryEditor }) => {
removeAllOtherQueryEditors,
queryEditorSetTitle,
cloneQueryToNewTab,
- toggleLeftBar,
},
dispatch,
),
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx
index db87975ea9b..f051bc6b181 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/index.tsx
@@ -31,11 +31,13 @@ export interface SqlEditorTopBarProps {
queryEditorId: string;
defaultPrimaryActions: React.ReactNode;
defaultSecondaryActions: MenuItemType[];
+ extra?: React.ReactNode;
}
const SqlEditorTopBar = ({
defaultPrimaryActions,
defaultSecondaryActions,
+ extra,
}: SqlEditorTopBarProps) => (
@@ -47,6 +49,7 @@ const SqlEditorTopBar = ({
/>
+ {extra}
);
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts b/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts
index 3573405bfe0..717eed24e66 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts
@@ -293,6 +293,45 @@ describe('sqlLabReducer', () => {
newQueryEditor.tabViewId,
);
});
+ test('should toggle hideLeftBar via queryEditorId for the active editor', () => {
+ const action = {
+ type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditorId: qe!.id,
+ hideLeftBar: true,
+ };
+ newState = sqlLabReducer(newState, action as SqlLabAction);
+ expect(newState.unsavedQueryEditor.hideLeftBar).toBe(true);
+ expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
+ });
+
+ test('should toggle hideLeftBar back to false via queryEditorId', () => {
+ // first set to true
+ newState = sqlLabReducer(newState, {
+ type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditorId: qe!.id,
+ hideLeftBar: true,
+ } as SqlLabAction);
+ // then back to false
+ newState = sqlLabReducer(newState, {
+ type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditorId: qe!.id,
+ hideLeftBar: false,
+ } as SqlLabAction);
+ expect(newState.unsavedQueryEditor.hideLeftBar).toBe(false);
+ });
+
+ test('should toggle hideLeftBar in queryEditors array for non-active editor', () => {
+ const nonActiveId = defaultQueryEditor.id;
+ const action = {
+ type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
+ queryEditorId: nonActiveId,
+ hideLeftBar: true,
+ };
+ newState = sqlLabReducer(newState, action as SqlLabAction);
+ const updated = newState.queryEditors.find(e => e.id === nonActiveId);
+ expect(updated?.hideLeftBar).toBe(true);
+ });
+
test('should clear the destroyed query editors', () => {
const expectedQEId = '1233289';
const action = {
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.ts b/superset-frontend/src/SqlLab/reducers/sqlLab.ts
index c4593a73683..bb8d5a0d343 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.ts
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.ts
@@ -715,7 +715,7 @@ export default function sqlLabReducer(
{
hideLeftBar: action.hideLeftBar,
},
- action.queryEditor!.id!,
+ action.queryEditorId!,
),
};
},
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx
index e4f7e4f6626..cfa045462f2 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -186,6 +186,7 @@ export function DatabaseSelector({
onSchemaChange,
schema,
readOnly = false,
+ compactMode = false,
sqlLabMode = false,
onOpenModal,
}: DatabaseSelectorProps) {
@@ -382,6 +383,7 @@ export function DatabaseSelector({
});
const catalogOptions = catalogData || EMPTY_CATALOG_OPTIONS;
+ const sqlLabCompactMode = sqlLabMode && compactMode;
function changeDatabase(
value: { label: string; value: number },
@@ -604,8 +606,8 @@ export function DatabaseSelector({
>
{renderDatabaseSelect()}
{renderError()}
- {showCatalogSelector && renderCatalogSelect()}
- {showSchemaSelector && renderSchemaSelect()}
+ {!sqlLabCompactMode && showCatalogSelector && renderCatalogSelect()}
+ {!sqlLabCompactMode && showSchemaSelector && renderSchemaSelect()}
);
}
diff --git a/superset-frontend/src/components/DatabaseSelector/types.ts b/superset-frontend/src/components/DatabaseSelector/types.ts
index 5b10cae8d35..886fa28332d 100644
--- a/superset-frontend/src/components/DatabaseSelector/types.ts
+++ b/superset-frontend/src/components/DatabaseSelector/types.ts
@@ -50,5 +50,6 @@ export interface DatabaseSelectorProps {
schema?: string;
readOnly?: boolean;
sqlLabMode?: boolean;
+ compactMode?: boolean;
onOpenModal?: () => void;
}
diff --git a/superset-frontend/src/components/ResizableSidebar/useStoredSidebarWidth.ts b/superset-frontend/src/components/ResizableSidebar/useStoredSidebarWidth.ts
index 360a8cd5587..66272f1b801 100644
--- a/superset-frontend/src/components/ResizableSidebar/useStoredSidebarWidth.ts
+++ b/superset-frontend/src/components/ResizableSidebar/useStoredSidebarWidth.ts
@@ -34,7 +34,7 @@ export default function useStoredSidebarWidth(
widthsMapRef.current =
widthsMapRef.current ??
getItem(LocalStorageKeys.CommonResizableSidebarWidths, {});
- if (widthsMapRef.current[id]) {
+ if (typeof widthsMapRef.current[id] === 'number') {
setSidebarWidth(widthsMapRef.current[id]);
}
}, [id]);