refactor(sqllab): Separate left panel outside of tab container (#36360)

This commit is contained in:
JUST.in DO IT
2025-12-08 11:44:31 -08:00
committed by GitHub
parent 8a00badf45
commit 8d04c33adf
22 changed files with 819 additions and 463 deletions

View File

@@ -32,9 +32,11 @@ import {
LOG_ACTIONS_SQLLAB_MONITOR_LOCAL_STORAGE_USAGE,
} from 'src/logger/LogUtils';
jest.mock('src/SqlLab/components/TabbedSqlEditors', () => () => (
<div data-test="mock-tabbed-sql-editors" />
// eslint-disable-next-line react/display-name
jest.mock('src/SqlLab/components/PopEditorTab', () => () => (
<div data-test="mock-pop-editor-tab" />
));
// eslint-disable-next-line react/display-name
jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
<div data-test="mock-query-auto-refresh" />
));
@@ -68,7 +70,7 @@ describe('SqlLab App', () => {
test('should render', () => {
const { getByTestId } = render(<App />, { 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', () => {

View File

@@ -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<AppProps, AppState> {
queries={queries}
queriesLastUpdate={queriesLastUpdate}
/>
<TabbedSqlEditors />
<PopEditorTab>
<AppLayout>
<TabbedSqlEditors />
</AppLayout>
</PopEditorTab>
</SqlLabStyles>
);
}

View File

@@ -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;
}) => (
<div>
{children}
<button type="button" onClick={() => onResizeEnd([500])}>
Resize
</button>
<button type="button" onClick={() => onResizeEnd([0])}>
Resize to zero
</button>
</div>
);
// eslint-disable-next-line react/display-name
Splitter.Panel = ({ children }: { children: React.ReactNode }) => (
<div data-test="mock-panel">{children}</div>
);
return { Splitter };
});
jest.mock('@superset-ui/core/components/Grid', () => ({
...jest.requireActual('@superset-ui/core/components/Grid'),
useBreakpoint: jest.fn().mockReturnValue(true),
}));
const defaultProps = {
children: <div>Child</div>,
};
function createMockView(
id: string,
overrides: Partial<contributions.ViewContribution> = {},
): 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<core.Extension> = {},
): Promise<core.Extension> {
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(<AppLayout {...defaultProps} />, {
useRedux: true,
initialState,
});
expect(getAllByTestId('mock-panel')).toHaveLength(2);
});
test('renders children', () => {
const { getByText } = render(<AppLayout {...defaultProps} />, {
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(<AppLayout {...defaultProps} />, {
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(
<AppLayout {...defaultProps} />,
{
useRedux: true,
initialState,
},
);
expect(getByText('Child')).toBeInTheDocument();
expect(getAllByTestId('mock-panel')).toHaveLength(3);
});

View File

@@ -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<SqlLabRootState, string>(
({ 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 (
<StyledContainer>
<Splitter lazy onResizeEnd={onSidebarChange} onResize={noop}>
<Splitter.Panel
collapsible={{
start: true,
end: true,
showCollapsibleIcon: true,
}}
size={leftWidth}
min={SQL_EDITOR_LEFTBAR_WIDTH}
>
<StyledSidebar>
<SqlEditorLeftBar
key={queryEditorId}
queryEditorId={queryEditorId}
/>
</StyledSidebar>
</Splitter.Panel>
<Splitter.Panel className="sqllab-body">{children}</Splitter.Panel>
{contributions.length > 0 && (
<Splitter.Panel
collapsible={{
start: true,
end: true,
showCollapsibleIcon: true,
}}
size={rightWidth}
min={SQL_EDITOR_RIGHTBAR_WIDTH}
>
<ContentWrapper>
{contributions.map(contribution => getView(contribution.id))}
</ContentWrapper>
</Splitter.Panel>
)}
</Splitter>
</StyledContainer>
);
};
export default AppLayout;

View File

@@ -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(
<MemoryRouter initialEntries={[url]}>
<LocationProvider>
<PopEditorTab />
</LocationProvider>
</MemoryRouter>,
{
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',
);
});

View File

@@ -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<string>();
const { requestedQuery } = useLocationState();
const activeQueryEditorId = useSelector<SqlLabRootState, string>(
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);
const [updatedUrl, setUpdatedUrl] = useState<string>(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 <Skeleton active />;
}
return <>{children}</>;
};
export default PopEditorTab;

View File

@@ -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 ? (
<div
css={css`
padding-left: ${theme.sizeUnit * 4}px;
`}
>
<>
<QueryTable
columns={[
'state',
@@ -148,7 +143,7 @@ const QueryHistory = ({
/>
)}
{isFetching && <Skeleton active />}
</div>
</>
) : (
<StyledEmptyStateWrapper>
<EmptyState

View File

@@ -148,7 +148,6 @@ const ReturnedRows = styled.div`
const ResultSetControls = styled.div`
display: flex;
justify-content: space-between;
padding-left: ${({ theme }) => 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 = ({
<div
css={css`
flex: 1 1 auto;
padding-left: ${theme.sizeUnit * 4}px;
`}
>
<AutoSizer disableWidth>

View File

@@ -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) => ({

View File

@@ -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(() => (
<div data-test="mock-sql-editor-left-bar" />
));
(ResultSet as unknown as jest.Mock).mockClear();
(ResultSet as unknown as jest.Mock).mockImplementation(() => (
<div data-test="mock-result-set" />
@@ -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);
});

View File

@@ -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<Props> = ({
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<Props> = ({
[database],
);
const sqlEditorRef = useRef<HTMLDivElement>(null);
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
const startQuery = useCallback(
@@ -751,6 +736,7 @@ const SqlEditor: FC<Props> = ({
return (
<Button
key={contribution.view}
onClick={() => commands.executeCommand(command.command)}
tooltip={command?.description}
icon={<Icon iconSize="m" iconColor={theme.colorPrimary} />}
@@ -1012,68 +998,31 @@ const SqlEditor: FC<Props> = ({
? 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 (
<StyledSqlEditor ref={sqlEditorRef} className="SqlEditor">
<Splitter lazy onResizeEnd={onSidebarChange} onResize={noop}>
<Splitter.Panel
collapsible
size={hideLeftBar ? 0 : width}
min={SQL_EDITOR_LEFTBAR_WIDTH}
<StyledSqlEditor className="SqlEditor">
{shouldLoadQueryEditor ? (
<div
data-test="sqlEditor-loading"
css={css`
flex: 1;
padding: ${theme.sizeUnit * 4}px;
`}
>
<StyledSidebar>
<SqlEditorLeftBar
database={database}
queryEditorId={queryEditor.id}
/>
</StyledSidebar>
</Splitter.Panel>
<Splitter.Panel>
{shouldLoadQueryEditor ? (
<div
data-test="sqlEditor-loading"
css={css`
flex: 1;
padding: ${theme.sizeUnit * 4}px;
`}
>
<Skeleton active />
</div>
) : showEmptyState && !hasSqlStatement ? (
<EmptyState
image="vector.svg"
size="large"
title={t('Select a database to write a query')}
description={t(
'Choose one of the available databases from the panel on the left.',
)}
/>
) : (
queryPane()
<Skeleton active />
</div>
) : showEmptyState && !hasSqlStatement ? (
<EmptyState
image="vector.svg"
size="large"
title={t('Select a database to write a query')}
description={t(
'Choose one of the available databases from the panel on the left.',
)}
</Splitter.Panel>
</Splitter>
/>
) : (
queryPane()
)}
<Modal
show={showCreateAsModal}
name={t(createViewModalTitle)}

View File

@@ -39,12 +39,14 @@ import type { Store } from 'redux';
const mockedProps = {
queryEditorId: defaultQueryEditor.id,
height: 0,
};
const mockData = {
database: {
id: 1,
database_name: 'main',
backend: 'mysql',
},
height: 0,
};
beforeEach(() => {
@@ -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(
<SqlEditorLeftBar
{...mockedProps}
database={{
id: 2,
database_name: 'new_db',
backend: 'postgresql',
}}
queryEditorId={extraQueryEditor1.id}
/>,
<SqlEditorLeftBar {...mockedProps} queryEditorId={extraQueryEditor1.id} />,
);
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}/`,
),
]),
);

View File

@@ -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<SqlLabRootState, Table[]>(
({ 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<DatabaseObject | null>(

View File

@@ -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(),

View File

@@ -165,24 +165,6 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
</>
),
} as MenuItemType,
{
key: '3',
onClick: () => actions.toggleLeftBar(qe),
'data-test': 'toggle-menu-option',
label: (
<>
<IconContainer>
<Icons.VerticalAlignBottomOutlined
iconSize="l"
css={css`
rotate: ${qe.hideLeftBar ? '-90deg;' : '90deg;'};
`}
/>
</IconContainer>
{qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
</>
),
} as MenuItemType,
{
key: '4',
onClick: () => actions.removeAllOtherQueryEditors(qe),

View File

@@ -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', () => () => (
<div data-test="mock-sql-editor" />
));
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 }) => (
<div data-test="mock-sql-editor">{queryEditor.id}</div>
),
);
const setup = (overridesStore?: Store, initialState?: RootState) =>
render(<TabbedSqlEditors />, {
@@ -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: [],
},
});

View File

@@ -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<typeof mergeProps>;
const SQL_LAB_URL = '/sqllab';
class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
constructor(props: TabbedSqlEditorsProps) {
super(props);
@@ -73,103 +66,21 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
}
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<string, string>;
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<string, string>) {
// 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<TabbedSqlEditorsProps> {
}
}
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,

View File

@@ -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<Props> = ({ dbId, catalog, schema, tableName }) => {
flex-direction: column;
`}
>
<Breadcrumb
separator=">"
css={css`
padding-left: ${theme.sizeUnit * 4}px;
`}
>
<Breadcrumb separator=">">
<Breadcrumb.Item>{backend}</Breadcrumb.Item>
<Breadcrumb.Item>{databaseName}</Breadcrumb.Item>
{catalog && <Breadcrumb.Item>{catalog}</Breadcrumb.Item>}
@@ -421,9 +414,6 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
`}
tabBarStyle={{ paddingLeft: theme.sizeUnit * 4 }}
items={tabItems}
contentStyle={css`
padding-left: ${theme.sizeUnit * 4}px;
`}
/>
);
}}