mirror of
https://github.com/apache/superset.git
synced 2026-04-23 01:55:09 +00:00
refactor(sqllab): Separate left panel outside of tab container (#36360)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
138
superset-frontend/src/SqlLab/components/AppLayout/index.tsx
Normal file
138
superset-frontend/src/SqlLab/components/AppLayout/index.tsx
Normal 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;
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
122
superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx
Normal file
122
superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}/`,
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user