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}
+ onResizeEnd([500])}>
+ Resize
+
+ onResizeEnd([0])}>
+ Resize to zero
+
+
+ );
+ // 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 (
commands.executeCommand(command.command)}
tooltip={command?.description}
icon={ }
@@ -1012,68 +998,31 @@ const SqlEditor: FC = ({
? t('Specify name to CREATE VIEW AS schema in: public')
: t('Specify name to CREATE TABLE AS schema in: public');
- const [width, setWidth] = useStoredSidebarWidth(
- `sqllab:${queryEditor.id}`,
- SQL_EDITOR_LEFTBAR_WIDTH,
- );
-
- const onSidebarChange = useCallback(
- (sizes: number[]) => {
- const [updatedWidth] = sizes;
- if (hideLeftBar || updatedWidth === 0) {
- dispatch(toggleLeftBar({ id: queryEditor.id, hideLeftBar }));
- if (hideLeftBar) {
- // Due to a bug in the splitter, the width must be changed
- // in order to properly restore the previous size
- setWidth(width + 0.01);
- }
- } else {
- setWidth(updatedWidth);
- }
- },
- [dispatch, hideLeftBar],
- );
-
return (
-
-
-
+ {shouldLoadQueryEditor ? (
+
-
-
-
-
-
- {shouldLoadQueryEditor ? (
-
-
-
- ) : showEmptyState && !hasSqlStatement ? (
-
- ) : (
- queryPane()
+
+
+ ) : showEmptyState && !hasSqlStatement ? (
+
-
+ />
+ ) : (
+ queryPane()
+ )}
+
{
@@ -111,7 +113,14 @@ test('renders a TableElement', async () => {
const { findByText, getAllByTestId } = await renderAndWait(
mockedProps,
undefined,
- { ...initialState, sqlLab: { ...initialState.sqlLab, tables: [table] } },
+ {
+ ...initialState,
+ sqlLab: {
+ ...initialState.sqlLab,
+ tables: [table],
+ databases: { [mockData.database.id]: { ...mockData.database } },
+ },
+ },
);
expect(await findByText(/Database/i)).toBeInTheDocument();
const tableElement = getAllByTestId('table-element');
@@ -122,7 +131,11 @@ test('table should be visible when expanded is true', async () => {
const { container, getByText, getByRole, getAllByLabelText } =
await renderAndWait(mockedProps, undefined, {
...initialState,
- sqlLab: { ...initialState.sqlLab, tables: [table] },
+ sqlLab: {
+ ...initialState.sqlLab,
+ tables: [table],
+ databases: { [mockData.database.id]: { ...mockData.database } },
+ },
});
const dbSelect = getByRole('combobox', {
@@ -151,17 +164,24 @@ test('table should be visible when expanded is true', async () => {
test('catalog selector should be visible when enabled in the database', async () => {
const { container, getByText, getByRole } = await renderAndWait(
- {
- ...mockedProps,
- database: {
- ...mockedProps.database,
- allow_multi_catalog: true,
- },
- },
+ mockedProps,
undefined,
{
...initialState,
- sqlLab: { ...initialState.sqlLab, tables: [table] },
+ sqlLab: {
+ ...initialState.sqlLab,
+ unsavedQueryEditor: {
+ id: mockedProps.queryEditorId,
+ dbId: mockData.database.id,
+ },
+ tables: [table],
+ databases: {
+ [mockData.database.id]: {
+ ...mockData.database,
+ allow_multi_catalog: true,
+ },
+ },
+ },
},
);
@@ -194,7 +214,20 @@ test('catalog selector should be visible when enabled in the database', async ()
test('should toggle the table when the header is clicked', async () => {
const { container } = await renderAndWait(mockedProps, undefined, {
...initialState,
- sqlLab: { ...initialState.sqlLab, tables: [table] },
+ sqlLab: {
+ ...initialState.sqlLab,
+ tables: [table],
+ unsavedQueryEditor: {
+ id: mockedProps.queryEditorId,
+ dbId: mockData.database.id,
+ },
+ databases: {
+ [mockData.database.id]: {
+ ...mockData.database,
+ allow_multi_catalog: true,
+ },
+ },
+ },
});
const header = container.querySelector('.ant-collapse-header');
@@ -219,6 +252,7 @@ test('When changing database the schema and table list must be updated', async (
unsavedQueryEditor: {
id: defaultQueryEditor.id,
schema: 'db1_schema',
+ dbId: mockData.database.id,
},
queryEditors: [
defaultQueryEditor,
@@ -242,6 +276,17 @@ test('When changing database the schema and table list must be updated', async (
queryEditorId: extraQueryEditor1.id,
},
],
+ databases: {
+ [mockData.database.id]: {
+ ...mockData.database,
+ allow_multi_catalog: true,
+ },
+ 2: {
+ id: 2,
+ database_name: 'new_db',
+ backend: 'postgresql',
+ },
+ },
},
};
const { rerender } = await renderAndWait(mockedProps, undefined, reduxState);
@@ -250,15 +295,7 @@ test('When changing database the schema and table list must be updated', async (
expect(screen.getAllByText(/ab_user/i)[0]).toBeInTheDocument();
rerender(
- ,
+ ,
);
const updatedDbSelector = await screen.findAllByText(/new_db/i);
expect(updatedDbSelector[0]).toBeInTheDocument();
@@ -293,17 +330,23 @@ test('display no compatible schema found when schema api throws errors', async (
schema: undefined,
},
],
+ databases: {
+ [mockData.database.id]: {
+ ...mockData.database,
+ allow_multi_catalog: true,
+ },
+ 3: {
+ id: 3,
+ database_name: 'unauth_db',
+ backend: 'minervasql',
+ },
+ },
},
};
await renderAndWait(
{
...mockedProps,
queryEditorId: extraQueryEditor2.id,
- database: {
- id: 3,
- database_name: 'unauth_db',
- backend: 'minervasql',
- },
},
undefined,
reduxState,
@@ -331,8 +374,14 @@ test('ignore schema api when current schema is deprecated', async () => {
unsavedQueryEditor: {
id: defaultQueryEditor.id,
schema: invalidSchemaName,
+ dbId: mockData.database.id,
},
tables: [table],
+ databases: {
+ [mockData.database.id]: {
+ ...mockData.database,
+ },
+ },
},
});
@@ -340,7 +389,7 @@ test('ignore schema api when current schema is deprecated', async () => {
expect(fetchMock.calls()).not.toContainEqual(
expect.arrayContaining([
expect.stringContaining(
- `/tables/${mockedProps.database.id}/${invalidSchemaName}/`,
+ `/tables/${mockData.database.id}/${invalidSchemaName}/`,
),
]),
);
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 32232363719..5158ef7e3c9 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -48,7 +48,6 @@ import TableElement from '../TableElement';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
- database?: DatabaseObject;
}
const StyledScrollbarContainer = styled.div`
@@ -69,10 +68,11 @@ const LeftBarStyles = styled.div`
`}
`;
-const SqlEditorLeftBar = ({
- database,
- queryEditorId,
-}: SqlEditorLeftBarProps) => {
+const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
+ const databases = useSelector<
+ SqlLabRootState,
+ SqlLabRootState['sqlLab']['databases']
+ >(({ sqlLab }) => sqlLab.databases);
const allSelectedTables = useSelector(
({ sqlLab }) =>
sqlLab.tables.filter(table => table.queryEditorId === queryEditorId),
@@ -85,6 +85,10 @@ const SqlEditorLeftBar = ({
'schema',
'tabViewId',
]);
+ const database = useMemo(
+ () => (queryEditor.dbId ? databases[queryEditor.dbId] : undefined),
+ [databases, queryEditor.dbId],
+ );
const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
const [userSelectedDb, setUserSelected] = useState(
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
index 96b6b197967..242fd937be0 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
@@ -37,7 +37,6 @@ import {
REMOVE_QUERY_EDITOR,
QUERY_EDITOR_SET_TITLE,
ADD_QUERY_EDITOR,
- QUERY_EDITOR_TOGGLE_LEFT_BAR,
} from 'src/SqlLab/actions/sqlLab';
import SqlEditorTabHeader from 'src/SqlLab/components/SqlEditorTabHeader';
@@ -157,24 +156,6 @@ describe('SqlEditorTabHeader', () => {
mockPrompt.mockClear();
});
- test('should dispatch toggleLeftBar action', async () => {
- await waitFor(() =>
- expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
- );
- fireEvent.click(screen.getByTestId('toggle-menu-option'));
-
- const actions = store.getActions();
- await waitFor(() =>
- expect(actions[0]).toEqual({
- type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
- hideLeftBar: !defaultQueryEditor.hideLeftBar,
- queryEditor: expect.objectContaining({
- id: defaultQueryEditor.id,
- }),
- }),
- );
- });
-
test('should dispatch removeAllOtherQueryEditors action', async () => {
await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
index 84b46884e87..be3854553dd 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
@@ -165,24 +165,6 @@ const SqlEditorTabHeader: FC = ({ queryEditor }) => {
>
),
} as MenuItemType,
- {
- key: '3',
- onClick: () => actions.toggleLeftBar(qe),
- 'data-test': 'toggle-menu-option',
- label: (
- <>
-
-
-
- {qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
- >
- ),
- } as MenuItemType,
{
key: '4',
onClick: () => actions.removeAllOtherQueryEditors(qe),
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx
index 1022995eadf..f68e8f2eb6a 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx
@@ -16,25 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import URI from 'urijs';
-import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
-import { initialState } from 'src/SqlLab/fixtures';
+import { extraQueryEditor1, initialState } from 'src/SqlLab/fixtures';
import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
import { Store } from 'redux';
import { RootState } from 'src/views/store';
-import { SET_ACTIVE_QUERY_EDITOR } from 'src/SqlLab/actions/sqlLab';
+import { QueryEditor } from 'src/SqlLab/types';
-jest.mock('src/SqlLab/components/SqlEditor', () => () => (
-
-));
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const store = mockStore(initialState);
+jest.mock('src/SqlLab/components/SqlEditor', () =>
+ // eslint-disable-next-line react/display-name
+ ({ queryEditor }: { queryEditor: QueryEditor }) => (
+ {queryEditor.id}
+ ),
+);
const setup = (overridesStore?: Store, initialState?: RootState) =>
render( , {
@@ -42,106 +43,13 @@ const setup = (overridesStore?: Store, initialState?: RootState) =>
initialState,
...(overridesStore && { store: overridesStore }),
});
-let pathStub = jest.spyOn(URI.prototype, 'path');
beforeEach(() => {
fetchMock.get('glob:*/api/v1/database/*', {});
- fetchMock.get('glob:*/api/v1/saved_query/*', {});
- pathStub = jest.spyOn(URI.prototype, 'path').mockReturnValue(`/sqllab/`);
- store.clearActions();
});
afterEach(() => {
fetchMock.reset();
- pathStub.mockReset();
-});
-
-// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
-describe('componentDidMount', () => {
- let uriStub = jest.spyOn(URI.prototype, 'search');
- let replaceState = jest.spyOn(window.history, 'replaceState');
- beforeEach(() => {
- replaceState = jest.spyOn(window.history, 'replaceState');
- uriStub = jest.spyOn(URI.prototype, 'search');
- });
- afterEach(() => {
- replaceState.mockReset();
- uriStub.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,
- });
- uriStub.mockReturnValue({ id: 1 });
- setup(store);
- expect(replaceState).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- '/sqllab',
- );
- await waitFor(() =>
- expect(
- fetchMock.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`),
- ).toHaveLength(1),
- );
- 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,
- });
- pathStub.mockReturnValue(`/sqllab/p/${key}`);
- setup(store);
- 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', () => {
- uriStub.mockReturnValue({ savedQueryId: 1 });
- setup(store);
- expect(replaceState).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- '/sqllab',
- );
- });
- test('should handle sql', () => {
- uriStub.mockReturnValue({ sql: 1, dbid: 1 });
- setup(store);
- expect(replaceState).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- '/sqllab',
- );
- });
- test('should handle custom url params', () => {
- uriStub.mockReturnValue({
- sql: 1,
- dbid: 1,
- custom_value: 'str',
- extra_attr1: 'true',
- });
- setup(store);
- expect(replaceState).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- '/sqllab?custom_value=str&extra_attr1=true',
- );
- });
});
test('should removeQueryEditor', async () => {
@@ -169,6 +77,7 @@ test('should removeQueryEditor', async () => {
queryByText(initialState.sqlLab.queryEditors[0].name),
).not.toBeInTheDocument();
});
+
test('should add new query editor', async () => {
const { getAllByLabelText, getAllByRole } = setup(undefined, initialState);
const tabCount = getAllByRole('tab').filter(
@@ -188,6 +97,7 @@ test('should add new query editor', async () => {
)[tabCount],
).toHaveTextContent(/Untitled Query (\d+)+/);
});
+
test('should properly increment query tab name', async () => {
const { getAllByLabelText, getAllByRole } = setup(undefined, initialState);
const tabCount = getAllByRole('tab').filter(
@@ -208,27 +118,25 @@ test('should properly increment query tab name', async () => {
)[tabCount],
).toHaveTextContent(newTitle);
});
+
test('should handle select', async () => {
- const { getAllByRole } = setup(store);
+ const { getAllByRole } = setup(undefined, initialState);
const tabs = getAllByRole('tab').filter(
tab => !tab.classList.contains('ant-tabs-tab-remove'),
);
fireEvent.click(tabs[1]);
- await waitFor(() => expect(store.getActions()).toHaveLength(1));
- expect(store.getActions()[0]).toEqual(
- expect.objectContaining({
- type: SET_ACTIVE_QUERY_EDITOR,
- queryEditor: initialState.sqlLab.queryEditors[1],
- }),
- );
+ await screen.findByText(extraQueryEditor1.id);
+ expect(screen.getByText(extraQueryEditor1.id)).toBeInTheDocument();
});
+
test('should render', () => {
- const { getAllByRole } = setup(store);
+ const { getAllByRole } = setup(undefined, initialState);
const tabs = getAllByRole('tab').filter(
tab => !tab.classList.contains('ant-tabs-tab-remove'),
);
expect(tabs).toHaveLength(initialState.sqlLab.queryEditors.length);
});
+
test('should disable new tab when offline', () => {
const { queryAllByLabelText } = setup(undefined, {
...initialState,
@@ -239,12 +147,19 @@ test('should disable new tab when offline', () => {
});
expect(queryAllByLabelText('Add tab').length).toEqual(0);
});
+
test('should have an empty state when query editors is empty', async () => {
const { getByText, getByRole } = setup(undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
- queryEditors: [],
+ queryEditors: [
+ {
+ id: 1,
+ name: 'Untitled Query 1',
+ sql: '',
+ },
+ ],
tabHistory: [],
},
});
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
index 9fa47a93fd0..53ed449784c 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
@@ -17,10 +17,8 @@
* under the License.
*/
import { PureComponent } from 'react';
-import { pick } from 'lodash';
import { EditableTabs } from '@superset-ui/core/components/Tabs';
import { connect } from 'react-redux';
-import URI from 'urijs';
import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { FeatureFlag, t, isFeatureEnabled } from '@superset-ui/core';
import { styled, css } from '@apache-superset/core/ui';
@@ -28,9 +26,6 @@ import { Logger } from 'src/logger/LogUtils';
import { EmptyState, Tooltip } from '@superset-ui/core/components';
import { detectOS } from 'src/utils/common';
import * as Actions from 'src/SqlLab/actions/sqlLab';
-import getBootstrapData from 'src/utils/getBootstrapData';
-import { locationContext } from 'src/pages/SqlLab/LocationContext';
-import { navigateWithState } from 'src/utils/navigationUtils';
import { Icons } from '@superset-ui/core/components/Icons';
import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader';
@@ -62,8 +57,6 @@ const userOS = detectOS();
type TabbedSqlEditorsProps = ReturnType;
-const SQL_LAB_URL = '/sqllab';
-
class TabbedSqlEditors extends PureComponent {
constructor(props: TabbedSqlEditorsProps) {
super(props);
@@ -73,103 +66,21 @@ class TabbedSqlEditors extends PureComponent {
}
componentDidMount() {
- // merge post form data with GET search params
- // Hack: this data should be coming from getInitialState
- // but for some reason this data isn't being passed properly through
- // the reducer.
- const bootstrapData = getBootstrapData();
- const queryParameters = URI(window.location).search(true);
- const path = URI(window.location).path();
- const {
- id,
- name,
- sql,
- savedQueryId,
- datasourceKey,
- queryId,
- dbid,
- dbname,
- catalog,
- schema,
- autorun,
- new: isNewQuery,
- ...urlParams
- } = {
- ...this.context.requestedQuery,
- ...bootstrapData.requested_query,
- ...queryParameters,
- } as Record;
- const permalink = path.match(/\/p\/\w+/)?.[0].slice(3);
-
- // Popping a new tab based on the querystring
- if (permalink || id || sql || savedQueryId || datasourceKey || queryId) {
- if (permalink) {
- this.props.actions.popPermalink(permalink);
- } else if (id) {
- this.props.actions.popStoredQuery(id);
- } else if (savedQueryId) {
- this.props.actions.popSavedQuery(savedQueryId);
- } else if (queryId) {
- this.props.actions.popQuery(queryId);
- } else if (datasourceKey) {
- this.props.actions.popDatasourceQuery(datasourceKey, sql);
- } else if (sql) {
- let databaseId: string | number = dbid;
- if (databaseId) {
- databaseId = parseInt(databaseId, 10);
- } else {
- const { databases } = this.props;
- const databaseName = dbname;
- if (databaseName) {
- Object.keys(databases).forEach(db => {
- if (databases[db].database_name === databaseName) {
- databaseId = databases[db].id;
- }
- });
- }
- }
- const newQueryEditor = {
- name,
- dbId: databaseId,
- catalog,
- schema,
- autorun,
- sql,
- isDataset: this.context.isDataset,
- };
- this.props.actions.addQueryEditor(newQueryEditor);
- }
- this.popNewTab(pick(urlParams, Object.keys(queryParameters ?? {})));
- } else if (isNewQuery || this.props.queryEditors.length === 0) {
- this.newQueryEditor();
-
- if (isNewQuery) {
- navigateWithState(SQL_LAB_URL, {}, { replace: true });
- }
- } else {
- const qe = this.activeQueryEditor();
- const latestQuery = this.props.queries[qe?.latestQueryId || ''];
- if (
- isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
- latestQuery &&
- latestQuery.resultsKey
- ) {
- // when results are not stored in localStorage they need to be
- // fetched from the results backend (if configured)
- this.props.actions.fetchQueryResults(
- latestQuery,
- this.props.displayLimit,
- );
- }
+ const qe = this.activeQueryEditor();
+ const latestQuery = this.props.queries[qe?.latestQueryId || ''];
+ if (
+ isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
+ latestQuery?.resultsKey
+ ) {
+ // when results are not stored in localStorage they need to be
+ // fetched from the results backend (if configured)
+ this.props.actions.fetchQueryResults(
+ latestQuery,
+ this.props.displayLimit,
+ );
}
}
- popNewTab(urlParams: Record) {
- // Clean the url in browser history
- const updatedUrl = `${URI(SQL_LAB_URL).query(urlParams)}`;
- navigateWithState(updatedUrl, {}, { replace: true });
- }
-
activeQueryEditor() {
if (this.props.tabHistory.length === 0) {
return this.props.queryEditors[0];
@@ -308,11 +219,8 @@ class TabbedSqlEditors extends PureComponent {
}
}
-TabbedSqlEditors.contextType = locationContext;
-
export function mapStateToProps({ sqlLab, common }: SqlLabRootState) {
return {
- databases: sqlLab.databases,
queryEditors: sqlLab.queryEditors ?? DEFAULT_PROPS.queryEditors,
queries: sqlLab.queries,
tabHistory: sqlLab.tabHistory,
diff --git a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
index d52b1406907..5ac6b7c13b1 100644
--- a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
@@ -72,8 +72,6 @@ const Title = styled.div`
column-gap: ${theme.sizeUnit}px;
font-size: ${theme.fontSizeLG}px;
font-weight: ${theme.fontWeightStrong};
- padding-top: ${theme.sizeUnit * 2}px;
- padding-left: ${theme.sizeUnit * 4}px;
`}
`;
const renderWell = (partitions: TableMetaData['partitions']) => {
@@ -282,12 +280,7 @@ const TablePreview: FC = ({ dbId, catalog, schema, tableName }) => {
flex-direction: column;
`}
>
-
+
{backend}
{databaseName}
{catalog && {catalog} }
@@ -421,9 +414,6 @@ const TablePreview: FC = ({ dbId, catalog, schema, tableName }) => {
`}
tabBarStyle={{ paddingLeft: theme.sizeUnit * 4 }}
items={tabItems}
- contentStyle={css`
- padding-left: ${theme.sizeUnit * 4}px;
- `}
/>
);
}}
diff --git a/superset-frontend/src/SqlLab/constants.ts b/superset-frontend/src/SqlLab/constants.ts
index 97667325d2b..4422e4eac5e 100644
--- a/superset-frontend/src/SqlLab/constants.ts
+++ b/superset-frontend/src/SqlLab/constants.ts
@@ -68,6 +68,7 @@ export const TIME_OPTIONS = [
// SqlEditor layout constants
export const SQL_EDITOR_GUTTER_HEIGHT = 4;
export const SQL_EDITOR_LEFTBAR_WIDTH = 400;
+export const SQL_EDITOR_RIGHTBAR_WIDTH = 400;
export const INITIAL_NORTH_PERCENT = 30;
export const SET_QUERY_EDITOR_SQL_DEBOUNCE_MS = 2000;
export const VALIDATION_DEBOUNCE_MS = 600;
diff --git a/superset-frontend/src/SqlLab/contributions.ts b/superset-frontend/src/SqlLab/contributions.ts
new file mode 100644
index 00000000000..70f00f4d070
--- /dev/null
+++ b/superset-frontend/src/SqlLab/contributions.ts
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+export enum ViewContribution {
+ RightSidebar = 'sqllab.rightSidebar',
+ SouthPanels = 'sqllab.panels',
+}
diff --git a/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts b/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts
index 237e5fc23fe..7a5f3c6874f 100644
--- a/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts
+++ b/superset-frontend/src/SqlLab/hooks/useQueryEditor/index.ts
@@ -30,7 +30,10 @@ export default function useQueryEditor(
shallowEqual,
);
const queryEditorsById = useMemo(
- () => Object.fromEntries(queryEditors.map(editor => [editor.id, editor])),
+ () =>
+ Object.fromEntries(
+ queryEditors.map((editor, index) => [editor.id, index]),
+ ),
[queryEditors.map(({ id }) => id).join(',')],
);
@@ -38,7 +41,7 @@ export default function useQueryEditor(
({ sqlLab: { unsavedQueryEditor } }) =>
pick(
{
- ...queryEditorsById[sqlEditorId],
+ ...queryEditors[queryEditorsById[sqlEditorId]],
...(sqlEditorId === unsavedQueryEditor?.id && unsavedQueryEditor),
},
['id'].concat(attributes),
diff --git a/superset-frontend/src/pages/SqlLab/LocationContext.tsx b/superset-frontend/src/pages/SqlLab/LocationContext.tsx
index c972f0cbcd0..6574cd664c2 100644
--- a/superset-frontend/src/pages/SqlLab/LocationContext.tsx
+++ b/superset-frontend/src/pages/SqlLab/LocationContext.tsx
@@ -37,15 +37,15 @@ export const LocationProvider: FC = ({ children }: { children: ReactNode }) => {
return {children} ;
}
const queryParams = new URLSearchParams(location.search);
- if (queryParams.size > 0) {
- const dbid = queryParams.get('dbid');
- const sql = queryParams.get('sql');
- const name = queryParams.get('name');
- const schema = queryParams.get('schema');
+ const permalink = location.pathname.match(/\/p\/\w+/)?.[0].slice(3);
+ if (queryParams.size > 0 || permalink) {
const autorun = queryParams.get('autorun') === 'true';
-
const queryParamsState = {
- requestedQuery: { dbid, sql, name, schema, autorun },
+ requestedQuery: {
+ ...Object.fromEntries(queryParams),
+ autorun,
+ permalink,
+ },
isDataset: true,
} as LocationState;
return {children} ;