feat(sqllab): introduce splitter for adjusting sidebar and query panel (#34767)

This commit is contained in:
JUST.in DO IT
2025-08-21 12:47:25 -07:00
committed by GitHub
parent c568d463b9
commit 63bb1d55a4
9 changed files with 155 additions and 200 deletions

View File

@@ -302,7 +302,6 @@ const QueryTable = ({
<ResultSet
showSql
queryId={query.id}
height={400}
displayLimit={displayLimit}
defaultQueryLimit={1000}
/>

View File

@@ -104,7 +104,6 @@ export interface ResultSetProps {
csv?: boolean;
database?: Record<string, any>;
displayLimit: number;
height: number;
queryId: string;
search?: boolean;
showSql?: boolean;
@@ -176,7 +175,6 @@ const ResultSet = ({
csv = true,
database = {},
displayLimit,
height,
queryId,
search = true,
showSql = false,

View File

@@ -25,11 +25,8 @@ import { SqlLabRootState } from 'src/SqlLab/types';
import ResultSet from '../ResultSet';
import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../constants';
const EXTRA_HEIGHT_RESULTS = 8; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
type Props = {
latestQueryId?: string;
height: number;
displayLimit: number;
defaultQueryLimit: number;
};
@@ -47,7 +44,6 @@ const StyledEmptyStateWrapper = styled.div`
const Results: FC<Props> = ({
latestQueryId,
height,
displayLimit,
defaultQueryLimit,
}) => {
@@ -92,7 +88,6 @@ const Results: FC<Props> = ({
<ResultSet
search
queryId={latestQuery.id}
height={height + EXTRA_HEIGHT_RESULTS}
database={databases[latestQuery.dbId]}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}

View File

@@ -37,8 +37,6 @@ import {
import Results from './Results';
import TablePreview from '../TablePreview';
const TAB_HEIGHT = 130;
/*
editorQueries are queries executed by users passed from SqlEditor component
dataPreviewQueries are all queries executed for preview of table data (from SqlEditorLeft)
@@ -46,23 +44,18 @@ const TAB_HEIGHT = 130;
export interface SouthPaneProps {
queryEditorId: string;
latestQueryId?: string;
height: number;
displayLimit: number;
defaultQueryLimit: number;
}
type StyledPaneProps = {
height: number;
};
const TABS_KEYS = {
RESULTS: 'Results',
HISTORY: 'History',
};
const StyledPane = styled.div<StyledPaneProps>`
const StyledPane = styled.div`
width: 100%;
height: ${props => props.height}px;
height: 100%;
.ant-tabs .ant-tabs-content-holder {
overflow: visible;
}
@@ -93,7 +86,6 @@ const StyledPane = styled.div<StyledPaneProps>`
const SouthPane = ({
queryEditorId,
latestQueryId,
height,
displayLimit,
defaultQueryLimit,
}: SouthPaneProps) => {
@@ -127,7 +119,6 @@ const SouthPane = ({
),
[pinnedTables],
);
const innerTabContentHeight = height - TAB_HEIGHT;
const southPaneRef = createRef<HTMLDivElement>();
const switchTab = (id: string) => {
dispatch(setActiveSouthPaneTab(id));
@@ -159,7 +150,6 @@ const SouthPane = ({
label: t('Results'),
children: (
<Results
height={innerTabContentHeight}
latestQueryId={latestQueryId}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
@@ -205,12 +195,7 @@ const SouthPane = ({
];
return (
<StyledPane
data-test="south-pane"
className="SouthPane"
height={height}
ref={southPaneRef}
>
<StyledPane data-test="south-pane" className="SouthPane" ref={southPaneRef}>
<Tabs
type="editable-card"
activeKey={pinnedTableKeys[activeSouthPaneTab] || activeSouthPaneTab}

View File

@@ -45,6 +45,16 @@ import setupExtensions from 'src/setup/setupExtensions';
import type { Action, Middleware, Store } from 'redux';
import SqlEditor, { Props } from '.';
jest.mock(
'react-virtualized-auto-sizer',
() =>
({
children,
}: {
children: (params: { height: number }) => React.ReactChild;
}) =>
children({ height: 500 }),
);
jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
...jest.requireActual('@superset-ui/core/components/AsyncAceEditor'),
FullSQLEditor: ({

View File

@@ -30,9 +30,8 @@ import {
import type AceEditor from 'react-ace';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { CSSTransition } from 'react-transition-group';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Split from 'react-split';
import AutoSizer from 'react-virtualized-auto-sizer';
import {
css,
FeatureFlag,
@@ -50,7 +49,7 @@ import type {
CursorPosition,
} from 'src/SqlLab/types';
import type { DatabaseObject } from 'src/features/databases/types';
import { debounce, isEmpty } from 'lodash';
import { debounce, isEmpty, noop } from 'lodash';
import Mousetrap from 'mousetrap';
import {
Alert,
@@ -61,7 +60,8 @@ import {
Modal,
Timer,
} from '@superset-ui/core/components';
import ResizableSidebar from 'src/components/ResizableSidebar';
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';
import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
@@ -87,16 +87,13 @@ import {
formatQuery,
fetchQueryEditor,
switchQueryEditor,
toggleLeftBar,
} from 'src/SqlLab/actions/sqlLab';
import {
STATE_TYPE_MAP,
SQL_EDITOR_GUTTER_HEIGHT,
SQL_EDITOR_GUTTER_MARGIN,
SQL_TOOLBAR_HEIGHT,
SQL_EDITOR_LEFTBAR_WIDTH,
SQL_EDITOR_PADDING,
INITIAL_NORTH_PERCENT,
INITIAL_SOUTH_PERCENT,
SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
} from 'src/SqlLab/constants';
import {
@@ -167,12 +164,8 @@ const StyledToolbar = styled.div`
}
`;
const StyledSidebar = styled.div<{ width: number; hide: boolean | undefined }>`
flex: 0 0 ${({ width }) => width}px;
width: ${({ width }) => width}px;
padding: ${({ theme, hide }) => (hide ? 0 : theme.sizeUnit * 2.5)}px;
border-right: 1px solid
${({ theme, hide }) => (hide ? 'transparent' : theme.colorBorder)};
const StyledSidebar = styled.div`
padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
`;
const StyledSqlEditor = styled.div`
@@ -186,47 +179,26 @@ const StyledSqlEditor = styled.div`
}
.queryPane {
flex: 1 1 auto;
padding: ${theme.sizeUnit * 2}px;
overflow-y: auto;
overflow-x: scroll;
+ .ant-splitter-bar .ant-splitter-bar-dragger {
&::before {
background: transparent;
}
&::after {
height: ${SQL_EDITOR_GUTTER_HEIGHT}px;
background: transparent;
border-top: 1px solid ${theme.colorBorder};
border-bottom: 1px solid ${theme.colorBorder};
}
}
}
.schemaPane-enter-done,
.schemaPane-exit {
transform: translateX(0);
z-index: 7;
.north-pane {
height: 100%;
}
.schemaPane-exit-active {
transform: translateX(-120%);
}
.schemaPane-enter-active {
transform: translateX(0);
max-width: ${theme.sizeUnit * 75}px;
}
.schemaPane-enter,
.schemaPane-exit-done {
max-width: 0;
transform: translateX(-120%);
overflow: hidden;
}
.schemaPane-exit-done + .queryPane {
margin-left: 0;
}
.gutter {
border-top: 1px solid ${theme.colorBorder};
border-bottom: 1px solid ${theme.colorBorder};
width: 3%;
margin: ${SQL_EDITOR_GUTTER_MARGIN}px 47%;
}
.gutter.gutter-vertical {
cursor: row-resize;
.sql-container {
flex: 1 1 auto;
}
`}
`;
@@ -242,16 +214,6 @@ export type Props = {
scheduleQueryWarning: string | null;
};
const elementStyle = (
dimension: string,
elementSize: number,
gutterSize: number,
) => ({
[dimension]: `calc(${elementSize}% - ${
gutterSize + SQL_EDITOR_GUTTER_MARGIN
}px)`,
});
const SqlEditor: FC<Props> = ({
queryEditor,
defaultQueryLimit,
@@ -304,9 +266,6 @@ const SqlEditor: FC<Props> = ({
const [northPercent, setNorthPercent] = useState(
queryEditor.northPercent || INITIAL_NORTH_PERCENT,
);
const [southPercent, setSouthPercent] = useState(
queryEditor.southPercent || INITIAL_SOUTH_PERCENT,
);
const [autocompleteEnabled, setAutocompleteEnabled] = useState(
getItem(LocalStorageKeys.SqllabIsAutocompleteEnabled, true),
);
@@ -322,7 +281,6 @@ const SqlEditor: FC<Props> = ({
);
const sqlEditorRef = useRef<HTMLDivElement>(null);
const northPaneRef = useRef<HTMLDivElement>(null);
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
@@ -380,12 +338,6 @@ const SqlEditor: FC<Props> = ({
}
}, [autorun, dispatch, queryEditor, startQuery]);
// One layer of abstraction for easy spying in unit tests
const getSqlEditorHeight = () =>
sqlEditorRef.current
? sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2
: 0;
const getHotkeyConfig = useCallback(() => {
// Get the user's OS
const userOS = detectOS();
@@ -618,13 +570,12 @@ const SqlEditor: FC<Props> = ({
}
};
const onResizeEnd = ([northPercent, southPercent]: number[]) => {
setNorthPercent(northPercent);
setSouthPercent(southPercent);
const onResizeEnd = ([nHeight, sHeight]: number[]) => {
const northPercent = Math.round((nHeight * 100) / (nHeight + sHeight));
const southPercent = 100 - northPercent;
if (northPaneRef.current?.clientHeight) {
dispatch(persistEditorHeight(queryEditor, northPercent, southPercent));
}
setNorthPercent(northPercent);
dispatch(persistEditorHeight(queryEditor, northPercent, southPercent));
};
const setQueryEditorAndSaveSql = useCallback(
@@ -644,22 +595,6 @@ const SqlEditor: FC<Props> = ({
dispatch(queryEditorSetSql(queryEditor, sql));
});
// Return the heights for the ace editor and the south pane as an object
// given the height of the sql editor, north pane percent and south pane percent.
const getAceEditorAndSouthPaneHeights = (
height: number,
northPercent: number,
southPercent: number,
) => ({
aceEditorHeight:
(height * northPercent) / (theme.sizeUnit * 25) -
(SQL_EDITOR_GUTTER_HEIGHT / 2 + SQL_EDITOR_GUTTER_MARGIN) -
SQL_TOOLBAR_HEIGHT,
southPaneHeight:
(height * southPercent) / (theme.sizeUnit * 25) -
(SQL_EDITOR_GUTTER_HEIGHT / 2 + SQL_EDITOR_GUTTER_MARGIN),
});
const getQueryCostEstimate = () => {
logAction(LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST, { shortcut: false });
if (database) {
@@ -946,6 +881,7 @@ const SqlEditor: FC<Props> = ({
font-size: ${theme.fontSize}px;
font-weight: ${theme.fontWeightStrong};
color: ${theme.colorPrimaryText};
margin: 0px;
`}
>
{' '}
@@ -957,6 +893,7 @@ const SqlEditor: FC<Props> = ({
font-size: ${theme.fontSize}px;
font-weight: ${theme.fontWeightStrong};
color: ${theme.colorPrimaryText};
margin: 0px;
`}
>
{t(
@@ -970,23 +907,18 @@ const SqlEditor: FC<Props> = ({
/>
);
const queryPane = () => {
const height = getSqlEditorHeight();
const { aceEditorHeight, southPaneHeight } =
getAceEditorAndSouthPaneHeights(height, northPercent, southPercent);
return (
<Split
expandToMin
const queryPane = () => (
<Splitter
layout="vertical"
onResizeStart={onResizeStart}
onResizeEnd={onResizeEnd}
>
<Splitter.Panel
min={queryEditor.isDataset ? 400 : 200}
defaultSize={`${northPercent}%`}
className="queryPane"
sizes={[northPercent, southPercent]}
elementStyle={elementStyle}
minSize={queryEditor.isDataset ? 400 : 200}
direction="vertical"
gutterSize={SQL_EDITOR_GUTTER_HEIGHT}
onDragStart={onResizeStart}
onDragEnd={onResizeEnd}
>
<div ref={northPaneRef} className="north-pane">
<div className="north-pane">
{SqlFormExtension && (
<SqlFormExtension
queryEditorId={queryEditor.id}
@@ -997,29 +929,38 @@ const SqlEditor: FC<Props> = ({
/>
)}
{queryEditor.isDataset && renderDatasetWarning()}
{isActive && (
<AceEditorWrapper
autocomplete={autocompleteEnabled && !isTempId(queryEditor.id)}
onBlur={onSqlChanged}
onChange={onSqlChanged}
queryEditorId={queryEditor.id}
onCursorPositionChange={handleCursorPositionChange}
height={`${aceEditorHeight}px`}
hotkeys={hotkeys}
/>
)}
<div className="sql-container">
<AutoSizer disableWidth>
{({ height }) =>
isActive && (
<AceEditorWrapper
autocomplete={
autocompleteEnabled && !isTempId(queryEditor.id)
}
onBlur={onSqlChanged}
onChange={onSqlChanged}
queryEditorId={queryEditor.id}
onCursorPositionChange={handleCursorPositionChange}
height={`${height}px`}
hotkeys={hotkeys}
/>
)
}
</AutoSizer>
</div>
{renderEditorBottomBar(showEmptyState)}
</div>
</Splitter.Panel>
<Splitter.Panel className="queryPane">
<SouthPane
queryEditorId={queryEditor.id}
latestQueryId={latestQuery?.id}
height={southPaneHeight}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
/>
</Split>
);
};
</Splitter.Panel>
</Splitter>
);
const createViewModalTitle =
createAs === CtasEnum.View ? 'CREATE VIEW AS' : 'CREATE TABLE AS';
@@ -1029,54 +970,68 @@ 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 leftBarStateClass = hideLeftBar
? 'schemaPane-exit-done'
: 'schemaPane-enter-done';
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">
<CSSTransition classNames="schemaPane" in={!hideLeftBar} timeout={300}>
<ResizableSidebar
id={`sqllab:${queryEditor.id}`}
minWidth={SQL_EDITOR_LEFTBAR_WIDTH}
initialWidth={SQL_EDITOR_LEFTBAR_WIDTH}
enable={!hideLeftBar}
<Splitter lazy onResizeEnd={onSidebarChange} onResize={noop}>
<Splitter.Panel
collapsible
size={hideLeftBar ? 0 : width}
min={SQL_EDITOR_LEFTBAR_WIDTH}
>
{adjustedWidth => (
<StyledSidebar
className={`schemaPane ${leftBarStateClass}`}
width={adjustedWidth}
hide={hideLeftBar}
<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;
`}
>
<SqlEditorLeftBar
database={database}
queryEditorId={queryEditor.id}
/>
</StyledSidebar>
<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()
)}
</ResizableSidebar>
</CSSTransition>
{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()
)}
</Splitter.Panel>
</Splitter>
<Modal
show={showCreateAsModal}
name={t(createViewModalTitle)}

View File

@@ -86,7 +86,6 @@ const MENUS = [
},
];
const TAB_HEADER_HEIGHT = 80;
const PREVIEW_TOP_ACTION_HEIGHT = 30;
const PREVIEW_QUERY_LIMIT = 100;
const Title = styled.div`
@@ -382,9 +381,6 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
visualize={false}
csv={false}
cache
height={
height - TAB_HEADER_HEIGHT - PREVIEW_TOP_ACTION_HEIGHT
}
displayLimit={PREVIEW_QUERY_LIMIT}
defaultQueryLimit={PREVIEW_QUERY_LIMIT}
/>