feat(sqllab): Make LeftBar width resizable (#21300)

This commit is contained in:
JUST.in DO IT 2022-09-01 21:26:20 -07:00 committed by GitHub
parent 38782bb98a
commit 2d70ef670e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 206 additions and 112 deletions

View File

@ -31,6 +31,7 @@ import StyledModal from 'src/components/Modal';
import Mousetrap from 'mousetrap';
import Button from 'src/components/Button';
import Timer from 'src/components/Timer';
import ResizableSidebar from 'src/components/ResizableSidebar';
import { AntdDropdown, AntdSwitch } from 'src/components';
import { Input } from 'src/components/Input';
import { Menu } from 'src/components/Menu';
@ -60,6 +61,7 @@ import {
SQL_EDITOR_GUTTER_HEIGHT,
SQL_EDITOR_GUTTER_MARGIN,
SQL_TOOLBAR_HEIGHT,
SQL_EDITOR_LEFTBAR_WIDTH,
} from 'src/SqlLab/constants';
import {
getItem,
@ -127,6 +129,15 @@ const StyledToolbar = styled.div`
}
`;
const StyledSidebar = styled.div`
flex: 0 0 ${({ width }) => width}px;
width: ${({ width }) => width}px;
padding: ${({ hide }) => (hide ? 0 : 10)}px;
border-right: 1px solid
${({ theme, hide }) =>
hide ? 'transparent' : theme.colors.grayscale.light2};
`;
const propTypes = {
actions: PropTypes.object.isRequired,
database: PropTypes.object,
@ -674,7 +685,6 @@ class SqlEditor extends React.PureComponent {
this.state.createAs === CtasEnum.VIEW
? 'Specify name to CREATE VIEW AS schema in: public'
: 'Specify name to CREATE TABLE AS schema in: public';
const leftBarStateClass = this.props.hideLeftBar
? 'schemaPane-exit-done'
: 'schemaPane-enter-done';
@ -685,15 +695,28 @@ class SqlEditor extends React.PureComponent {
in={!this.props.hideLeftBar}
timeout={300}
>
<div className={`schemaPane ${leftBarStateClass}`}>
<SqlEditorLeftBar
database={this.props.database}
queryEditor={this.props.queryEditor}
tables={this.props.tables}
actions={this.props.actions}
setEmptyState={this.setEmptyState}
/>
</div>
<ResizableSidebar
id={`sqllab:${this.props.queryEditor.id}`}
minWidth={SQL_EDITOR_LEFTBAR_WIDTH}
initialWidth={SQL_EDITOR_LEFTBAR_WIDTH}
enable={!this.props.hideLeftBar}
>
{adjustedWidth => (
<StyledSidebar
className={`schemaPane ${leftBarStateClass}`}
width={adjustedWidth}
hide={this.props.hideLeftBar}
>
<SqlEditorLeftBar
database={this.props.database}
queryEditor={this.props.queryEditor}
tables={this.props.tables}
actions={this.props.actions}
setEmptyState={this.setEmptyState}
/>
</StyledSidebar>
)}
</ResizableSidebar>
</CSSTransition>
{this.state.showEmptyState ? (
<EmptyStateBig
@ -754,17 +777,22 @@ SqlEditor.defaultProps = defaultProps;
SqlEditor.propTypes = propTypes;
function mapStateToProps({ sqlLab }, { queryEditor }) {
let { latestQueryId, dbId } = queryEditor;
let { latestQueryId, dbId, hideLeftBar } = queryEditor;
if (sqlLab.unsavedQueryEditor.id === queryEditor.id) {
const { latestQueryId: unsavedQID, dbId: unsavedDBID } =
sqlLab.unsavedQueryEditor;
const {
latestQueryId: unsavedQID,
dbId: unsavedDBID,
hideLeftBar: unsavedHideLeftBar,
} = sqlLab.unsavedQueryEditor;
latestQueryId = unsavedQID || latestQueryId;
dbId = unsavedDBID || dbId;
hideLeftBar = unsavedHideLeftBar || hideLeftBar;
}
const database = sqlLab.databases[dbId];
const latestQuery = sqlLab.queries[latestQueryId];
return {
hideLeftBar,
queryEditors: sqlLab.queryEditors,
latestQuery,
database,

View File

@ -305,7 +305,6 @@ class TabbedSqlEditors extends React.PureComponent {
editorQueries={this.state.queriesArray}
dataPreviewQueries={this.state.dataPreviewQueries}
actions={this.props.actions}
hideLeftBar={qe.hideLeftBar}
defaultQueryLimit={this.props.defaultQueryLimit}
maxRow={this.props.maxRow}
displayLimit={this.props.displayLimit}

View File

@ -48,6 +48,7 @@ export const TIME_OPTIONS = [
export const SQL_EDITOR_GUTTER_HEIGHT = 5;
export const SQL_EDITOR_GUTTER_MARGIN = 3;
export const SQL_TOOLBAR_HEIGHT = 51;
export const SQL_EDITOR_LEFTBAR_WIDTH = 400;
// kilobyte storage
export const KB_STORAGE = 1024;

View File

@ -283,16 +283,14 @@ div.Workspace {
display: flex;
flex-direction: row;
height: 100%;
padding: 10px;
.schemaPane {
flex: 0 0 400px;
transition: transform @timing-normal ease-in-out;
}
.queryPane {
flex: 1 1 auto;
padding-left: 10px;
padding: 10px;
overflow-y: none;
overflow-x: scroll;
}

View File

@ -0,0 +1,82 @@
/**
* 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 React from 'react';
import { Resizable } from 're-resizable';
import { styled } from '@superset-ui/core';
import useStoredSidebarWidth from './useStoredSidebarWidth';
const ResizableWrapper = styled.div`
position: absolute;
height: 100%;
:hover .sidebar-resizer::after {
background-color: ${({ theme }) => theme.colors.primary.base};
}
.sidebar-resizer {
// @z-index-above-sticky-header (100) + 1 = 101
z-index: 101;
}
.sidebar-resizer::after {
display: block;
content: '';
width: 1px;
height: 100%;
margin: 0 auto;
}
`;
type Props = {
id: string;
initialWidth: number;
enable: boolean;
minWidth?: number;
maxWidth?: number;
children: (width: number) => React.ReactNode;
};
const ResizableSidebar: React.FC<Props> = ({
id,
initialWidth,
minWidth,
maxWidth,
enable,
children,
}) => {
const [width, setWidth] = useStoredSidebarWidth(id, initialWidth);
return (
<>
<ResizableWrapper>
<Resizable
enable={{ right: enable }}
handleClasses={{ right: 'sidebar-resizer' }}
size={{ width, height: '100%' }}
minWidth={minWidth}
maxWidth={maxWidth}
onResizeStop={(e, direction, ref, d) => setWidth(width + d.width)}
/>
</ResizableWrapper>
{children(width)}
</>
);
};
export default ResizableSidebar;

View File

@ -22,10 +22,11 @@ import {
setItem,
getItem,
} from 'src/utils/localStorageHelpers';
import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
import useStoredFilterBarWidth from './useStoredFilterBarWidth';
import useStoredSidebarWidth from './useStoredSidebarWidth';
describe('useStoredFilterBarWidth', () => {
const INITIAL_WIDTH = 300;
describe('useStoredSidebarWidth', () => {
beforeEach(() => {
localStorage.clear();
});
@ -34,22 +35,26 @@ describe('useStoredFilterBarWidth', () => {
localStorage.clear();
});
it('returns a default filterBar width by OPEN_FILTER_BAR_WIDTH', () => {
const dashboardId = '123';
const { result } = renderHook(() => useStoredFilterBarWidth(dashboardId));
it('returns a default filterBar width by initialWidth', () => {
const id = '123';
const { result } = renderHook(() =>
useStoredSidebarWidth(id, INITIAL_WIDTH),
);
const [actualWidth] = result.current;
expect(actualWidth).toEqual(OPEN_FILTER_BAR_WIDTH);
expect(actualWidth).toEqual(INITIAL_WIDTH);
});
it('returns a stored filterBar width from localStorage', () => {
const dashboardId = '123';
const id = '123';
const expectedWidth = 378;
setItem(LocalStorageKeys.dashboard__custom_filter_bar_widths, {
[dashboardId]: expectedWidth,
setItem(LocalStorageKeys.common__resizable_sidebar_widths, {
[id]: expectedWidth,
'456': 250,
});
const { result } = renderHook(() => useStoredFilterBarWidth(dashboardId));
const { result } = renderHook(() =>
useStoredSidebarWidth(id, INITIAL_WIDTH),
);
const [actualWidth] = result.current;
expect(actualWidth).toEqual(expectedWidth);
@ -57,15 +62,17 @@ describe('useStoredFilterBarWidth', () => {
});
it('returns a setter for filterBar width that stores the state in localStorage together', () => {
const dashboardId = '123';
const id = '123';
const expectedWidth = 378;
const otherDashboardId = '456';
const otherDashboardWidth = 253;
setItem(LocalStorageKeys.dashboard__custom_filter_bar_widths, {
[dashboardId]: 300,
setItem(LocalStorageKeys.common__resizable_sidebar_widths, {
[id]: 300,
[otherDashboardId]: otherDashboardWidth,
});
const { result } = renderHook(() => useStoredFilterBarWidth(dashboardId));
const { result } = renderHook(() =>
useStoredSidebarWidth(id, INITIAL_WIDTH),
);
const [prevWidth, setter] = result.current;
expect(prevWidth).toEqual(300);
@ -74,10 +81,10 @@ describe('useStoredFilterBarWidth', () => {
const updatedWidth = result.current[0];
const widthsMap = getItem(
LocalStorageKeys.dashboard__custom_filter_bar_widths,
LocalStorageKeys.common__resizable_sidebar_widths,
{},
);
expect(widthsMap[dashboardId]).toEqual(expectedWidth);
expect(widthsMap[id]).toEqual(expectedWidth);
expect(widthsMap[otherDashboardId]).toEqual(otherDashboardWidth);
expect(updatedWidth).toEqual(expectedWidth);
expect(updatedWidth).not.toEqual(250);

View File

@ -22,30 +22,30 @@ import {
setItem,
getItem,
} from 'src/utils/localStorageHelpers';
import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
export default function useStoredFilterBarWidth(dashboardId: string) {
export default function useStoredSidebarWidth(
id: string,
initialWidth: number,
) {
const widthsMapRef = useRef<Record<string, number>>();
const [filterBarWidth, setFilterBarWidth] = useState<number>(
OPEN_FILTER_BAR_WIDTH,
);
const [sidebarWidth, setSidebarWidth] = useState<number>(initialWidth);
useEffect(() => {
widthsMapRef.current =
widthsMapRef.current ??
getItem(LocalStorageKeys.dashboard__custom_filter_bar_widths, {});
if (widthsMapRef.current[dashboardId]) {
setFilterBarWidth(widthsMapRef.current[dashboardId]);
getItem(LocalStorageKeys.common__resizable_sidebar_widths, {});
if (widthsMapRef.current[id]) {
setSidebarWidth(widthsMapRef.current[id]);
}
}, [dashboardId]);
}, [id]);
function setStoredFilterBarWidth(updatedWidth: number) {
setFilterBarWidth(updatedWidth);
setItem(LocalStorageKeys.dashboard__custom_filter_bar_widths, {
function setStoredSidebarWidth(updatedWidth: number) {
setSidebarWidth(updatedWidth);
setItem(LocalStorageKeys.common__resizable_sidebar_widths, {
...widthsMapRef.current,
[dashboardId]: updatedWidth,
[id]: updatedWidth,
});
}
return [filterBarWidth, setStoredFilterBarWidth] as const;
return [sidebarWidth, setStoredSidebarWidth] as const;
}

View File

@ -39,12 +39,14 @@ import { useToasts } from 'src/components/MessageToasts/withToasts';
import { SchemaOption } from 'src/SqlLab/types';
import { useTables, Table } from 'src/hooks/apiResources';
const REFRESH_WIDTH = 30;
const TableSelectorWrapper = styled.div`
${({ theme }) => `
.refresh {
display: flex;
align-items: center;
width: 30px;
width: ${REFRESH_WIDTH}px;
margin-left: ${theme.gridUnit}px;
margin-top: ${theme.gridUnit * 5}px;
}
@ -66,6 +68,7 @@ const TableSelectorWrapper = styled.div`
.select {
flex: 1;
max-width: calc(100% - ${theme.gridUnit + REFRESH_WIDTH}px)
}
`}
`;

View File

@ -23,7 +23,7 @@ import { render } from 'spec/helpers/testing-library';
import { fireEvent, within } from '@testing-library/react';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder';
import useStoredFilterBarWidth from 'src/dashboard/components/DashboardBuilder/useStoredFilterBarWidth';
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
import {
fetchFaveStar,
setActiveTabs,
@ -46,7 +46,7 @@ jest.mock('src/dashboard/actions/dashboardState', () => ({
setDirectPathToChild: jest.fn(),
}));
jest.mock('src/featureFlags');
jest.mock('src/dashboard/components/DashboardBuilder/useStoredFilterBarWidth');
jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth');
// mock following dependant components to fix the prop warnings
jest.mock('src/components/Icons/Icon', () => () => (
@ -98,7 +98,7 @@ describe('DashboardBuilder', () => {
activeTabsStub = (setActiveTabs as jest.Mock).mockReturnValue({
type: 'mock-action',
});
(useStoredFilterBarWidth as jest.Mock).mockImplementation(() => [
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
100,
jest.fn(),
]);
@ -108,7 +108,7 @@ describe('DashboardBuilder', () => {
afterAll(() => {
favStarStub.mockReset();
activeTabsStub.mockReset();
(useStoredFilterBarWidth as jest.Mock).mockReset();
(useStoredSidebarWidth as jest.Mock).mockReset();
});
function setup(overrideState = {}, overrideStore?: Store) {
@ -259,10 +259,10 @@ describe('DashboardBuilder', () => {
(isFeatureEnabled as jest.Mock).mockReset();
});
it('should set FilterBar width by useStoredFilterBarWidth', () => {
it('should set FilterBar width by useStoredSidebarWidth', () => {
const expectedValue = 200;
const setter = jest.fn();
(useStoredFilterBarWidth as jest.Mock).mockImplementation(() => [
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
expectedValue,
setter,
]);

View File

@ -26,7 +26,6 @@ import React, {
useMemo,
useRef,
} from 'react';
import { Resizable } from 're-resizable';
import { JsonObject, styled, css, t } from '@superset-ui/core';
import { Global } from '@emotion/react';
import { useDispatch, useSelector } from 'react-redux';
@ -62,6 +61,7 @@ import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
import Loading from 'src/components/Loading';
import { EmptyStateBig } from 'src/components/EmptyState';
import { useUiConfig } from 'src/components/UiConfigContext';
import ResizableSidebar from 'src/components/ResizableSidebar';
import {
BUILDER_SIDEPANEL_WIDTH,
CLOSED_FILTER_BAR_WIDTH,
@ -74,7 +74,6 @@ import {
import { shouldFocusTabs, getRootLevelTabsComponent } from './utils';
import DashboardContainer from './DashboardContainer';
import { useNativeFilters } from './state';
import useStoredFilterBarWidth from './useStoredFilterBarWidth';
type DashboardBuilderProps = {};
@ -220,27 +219,6 @@ const StyledDashboardContent = styled.div<{
}
`;
const ResizableFilterBarWrapper = styled.div`
position: absolute;
:hover .filterbar-resizer::after {
background-color: ${({ theme }) => theme.colors.primary.base};
}
.filterbar-resizer {
// @z-index-above-sticky-header (100) + 1 = 101
z-index: 101;
}
.filterbar-resizer::after {
display: block;
content: '';
width: 1px;
height: 100%;
margin: 0 auto;
}
`;
const DashboardBuilder: FC<DashboardBuilderProps> = () => {
const dispatch = useDispatch();
const uiConfig = useUiConfig();
@ -327,13 +305,6 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
nativeFiltersEnabled,
} = useNativeFilters();
const [adjustedFilterBarWidth, setAdjustedFilterBarWidth] =
useStoredFilterBarWidth(dashboardId);
const filterBarWidth = dashboardFiltersOpen
? adjustedFilterBarWidth
: CLOSED_FILTER_BAR_WIDTH;
const [containerRef, isSticky] = useElementOnScreen<HTMLDivElement>({
threshold: [1],
});
@ -425,35 +396,38 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
<StyledDiv>
{nativeFiltersEnabled && !editMode && (
<>
<ResizableFilterBarWrapper>
<Resizable
enable={{ right: dashboardFiltersOpen }}
handleClasses={{ right: 'filterbar-resizer' }}
size={{ width: filterBarWidth, height: '100vh' }}
minWidth={OPEN_FILTER_BAR_WIDTH}
maxWidth={OPEN_FILTER_BAR_MAX_WIDTH}
onResizeStop={(e, direction, ref, d) =>
setAdjustedFilterBarWidth(filterBarWidth + d.width)
}
/>
</ResizableFilterBarWrapper>
<FiltersPanel
width={filterBarWidth}
data-test="dashboard-filters-panel"
<ResizableSidebar
id={`dashboard:${dashboardId}`}
enable={dashboardFiltersOpen}
minWidth={OPEN_FILTER_BAR_WIDTH}
maxWidth={OPEN_FILTER_BAR_MAX_WIDTH}
initialWidth={OPEN_FILTER_BAR_WIDTH}
>
<StickyPanel ref={containerRef} width={filterBarWidth}>
<ErrorBoundary>
<FilterBar
filtersOpen={dashboardFiltersOpen}
toggleFiltersBar={toggleDashboardFiltersOpen}
directPathToChild={directPathToChild}
{adjustedWidth => {
const filterBarWidth = dashboardFiltersOpen
? adjustedWidth
: CLOSED_FILTER_BAR_WIDTH;
return (
<FiltersPanel
width={filterBarWidth}
height={filterBarHeight}
offset={filterBarOffset}
/>
</ErrorBoundary>
</StickyPanel>
</FiltersPanel>
data-test="dashboard-filters-panel"
>
<StickyPanel ref={containerRef} width={filterBarWidth}>
<ErrorBoundary>
<FilterBar
filtersOpen={dashboardFiltersOpen}
toggleFiltersBar={toggleDashboardFiltersOpen}
directPathToChild={directPathToChild}
width={filterBarWidth}
height={filterBarHeight}
offset={filterBarOffset}
/>
</ErrorBoundary>
</StickyPanel>
</FiltersPanel>
);
}}
</ResizableSidebar>
</>
)}
<StyledHeader ref={headerRef}>

View File

@ -55,6 +55,7 @@ export enum LocalStorageKeys {
explore__data_table_original_formatted_time_columns = 'explore__data_table_original_formatted_time_columns',
dashboard__custom_filter_bar_widths = 'dashboard__custom_filter_bar_widths',
dashboard__explore_context = 'dashboard__explore_context',
common__resizable_sidebar_widths = 'common__resizable_sidebar_widths',
}
export type LocalStorageValues = {
@ -73,6 +74,7 @@ export type LocalStorageValues = {
explore__data_table_original_formatted_time_columns: Record<string, string[]>;
dashboard__custom_filter_bar_widths: Record<string, number>;
dashboard__explore_context: Record<string, DashboardContextForExplore>;
common__resizable_sidebar_widths: Record<string, number>;
};
/*