diff --git a/superset-frontend/src/SqlLab/components/App/App.test.tsx b/superset-frontend/src/SqlLab/components/App/App.test.tsx index e3567ced4a3..4d1f922add4 100644 --- a/superset-frontend/src/SqlLab/components/App/App.test.tsx +++ b/superset-frontend/src/SqlLab/components/App/App.test.tsx @@ -32,9 +32,11 @@ import { LOG_ACTIONS_SQLLAB_MONITOR_LOCAL_STORAGE_USAGE, } from 'src/logger/LogUtils'; -jest.mock('src/SqlLab/components/TabbedSqlEditors', () => () => ( -
+// eslint-disable-next-line react/display-name +jest.mock('src/SqlLab/components/PopEditorTab', () => () => ( +
)); +// eslint-disable-next-line react/display-name jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
)); @@ -68,7 +70,7 @@ describe('SqlLab App', () => { test('should render', () => { const { getByTestId } = render(, { useRedux: true, store }); expect(getByTestId('SqlLabApp')).toBeInTheDocument(); - expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument(); + expect(getByTestId('mock-pop-editor-tab')).toBeInTheDocument(); }); test('reset hotkey events on unmount', () => { diff --git a/superset-frontend/src/SqlLab/components/App/index.tsx b/superset-frontend/src/SqlLab/components/App/index.tsx index b0d197577a2..7e19359a03f 100644 --- a/superset-frontend/src/SqlLab/components/App/index.tsx +++ b/superset-frontend/src/SqlLab/components/App/index.tsx @@ -37,6 +37,8 @@ import { } from 'src/logger/LogUtils'; import TabbedSqlEditors from '../TabbedSqlEditors'; import QueryAutoRefresh from '../QueryAutoRefresh'; +import PopEditorTab from '../PopEditorTab'; +import AppLayout from '../AppLayout'; const SqlLabStyles = styled.div` ${({ theme }) => css` @@ -46,7 +48,7 @@ const SqlLabStyles = styled.div` right: 0; bottom: 0; left: 0; - padding: 0 ${theme.sizeUnit * 2}px; + padding: 0; pre:not(.code) { padding: 0 !important; @@ -216,7 +218,11 @@ class App extends PureComponent { queries={queries} queriesLastUpdate={queriesLastUpdate} /> - + + + + + ); } diff --git a/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx b/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx new file mode 100644 index 00000000000..876928bd4fa --- /dev/null +++ b/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx @@ -0,0 +1,174 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { render, userEvent, waitFor } from 'spec/helpers/testing-library'; +import { initialState } from 'src/SqlLab/fixtures'; +import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth'; +import type { contributions, core } from '@apache-superset/core'; +import ExtensionsManager from 'src/extensions/ExtensionsManager'; +import { ViewContribution } from 'src/SqlLab/contributions'; +import AppLayout from './index'; + +jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth'); +jest.mock('src/components/Splitter', () => { + const Splitter = ({ + onResizeEnd, + children, + }: { + onResizeEnd: (sizes: number[]) => void; + children: React.ReactNode; + }) => ( +
+ {children} + + +
+ ); + // eslint-disable-next-line react/display-name + Splitter.Panel = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); + return { Splitter }; +}); +jest.mock('@superset-ui/core/components/Grid', () => ({ + ...jest.requireActual('@superset-ui/core/components/Grid'), + useBreakpoint: jest.fn().mockReturnValue(true), +})); + +const defaultProps = { + children:
Child
, +}; + +function createMockView( + id: string, + overrides: Partial = {}, +): contributions.ViewContribution { + return { + id, + name: `${id} View`, + ...overrides, + }; +} + +function setupActivatedExtension( + manager: ExtensionsManager, + extension: core.Extension, +) { + const context = { disposables: [] }; + (manager as any).contextIndex.set(extension.id, context); + (manager as any).extensionContributions.set(extension.id, { + commands: extension.contributions.commands, + menus: extension.contributions.menus, + views: extension.contributions.views, + }); +} + +async function createActivatedExtension( + manager: ExtensionsManager, + extensionOptions: Partial = {}, +): Promise { + const mockExtension: core.Extension = { + id: 'test-extension', + name: 'Test Extension', + description: 'A test extension', + version: '1.0.0', + dependencies: [], + remoteEntry: '', + exposedModules: [], + extensionDependencies: [], + contributions: { + commands: [], + menus: {}, + views: {}, + }, + activate: jest.fn(), + deactivate: jest.fn(), + ...extensionOptions, + }; + + await manager.initializeExtension(mockExtension); + setupActivatedExtension(manager, mockExtension); + + return mockExtension; +} + +beforeEach(() => { + jest.clearAllMocks(); + (useStoredSidebarWidth as jest.Mock).mockReturnValue([250, jest.fn()]); + (ExtensionsManager as any).instance = undefined; +}); + +test('renders two panels', () => { + const { getAllByTestId } = render(, { + useRedux: true, + initialState, + }); + expect(getAllByTestId('mock-panel')).toHaveLength(2); +}); + +test('renders children', () => { + const { getByText } = render(, { + useRedux: true, + initialState, + }); + expect(getByText('Child')).toBeInTheDocument(); +}); + +test('calls setWidth on sidebar resize when not hidden', async () => { + const setWidth = jest.fn(); + (useStoredSidebarWidth as jest.Mock).mockReturnValue([250, setWidth]); + const { getByRole } = render(, { + useRedux: true, + initialState, + }); + + // toggle sidebar to show + await userEvent.click(getByRole('button', { name: 'Resize' })); + // set different width + await userEvent.click(getByRole('button', { name: 'Resize' })); + await waitFor(() => expect(setWidth).toHaveBeenCalled()); +}); + +test('renders right sidebar when RIGHT_SIDEBAR_VIEW_ID view is contributed', async () => { + const manager = ExtensionsManager.getInstance(); + const viewId = 'test-right-sidebar-view'; + await createActivatedExtension(manager, { + contributions: { + commands: [], + menus: {}, + views: { + [ViewContribution.RightSidebar]: [createMockView(viewId)], + }, + }, + }); + + const { getByText, getAllByTestId } = render( + , + { + useRedux: true, + initialState, + }, + ); + + expect(getByText('Child')).toBeInTheDocument(); + expect(getAllByTestId('mock-panel')).toHaveLength(3); +}); diff --git a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx new file mode 100644 index 00000000000..8d8f06d34ec --- /dev/null +++ b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx @@ -0,0 +1,138 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useSelector } from 'react-redux'; +import { noop } from 'lodash'; +import type { SqlLabRootState } from 'src/SqlLab/types'; +import { styled } from '@apache-superset/core'; +import { useComponentDidUpdate } from '@superset-ui/core'; +import { Grid } from '@superset-ui/core/components'; +import ExtensionsManager from 'src/extensions/ExtensionsManager'; +import { useExtensionsContext } from 'src/extensions/ExtensionsContext'; +import { Splitter } from 'src/components/Splitter'; +import useEffectEvent from 'src/hooks/useEffectEvent'; +import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth'; +import { + SQL_EDITOR_LEFTBAR_WIDTH, + SQL_EDITOR_RIGHTBAR_WIDTH, +} from 'src/SqlLab/constants'; + +import SqlEditorLeftBar from '../SqlEditorLeftBar'; +import { ViewContribution } from 'src/SqlLab/contributions'; + +const StyledContainer = styled.div` + height: 100%; + + & .ant-splitter-panel:not(.sqllab-body):not(.queryPane) { + background-color: ${({ theme }) => theme.colorBgBase}; + } + + & .sqllab-body { + flex-grow: 1 !important; + padding-top: ${({ theme }) => theme.sizeUnit * 2.5}px; + } +`; + +const StyledSidebar = styled.div` + position: relative; + padding: ${({ theme }) => theme.sizeUnit * 2.5}px; +`; + +const ContentWrapper = styled.div` + flex: 1; + overflow: auto; +`; + +const AppLayout: React.FC = ({ children }) => { + const queryEditorId = useSelector( + ({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0], + ); + const { md } = Grid.useBreakpoint(); + const [leftWidth, setLeftWidth] = useStoredSidebarWidth( + 'sqllab:leftbar', + SQL_EDITOR_LEFTBAR_WIDTH, + ); + const [rightWidth, setRightWidth] = useStoredSidebarWidth( + 'sqllab:rightbar', + SQL_EDITOR_RIGHTBAR_WIDTH, + ); + const autoHide = useEffectEvent(() => { + if (leftWidth > 0) { + setLeftWidth(0); + } + }); + useComponentDidUpdate(() => { + if (!md) { + autoHide(); + } + }, [md]); + const onSidebarChange = (sizes: number[]) => { + const [updatedWidth, _, possibleRightWidth] = sizes; + setLeftWidth(updatedWidth); + + if (typeof possibleRightWidth === 'number') { + setRightWidth(possibleRightWidth); + } + }; + const contributions = + ExtensionsManager.getInstance().getViewContributions( + ViewContribution.RightSidebar, + ) || []; + const { getView } = useExtensionsContext(); + + return ( + + + + + + + + {children} + {contributions.length > 0 && ( + + + {contributions.map(contribution => getView(contribution.id))} + + + )} + + + ); +}; + +export default AppLayout; diff --git a/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx b/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx new file mode 100644 index 00000000000..e66e5bba91c --- /dev/null +++ b/superset-frontend/src/SqlLab/components/PopEditorTab/PopEditorTab.test.tsx @@ -0,0 +1,137 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { MemoryRouter } from 'react-router-dom'; +import { render, waitFor } from 'spec/helpers/testing-library'; +import fetchMock from 'fetch-mock'; +import { initialState } from 'src/SqlLab/fixtures'; +import { Store } from 'redux'; +import { RootState } from 'src/views/store'; + +import PopEditorTab from '.'; +import { LocationProvider } from 'src/pages/SqlLab/LocationContext'; + +const setup = ( + url = '/sqllab', + overridesStore?: Store, + overridesInitialState?: RootState, +) => + render( + + + + + , + { + useRedux: true, + initialState: overridesInitialState || initialState, + ...(overridesStore && { store: overridesStore }), + }, + ); + +beforeEach(() => { + fetchMock.get('glob:*/api/v1/database/*', {}); + fetchMock.get('glob:*/api/v1/saved_query/*', { + result: { + id: 2, + database: { id: 1 }, + label: 'test', + sql: 'SELECT * FROM test_table', + }, + }); +}); + +afterEach(() => { + fetchMock.reset(); +}); + +let replaceState = jest.spyOn(window.history, 'replaceState'); +beforeEach(() => { + replaceState = jest.spyOn(window.history, 'replaceState'); +}); +afterEach(() => { + replaceState.mockReset(); +}); + +test('should handle id', async () => { + const id = 1; + fetchMock.get(`glob:*/api/v1/sqllab/permalink/kv:${id}`, { + label: 'test permalink', + sql: 'SELECT * FROM test_table', + dbId: 1, + }); + setup('/sqllab?id=1'); + await waitFor(() => + expect( + fetchMock.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`), + ).toHaveLength(1), + ); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/sqllab', + ); + fetchMock.reset(); +}); +test('should handle permalink', async () => { + const key = '9sadkfl'; + fetchMock.get(`glob:*/api/v1/sqllab/permalink/${key}`, { + label: 'test permalink', + sql: 'SELECT * FROM test_table', + dbId: 1, + }); + setup('/sqllab/p/9sadkfl'); + await waitFor(() => + expect( + fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`), + ).toHaveLength(1), + ); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/sqllab', + ); + fetchMock.reset(); +}); +test('should handle savedQueryId', async () => { + setup('/sqllab?savedQueryId=1'); + await waitFor(() => + expect(fetchMock.calls('glob:*/api/v1/saved_query/1')).toHaveLength(1), + ); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/sqllab', + ); +}); +test('should handle sql', () => { + setup('/sqllab?sql=1&dbid=1'); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/sqllab', + ); +}); +test('should handle custom url params', () => { + setup('/sqllab?sql=1&dbid=1&custom_value=str&extra_attr1=true'); + expect(replaceState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/sqllab?custom_value=str&extra_attr1=true', + ); +}); diff --git a/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx b/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx new file mode 100644 index 00000000000..8977d4c3598 --- /dev/null +++ b/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx @@ -0,0 +1,122 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import URI from 'urijs'; +import { pick } from 'lodash'; +import { useComponentDidUpdate } from '@superset-ui/core'; +import { Skeleton } from '@superset-ui/core/components'; +import useEffectEvent from 'src/hooks/useEffectEvent'; +import { useLocationState } from 'src/pages/SqlLab/LocationContext'; +import { + addNewQueryEditor, + addQueryEditor, + popDatasourceQuery, + popPermalink, + popQuery, + popSavedQuery, + popStoredQuery, +} from 'src/SqlLab/actions/sqlLab'; +import { SqlLabRootState } from 'src/SqlLab/types'; +import { navigateWithState } from 'src/utils/navigationUtils'; +import getBootstrapData from 'src/utils/getBootstrapData'; + +const SQL_LAB_URL = '/sqllab'; + +const PopEditorTab: React.FC = ({ children }) => { + const [isLoading, setIsLoading] = useState(false); + const [queryEditorId, setQueryEditorId] = useState(); + const { requestedQuery } = useLocationState(); + const activeQueryEditorId = useSelector( + ({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0], + ); + const [updatedUrl, setUpdatedUrl] = useState(SQL_LAB_URL); + const dispatch = useDispatch(); + useComponentDidUpdate(() => { + setQueryEditorId(assigned => assigned ?? activeQueryEditorId); + if (activeQueryEditorId) { + navigateWithState(updatedUrl, {}, { replace: true }); + } + }, [activeQueryEditorId]); + + const popSqlEditor = useEffectEvent(() => { + const bootstrapData = getBootstrapData(); + const { + id = undefined, + name = undefined, + sql = undefined, + savedQueryId = undefined, + datasourceKey = undefined, + queryId = undefined, + dbid = 0, + catalog = undefined, + schema = undefined, + autorun = false, + permalink = undefined, + new: isNewQuery = undefined, + ...restUrlParams + } = { + ...requestedQuery, + ...bootstrapData.requested_query, + }; + + // Popping a new tab based on the querystring + if (permalink || id || sql || savedQueryId || datasourceKey || queryId) { + setIsLoading(true); + const targetUrl = `${URI(SQL_LAB_URL).query(pick(requestedQuery, Object.keys(restUrlParams)))}`; + setUpdatedUrl(targetUrl); + if (permalink) { + dispatch(popPermalink(permalink)); + } else if (id) { + dispatch(popStoredQuery(id)); + } else if (savedQueryId) { + dispatch(popSavedQuery(savedQueryId)); + } else if (queryId) { + dispatch(popQuery(queryId)); + } else if (datasourceKey) { + dispatch(popDatasourceQuery(datasourceKey, sql)); + } else if (sql) { + const newQueryEditor = { + name, + dbId: Number(dbid), + catalog, + schema, + autorun, + sql, + }; + dispatch(addQueryEditor(newQueryEditor)); + } + } else if (isNewQuery) { + setIsLoading(true); + dispatch(addNewQueryEditor()); + } + }); + + useEffect(() => { + popSqlEditor(); + }, [popSqlEditor]); + + if (isLoading && !queryEditorId) { + return ; + } + + return <>{children}; +}; + +export default PopEditorTab; diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx index ddc457c0af8..9884c1bb206 100644 --- a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx @@ -22,7 +22,7 @@ import { useInView } from 'react-intersection-observer'; import { omit } from 'lodash'; import { EmptyState, Skeleton } from '@superset-ui/core/components'; import { t, FeatureFlag, isFeatureEnabled } from '@superset-ui/core'; -import { styled, css, useTheme } from '@apache-superset/core/ui'; +import { styled, css } from '@apache-superset/core/ui'; import QueryTable from 'src/SqlLab/components/QueryTable'; import { SqlLabRootState } from 'src/SqlLab/types'; import { useEditorQueriesQuery } from 'src/hooks/apiResources/queries'; @@ -62,7 +62,6 @@ const QueryHistory = ({ const { id, tabViewId } = useQueryEditor(String(queryEditorId), [ 'tabViewId', ]); - const theme = useTheme(); const editorId = tabViewId ?? id; const [ref, hasReachedBottom] = useInView({ threshold: 0 }); const [pageIndex, setPageIndex] = useState(0); @@ -118,11 +117,7 @@ const QueryHistory = ({ } return editorQueries.length > 0 ? ( -
+ <> )} {isFetching && } -
+ ) : ( theme.sizeUnit * 4}px; `; const ResultSetButtons = styled.div` @@ -722,7 +721,6 @@ const ResultSet = ({ css={css` display: flex; justify-content: space-between; - padding-left: ${theme.sizeUnit * 4}px; align-items: center; gap: ${GAP}px; `} @@ -758,7 +756,6 @@ const ResultSet = ({
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx index 77c6e8a5b3a..b154371addd 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx @@ -41,6 +41,7 @@ import { } from '../../constants'; import Results from './Results'; import TablePreview from '../TablePreview'; +import { ViewContribution } from 'src/SqlLab/contributions'; /* editorQueries are queries executed by users passed from SqlEditor component @@ -99,7 +100,9 @@ const SouthPane = ({ const theme = useTheme(); const dispatch = useDispatch(); const contributions = - ExtensionsManager.getInstance().getViewContributions('sqllab.panels') || []; + ExtensionsManager.getInstance().getViewContributions( + ViewContribution.SouthPanels, + ) || []; const { getView } = useExtensionsContext(); const { offline, tables } = useSelector( ({ sqlLab: { offline, tables } }: SqlLabRootState) => ({ diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx index c47aaef24c7..b0b7cd03e27 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx @@ -38,7 +38,6 @@ import { table, defaultQueryEditor, } from 'src/SqlLab/fixtures'; -import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar'; import ResultSet from 'src/SqlLab/components/ResultSet'; import { api } from 'src/hooks/apiResources/queryApi'; import setupCodeOverrides from 'src/setup/setupCodeOverrides'; @@ -74,7 +73,6 @@ jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({ /> ), })); -jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn()); jest.mock('src/SqlLab/components/ResultSet', () => jest.fn()); fetchMock.get('glob:*/api/v1/database/*/function_names/', { @@ -177,10 +175,6 @@ describe('SqlEditor', () => { store = createStore(mockInitialState); actions = []; - (SqlEditorLeftBar as jest.Mock).mockClear(); - (SqlEditorLeftBar as jest.Mock).mockImplementation(() => ( -
- )); (ResultSet as unknown as jest.Mock).mockClear(); (ResultSet as unknown as jest.Mock).mockImplementation(() => (
@@ -211,17 +205,6 @@ describe('SqlEditor', () => { ).toBeInTheDocument(); }); - test('render a SqlEditorLeftBar', async () => { - const { getByTestId, unmount } = setup(mockedProps, store); - - await waitFor( - () => expect(getByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(), - { timeout: 10000 }, - ); - - unmount(); - }, 15000); - // Update other similar tests with timeouts test('render an AceEditorWrapper', async () => { const { findByTestId, unmount } = setup(mockedProps, store); @@ -235,14 +218,13 @@ describe('SqlEditor', () => { }, 15000); test('skip rendering an AceEditorWrapper when the current tab is inactive', async () => { - const { findByTestId, queryByTestId } = setup( + const { queryByTestId } = setup( { ...mockedProps, queryEditor: initialState.sqlLab.queryEditors[1], }, store, ); - expect(await findByTestId('mock-sql-editor-left-bar')).toBeInTheDocument(); expect(queryByTestId('react-ace')).not.toBeInTheDocument(); }); @@ -250,14 +232,10 @@ describe('SqlEditor', () => { const { findByTestId } = setup(mockedProps, store); const editor = await findByTestId('react-ace'); const sql = 'select *'; - const renderCount = (SqlEditorLeftBar as jest.Mock).mock.calls.length; const renderCountForSouthPane = (ResultSet as unknown as jest.Mock).mock .calls.length; - expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount); expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane); fireEvent.change(editor, { target: { value: sql } }); - // Verify the rendering regression - expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount); expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane); }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index 9c3795e29ce..99777784301 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx @@ -47,7 +47,7 @@ import type { CursorPosition, } from 'src/SqlLab/types'; import type { DatabaseObject } from 'src/features/databases/types'; -import { debounce, isEmpty, noop } from 'lodash'; +import { debounce, isEmpty } from 'lodash'; import Mousetrap from 'mousetrap'; import { Button, @@ -57,7 +57,6 @@ import { Modal, Timer, } from '@superset-ui/core/components'; -import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth'; import { Splitter } from 'src/components/Splitter'; import { Skeleton } from '@superset-ui/core/components/Skeleton'; import { Switch } from '@superset-ui/core/components/Switch'; @@ -84,12 +83,10 @@ import { formatQuery, fetchQueryEditor, switchQueryEditor, - toggleLeftBar, } from 'src/SqlLab/actions/sqlLab'; import { STATE_TYPE_MAP, SQL_EDITOR_GUTTER_HEIGHT, - SQL_EDITOR_LEFTBAR_WIDTH, INITIAL_NORTH_PERCENT, SET_QUERY_EDITOR_SQL_DEBOUNCE_MS, } from 'src/SqlLab/constants'; @@ -119,7 +116,6 @@ import SaveQuery, { QueryPayload } from '../SaveQuery'; import ScheduleQueryButton from '../ScheduleQueryButton'; import EstimateQueryCostButton from '../EstimateQueryCostButton'; import ShareSqlLabQuery from '../ShareSqlLabQuery'; -import SqlEditorLeftBar from '../SqlEditorLeftBar'; import AceEditorWrapper from '../AceEditorWrapper'; import RunQueryActionButton from '../RunQueryActionButton'; import QueryLimitSelect from '../QueryLimitSelect'; @@ -148,6 +144,8 @@ const StyledToolbar = styled.div` .rightItems { display: flex; align-items: center; + flex-wrap: wrap; + gap: ${({ theme }) => theme.sizeUnit}px; & > span { margin-right: ${({ theme }) => theme.sizeUnit * 2}px; display: inline-block; @@ -163,26 +161,12 @@ const StyledToolbar = styled.div` } `; -const StyledSidebar = styled.div` - padding: ${({ theme }) => theme.sizeUnit * 2.5}px; - height: 100%; - display: flex; - flex-direction: column; -`; - const StyledSqlEditor = styled.div` ${({ theme }) => css` - display: flex; - flex-direction: row; height: 100%; - .schemaPane { - transition: transform ${theme.motionDurationMid} ease-in-out; - } - .queryPane { - padding: ${theme.sizeUnit * 2}px; - padding-left: 0px; + padding: ${theme.sizeUnit * 2}px 0px; + .ant-splitter-bar .ant-splitter-bar-dragger { &::before { background: transparent; @@ -198,6 +182,14 @@ const StyledSqlEditor = styled.div` .north-pane { height: 100%; + margin: 0 ${theme.sizeUnit * 4}px; + } + + .SouthPane .ant-tabs-tabpane { + margin: 0 ${theme.sizeUnit * 4}px; + & .ant-tabs { + margin: 0 ${theme.sizeUnit * -4}px; + } } .sql-container { @@ -228,39 +220,34 @@ const SqlEditor: FC = ({ const theme = useTheme(); const dispatch = useDispatch(); - 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 = - 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, 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 logAction = useLogAction({ queryEditorId: queryEditor.id }); const isActive = currentQueryEditorId === queryEditor.id; @@ -283,8 +270,6 @@ const SqlEditor: FC = ({ [database], ); - const sqlEditorRef = useRef(null); - const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form'); const startQuery = useCallback( @@ -751,6 +736,7 @@ const SqlEditor: FC = ({ return (