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]);