feat(explore): Move chart header to top of the page (#19529)

* Move chart header to top of the page

* Implement truncating and dynamic input

* fix typing

* Prevent cmd+z undoing changes when not in edit mode

* Fix tests, add missing types

* Show changed title in altered
This commit is contained in:
Kamil Gabryjelski 2022-04-05 15:20:29 +02:00 committed by GitHub
parent 1eef923b31
commit 602afbaa31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 604 additions and 302 deletions

View File

@ -18,7 +18,7 @@
*/
import React, { useCallback } from 'react';
import { t, styled } from '@superset-ui/core';
import { css, t, styled } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { useComponentDidMount } from 'src/hooks/useComponentDidMount';
import Icons from 'src/components/Icons';
@ -32,9 +32,11 @@ interface FaveStarProps {
}
const StyledLink = styled.a`
font-size: ${({ theme }) => theme.typography.sizes.xl}px;
display: flex;
padding: 0 0 0 0.5em;
${({ theme }) => css`
font-size: ${theme.typography.sizes.xl}px;
display: flex;
padding: 0 0 0 ${theme.gridUnit * 2}px;
`};
`;
const FaveStar = ({

View File

@ -86,7 +86,7 @@ const DatasourceContainer = styled.div`
color: ${theme.colors.grayscale.light1};
}
.form-control.input-md {
width: calc(100% - ${theme.gridUnit * 4}px);
width: calc(100% - ${theme.gridUnit * 8}px);
height: ${theme.gridUnit * 8}px;
margin: ${theme.gridUnit * 2}px auto;
}

View File

@ -0,0 +1,68 @@
/**
* 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 userEvent from '@testing-library/user-event';
import { render, screen } from 'spec/helpers/testing-library';
import { ChartEditableTitle } from './index';
const createProps = (overrides: Record<string, any> = {}) => ({
title: 'Chart title',
placeholder: 'Add the name of the chart',
canEdit: true,
onSave: jest.fn(),
...overrides,
});
describe('Chart editable title', () => {
it('renders chart title', () => {
const props = createProps();
render(<ChartEditableTitle {...props} />);
expect(screen.getByText('Chart title')).toBeVisible();
});
it('renders placeholder', () => {
const props = createProps({
title: '',
});
render(<ChartEditableTitle {...props} />);
expect(screen.getByText('Add the name of the chart')).toBeVisible();
});
it('click, edit and save title', () => {
const props = createProps();
render(<ChartEditableTitle {...props} />);
const textboxElement = screen.getByRole('textbox');
userEvent.click(textboxElement);
userEvent.type(textboxElement, ' edited');
expect(screen.getByText('Chart title edited')).toBeVisible();
userEvent.type(textboxElement, '{enter}');
expect(props.onSave).toHaveBeenCalled();
});
it('renders in non-editable mode', () => {
const props = createProps({ canEdit: false });
render(<ChartEditableTitle {...props} />);
const titleElement = screen.getByLabelText('Chart title');
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
expect(titleElement).toBeVisible();
userEvent.click(titleElement);
userEvent.type(titleElement, ' edited{enter}');
expect(props.onSave).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,213 @@
/**
* 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, {
ChangeEvent,
KeyboardEvent,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { css, styled, t } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { useResizeDetector } from 'react-resize-detector';
export type ChartEditableTitleProps = {
title: string;
placeholder: string;
onSave: (title: string) => void;
canEdit: boolean;
};
const Styles = styled.div`
${({ theme }) => css`
display: flex;
font-size: ${theme.typography.sizes.xl}px;
font-weight: ${theme.typography.weights.bold};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& .chart-title,
& .chart-title-input {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& .chart-title {
cursor: default;
}
& .chart-title-input {
border: none;
padding: 0;
outline: none;
&::placeholder {
color: ${theme.colors.grayscale.light1};
}
}
& .input-sizer {
position: absolute;
left: -9999px;
display: inline-block;
}
`}
`;
export const ChartEditableTitle = ({
title,
placeholder,
onSave,
canEdit,
}: ChartEditableTitleProps) => {
const [isEditing, setIsEditing] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title || '');
const contentRef = useRef<HTMLInputElement>(null);
const [showTooltip, setShowTooltip] = useState(false);
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
const { width: containerWidth, ref: containerRef } = useResizeDetector({
refreshMode: 'debounce',
});
useEffect(() => {
if (isEditing && contentRef?.current) {
contentRef.current.focus();
// move cursor and scroll to the end
if (contentRef.current.setSelectionRange) {
const { length } = contentRef.current.value;
contentRef.current.setSelectionRange(length, length);
contentRef.current.scrollLeft = contentRef.current.scrollWidth;
}
}
}, [isEditing]);
// a trick to make the input grow when user types text
// we make additional span component, place it somewhere out of view and copy input
// then we can measure the width of that span to resize the input element
useLayoutEffect(() => {
if (sizerRef?.current) {
sizerRef.current.innerHTML = (currentTitle || placeholder).replace(
/\s/g,
'&nbsp;',
);
}
}, [currentTitle, placeholder, sizerRef]);
useEffect(() => {
if (
contentRef.current &&
contentRef.current.scrollWidth > contentRef.current.clientWidth
) {
setShowTooltip(true);
} else {
setShowTooltip(false);
}
}, [inputWidth, containerWidth]);
const handleClick = useCallback(() => {
if (!canEdit || isEditing) {
return;
}
setIsEditing(true);
}, [canEdit, isEditing]);
const handleBlur = useCallback(() => {
if (!canEdit) {
return;
}
const formattedTitle = currentTitle.trim();
setCurrentTitle(formattedTitle);
if (title !== formattedTitle) {
onSave(formattedTitle);
}
setIsEditing(false);
}, [canEdit, currentTitle, onSave, title]);
const handleChange = useCallback(
(ev: ChangeEvent<HTMLInputElement>) => {
if (!canEdit || !isEditing) {
return;
}
setCurrentTitle(ev.target.value);
},
[canEdit, isEditing],
);
const handleKeyPress = useCallback(
(ev: KeyboardEvent<HTMLInputElement>) => {
if (!canEdit) {
return;
}
if (ev.key === 'Enter') {
ev.preventDefault();
contentRef.current?.blur();
}
},
[canEdit],
);
return (
<Styles ref={containerRef}>
<Tooltip
id="title-tooltip"
title={showTooltip && currentTitle && !isEditing ? currentTitle : null}
>
{canEdit ? (
<input
data-test="editable-title-input"
className="chart-title-input"
aria-label={t('Chart title')}
ref={contentRef}
onChange={handleChange}
onBlur={handleBlur}
onClick={handleClick}
onKeyPress={handleKeyPress}
placeholder={placeholder}
value={currentTitle}
css={css`
cursor: ${isEditing ? 'text' : 'pointer'};
${inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`}
`}
/>
) : (
<span
className="chart-title"
aria-label={t('Chart title')}
ref={contentRef}
>
{currentTitle}
</span>
)}
</Tooltip>
<span ref={sizerRef} className="input-sizer" aria-hidden tabIndex={-1} />
</Styles>
);
};

View File

@ -22,6 +22,7 @@ import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import {
CategoricalColorNamespace,
css,
SupersetClient,
styled,
t,
@ -33,7 +34,6 @@ import {
} from 'src/reports/actions/reports';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import EditableTitle from 'src/components/EditableTitle';
import AlteredSliceTag from 'src/components/AlteredSliceTag';
import FaveStar from 'src/components/FaveStar';
import Timer from 'src/components/Timer';
@ -44,6 +44,7 @@ import CertifiedBadge from 'src/components/CertifiedBadge';
import withToasts from 'src/components/MessageToasts/withToasts';
import RowCountLabel from '../RowCountLabel';
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu';
import { ChartEditableTitle } from './ChartEditableTitle';
const CHART_STATUS_MAP = {
failed: 'danger',
@ -53,8 +54,8 @@ const CHART_STATUS_MAP = {
const propTypes = {
actions: PropTypes.object.isRequired,
can_overwrite: PropTypes.bool.isRequired,
can_download: PropTypes.bool.isRequired,
canOverwrite: PropTypes.bool.isRequired,
canDownload: PropTypes.bool.isRequired,
dashboardId: PropTypes.number,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
@ -67,37 +68,41 @@ const propTypes = {
};
const StyledHeader = styled.div`
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
span[role='button'] {
${({ theme }) => css`
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: space-between;
height: 100%;
}
.title-panel {
display: flex;
align-items: center;
}
.right-button-panel {
display: flex;
align-items: center;
> .btn-group {
flex: 0 0 auto;
margin-left: ${({ theme }) => theme.gridUnit}px;
span[role='button'] {
display: flex;
height: 100%;
}
}
.action-button {
color: ${({ theme }) => theme.colors.grayscale.base};
margin: 0 ${({ theme }) => theme.gridUnit * 1.5}px 0
${({ theme }) => theme.gridUnit}px;
}
.title-panel {
display: flex;
align-items: center;
min-width: 0;
margin-right: ${theme.gridUnit * 6}px;
}
.right-button-panel {
display: flex;
align-items: center;
> .btn-group {
flex: 0 0 auto;
margin-left: ${theme.gridUnit}px;
}
}
.action-button {
color: ${theme.colors.grayscale.base};
margin: 0 ${theme.gridUnit * 1.5}px 0 ${theme.gridUnit}px;
}
`}
`;
const StyledButtons = styled.span`
@ -173,13 +178,6 @@ export class ExploreChartHeader extends React.PureComponent {
.catch(() => {});
}
getSliceName() {
const { sliceName, table_name: tableName } = this.props;
const title = sliceName || t('%s - untitled', tableName);
return title;
}
postChartFormData() {
this.props.actions.postChartFormData(
this.props.form_data,
@ -221,22 +219,45 @@ export class ExploreChartHeader extends React.PureComponent {
}
render() {
const { user, form_data: formData, slice } = this.props;
const {
actions,
chart,
user,
formData,
slice,
canOverwrite,
canDownload,
isStarred,
sliceUpdated,
sliceName,
} = this.props;
const {
chartStatus,
chartUpdateEndTime,
chartUpdateStartTime,
latestQueryFormData,
queriesResponse,
} = this.props.chart;
sliceFormData,
} = chart;
// TODO: when will get appropriate design for multi queries use all results and not first only
const queryResponse = queriesResponse?.[0];
const oldSliceName = slice?.slice_name;
const chartFinished = ['failed', 'rendered', 'success'].includes(
this.props.chart.chartStatus,
chartStatus,
);
return (
<StyledHeader id="slice-header" className="panel-title-large">
<StyledHeader id="slice-header">
<div className="title-panel">
<ChartEditableTitle
title={sliceName}
canEdit={
!slice ||
canOverwrite ||
(slice?.owners || []).includes(user?.userId)
}
onSave={actions.updateChartTitle}
placeholder={t('Add the name of the chart')}
/>
{slice?.certified_by && (
<>
<CertifiedBadge
@ -245,26 +266,14 @@ export class ExploreChartHeader extends React.PureComponent {
/>{' '}
</>
)}
<EditableTitle
title={this.getSliceName()}
canEdit={
!this.props.slice ||
this.props.can_overwrite ||
(this.props.slice?.owners || []).includes(
this.props?.user?.userId,
)
}
onSaveTitle={this.props.actions.updateChartTitle}
/>
{this.props.slice && (
{slice && (
<StyledButtons>
{user.userId && (
<FaveStar
itemId={this.props.slice.slice_id}
fetchFaveStar={this.props.actions.fetchFaveStar}
saveFaveStar={this.props.actions.saveFaveStar}
isStarred={this.props.isStarred}
itemId={slice.slice_id}
fetchFaveStar={actions.fetchFaveStar}
saveFaveStar={actions.saveFaveStar}
isStarred={isStarred}
showTooltip
/>
)}
@ -272,15 +281,15 @@ export class ExploreChartHeader extends React.PureComponent {
<PropertiesModal
show={this.state.isPropertiesModalOpen}
onHide={this.closePropertiesModal}
onSave={this.props.sliceUpdated}
slice={this.props.slice}
onSave={sliceUpdated}
slice={slice}
/>
)}
{this.props.chart.sliceFormData && (
{sliceFormData && (
<AlteredSliceTag
className="altered"
origFormData={this.props.chart.sliceFormData}
currentFormData={formData}
origFormData={{ ...sliceFormData, chartTitle: oldSliceName }}
currentFormData={{ ...formData, chartTitle: sliceName }}
/>
)}
</StyledButtons>
@ -306,10 +315,10 @@ export class ExploreChartHeader extends React.PureComponent {
status={CHART_STATUS_MAP[chartStatus]}
/>
<ExploreAdditionalActionsMenu
onOpenInEditor={this.props.actions.redirectSQLLab}
onOpenInEditor={actions.redirectSQLLab}
onOpenPropertiesModal={this.openPropertiesModal}
slice={this.props.slice}
canDownloadCSV={this.props.can_download}
slice={slice}
canDownloadCSV={canDownload}
latestQueryFormData={latestQueryFormData}
canAddReports={this.canAddReports()}
/>

View File

@ -28,7 +28,6 @@ import {
setItem,
LocalStorageKeys,
} from 'src/utils/localStorageHelpers';
import ConnectedExploreChartHeader from './ExploreChartHeader';
import { DataTablesPane } from './DataTablesPane';
import { buildV1ChartDataPayload } from '../exploreUtils';
@ -63,7 +62,6 @@ const GUTTER_SIZE_FACTOR = 1.25;
const CHART_PANEL_PADDING_HORIZ = 30;
const CHART_PANEL_PADDING_VERTICAL = 15;
const HEADER_PADDING = 15;
const INITIAL_SIZES = [90, 10];
const MIN_SIZES = [300, 50];
@ -78,8 +76,8 @@ const Styles = styled.div`
box-shadow: none;
height: 100%;
& > div:last-of-type {
flex-basis: 100%;
& > div {
height: 100%;
}
.gutter {
@ -114,10 +112,6 @@ const ExploreChartPanel = props => {
const theme = useTheme();
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
const { height: hHeight, ref: headerRef } = useResizeDetector({
refreshMode: 'debounce',
refreshRate: 300,
});
const { width: chartPanelWidth, ref: chartPanelRef } = useResizeDetector({
refreshMode: 'debounce',
refreshRate: 300,
@ -156,21 +150,10 @@ const ExploreChartPanel = props => {
}, [updateQueryContext]);
const calcSectionHeight = useCallback(
percent => {
let headerHeight;
if (props.standalone) {
headerHeight = 0;
} else if (hHeight) {
headerHeight = hHeight + HEADER_PADDING;
} else {
headerHeight = 50;
}
const containerHeight = parseInt(props.height, 10) - headerHeight;
return (
(containerHeight * percent) / 100 - (gutterHeight / 2 + gutterMargin)
);
},
[gutterHeight, gutterMargin, props.height, props.standalone, hHeight],
percent =>
(parseInt(props.height, 10) * percent) / 100 -
(gutterHeight / 2 + gutterMargin),
[gutterHeight, gutterMargin, props.height, props.standalone],
);
const [tableSectionHeight, setTableSectionHeight] = useState(
@ -283,34 +266,12 @@ const ExploreChartPanel = props => {
return standaloneChartBody;
}
const header = (
<ConnectedExploreChartHeader
ownState={props.ownState}
actions={props.actions}
can_overwrite={props.can_overwrite}
can_download={props.can_download}
dashboardId={props.dashboardId}
isStarred={props.isStarred}
slice={props.slice}
sliceName={props.sliceName}
table_name={props.table_name}
form_data={props.form_data}
timeout={props.timeout}
chart={props.chart}
user={props.user}
reports={props.reports}
/>
);
const elementStyle = (dimension, elementSize, gutterSize) => ({
[dimension]: `calc(${elementSize}% - ${gutterSize + gutterMargin}px)`,
});
return (
<Styles className="panel panel-default chart-container" ref={chartPanelRef}>
<div className="panel-heading" ref={headerRef}>
{header}
</div>
{props.vizType === 'filter_box' ? (
panelBody
) : (

View File

@ -60,6 +60,7 @@ import {
LOG_ACTIONS_MOUNT_EXPLORER,
LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS,
} from '../../../logger/LogUtils';
import ConnectedExploreChartHeader from '../ExploreChartHeader';
const propTypes = {
...ExploreChartPanel.propTypes,
@ -82,69 +83,96 @@ const propTypes = {
vizType: PropTypes.string,
};
const Styles = styled.div`
background: ${({ theme }) => theme.colors.grayscale.light5};
text-align: left;
position: relative;
width: 100%;
max-height: 100%;
const ExploreContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-basis: 100vh;
align-items: stretch;
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
.explore-column {
display: flex;
flex-direction: column;
padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
max-height: 100%;
}
.data-source-selection {
background-color: ${({ theme }) => theme.colors.grayscale.light5};
padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
}
.main-explore-content {
flex: 1;
min-width: ${({ theme }) => theme.gridUnit * 128}px;
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
.panel {
margin-bottom: 0;
flex-direction: column;
height: 100%;
`;
const ExploreHeaderContainer = styled.div`
${({ theme }) => css`
background-color: ${theme.colors.grayscale.light5};
height: ${theme.gridUnit * 16}px;
padding: 0 ${theme.gridUnit * 4}px;
.editable-title {
overflow: hidden;
& > input[type='button'],
& > span {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
white-space: nowrap;
}
}
}
.controls-column {
align-self: flex-start;
padding: 0;
}
.title-container {
`}
`;
const ExplorePanelContainer = styled.div`
${({ theme }) => css`
background: ${theme.colors.grayscale.light5};
text-align: left;
position: relative;
width: 100%;
max-height: 100%;
min-height: 0;
display: flex;
flex-direction: row;
padding: 0 ${({ theme }) => 2 * theme.gridUnit}px;
justify-content: space-between;
.horizontal-text {
text-transform: uppercase;
color: ${({ theme }) => theme.colors.grayscale.light1};
font-size: ${({ theme }) => 4 * theme.typography.sizes.s};
flex: 1;
flex-wrap: nowrap;
border-top: 1px solid ${theme.colors.grayscale.light2};
.explore-column {
display: flex;
flex-direction: column;
padding: ${theme.gridUnit * 2}px 0;
max-height: 100%;
}
}
.no-show {
display: none;
}
.vertical-text {
writing-mode: vertical-rl;
text-orientation: mixed;
}
.sidebar {
height: 100%;
background-color: ${({ theme }) => theme.colors.grayscale.light4};
padding: ${({ theme }) => 2 * theme.gridUnit}px;
width: ${({ theme }) => 8 * theme.gridUnit}px;
}
.callpase-icon > svg {
color: ${({ theme }) => theme.colors.primary.base};
}
.data-source-selection {
background-color: ${theme.colors.grayscale.light5};
padding: ${theme.gridUnit * 2}px 0;
border-right: 1px solid ${theme.colors.grayscale.light2};
}
.main-explore-content {
flex: 1;
min-width: ${theme.gridUnit * 128}px;
border-left: 1px solid ${theme.colors.grayscale.light2};
.panel {
margin-bottom: 0;
}
}
.controls-column {
align-self: flex-start;
padding: 0;
}
.title-container {
position: relative;
display: flex;
flex-direction: row;
padding: 0 ${theme.gridUnit * 4}px;
justify-content: space-between;
.horizontal-text {
text-transform: uppercase;
color: ${theme.colors.grayscale.light1};
font-size: ${theme.typography.sizes.s * 4};
}
}
.no-show {
display: none;
}
.vertical-text {
writing-mode: vertical-rl;
text-orientation: mixed;
}
.sidebar {
height: 100%;
background-color: ${theme.colors.grayscale.light4};
padding: ${theme.gridUnit * 2}px;
width: ${theme.gridUnit * 8}px;
}
.callpase-icon > svg {
color: ${theme.colors.primary.base};
}
`};
`;
const getWindowSize = () => ({
@ -230,7 +258,7 @@ function ExploreViewContainer(props) {
const theme = useTheme();
const width = `${windowSize.width}px`;
const navHeight = props.standalone ? 0 : 90;
const navHeight = props.standalone ? 0 : 120;
const height = props.forcedHeight
? `${props.forcedHeight}px`
: `${windowSize.height - navHeight}px`;
@ -515,144 +543,164 @@ function ExploreViewContainer(props) {
}
return (
<Styles id="explore-container" height={height}>
<Global
styles={css`
.navbar {
margin-bottom: 0;
}
body {
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
#app-menu,
#app {
flex: 1 1 auto;
}
#app {
flex-basis: 100%;
overflow: hidden;
height: 100%;
}
#app-menu {
flex-shrink: 0;
}
`}
/>
{showingModal && (
<SaveModal
onHide={toggleModal}
<ExploreContainer>
<ExploreHeaderContainer>
<ConnectedExploreChartHeader
ownState={props.ownState}
actions={props.actions}
form_data={props.form_data}
sliceName={props.sliceName}
canOverwrite={props.can_overwrite}
canDownload={props.can_download}
dashboardId={props.dashboardId}
isStarred={props.isStarred}
slice={props.slice}
sliceName={props.sliceName}
table_name={props.table_name}
formData={props.form_data}
timeout={props.timeout}
chart={props.chart}
user={props.user}
reports={props.reports}
/>
)}
<Resizable
onResizeStop={(evt, direction, ref, d) => {
setShouldForceUpdate(d?.width);
setSidebarWidths(LocalStorageKeys.datasource_width, d);
}}
defaultSize={{
width: getSidebarWidths(LocalStorageKeys.datasource_width),
height: '100%',
}}
minWidth={defaultSidebarsWidth[LocalStorageKeys.datasource_width]}
maxWidth="33%"
enable={{ right: true }}
className={
isCollapsed ? 'no-show' : 'explore-column data-source-selection'
}
>
<div className="title-container">
<span className="horizont al-text">{t('Dataset')}</span>
<span
role="button"
tabIndex={0}
className="action-button"
onClick={toggleCollapse}
>
<Icons.Expand
className="collapse-icon"
iconColor={theme.colors.primary.base}
iconSize="l"
/>
</span>
</div>
<DataSourcePanel
datasource={props.datasource}
controls={props.controls}
actions={props.actions}
shouldForceUpdate={shouldForceUpdate}
</ExploreHeaderContainer>
<ExplorePanelContainer id="explore-container">
<Global
styles={css`
.navbar {
margin-bottom: 0;
}
body {
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
#app-menu,
#app {
flex: 1 1 auto;
}
#app {
flex-basis: 100%;
overflow: hidden;
height: 100%;
}
#app-menu {
flex-shrink: 0;
}
`}
/>
</Resizable>
{isCollapsed ? (
<div
className="sidebar"
onClick={toggleCollapse}
data-test="open-datasource-tab"
role="button"
tabIndex={0}
{showingModal && (
<SaveModal
onHide={toggleModal}
actions={props.actions}
form_data={props.form_data}
sliceName={props.sliceName}
dashboardId={props.dashboardId}
/>
)}
<Resizable
onResizeStop={(evt, direction, ref, d) => {
setShouldForceUpdate(d?.width);
setSidebarWidths(LocalStorageKeys.datasource_width, d);
}}
defaultSize={{
width: getSidebarWidths(LocalStorageKeys.datasource_width),
height: '100%',
}}
minWidth={defaultSidebarsWidth[LocalStorageKeys.datasource_width]}
maxWidth="33%"
enable={{ right: true }}
className={
isCollapsed ? 'no-show' : 'explore-column data-source-selection'
}
>
<span role="button" tabIndex={0} className="action-button">
<Tooltip title={t('Open Datasource tab')}>
<Icons.Collapse
<div className="title-container">
<span className="horizont al-text">{t('Dataset')}</span>
<span
role="button"
tabIndex={0}
className="action-button"
onClick={toggleCollapse}
>
<Icons.Expand
className="collapse-icon"
iconColor={theme.colors.primary.base}
iconSize="l"
/>
</Tooltip>
</span>
<Icons.DatasetPhysical
css={{ marginTop: theme.gridUnit * 2 }}
iconSize="l"
iconColor={theme.colors.grayscale.base}
</span>
</div>
<DataSourcePanel
datasource={props.datasource}
controls={props.controls}
actions={props.actions}
shouldForceUpdate={shouldForceUpdate}
/>
</Resizable>
{isCollapsed ? (
<div
className="sidebar"
onClick={toggleCollapse}
data-test="open-datasource-tab"
role="button"
tabIndex={0}
>
<span role="button" tabIndex={0} className="action-button">
<Tooltip title={t('Open Datasource tab')}>
<Icons.Collapse
className="collapse-icon"
iconColor={theme.colors.primary.base}
iconSize="l"
/>
</Tooltip>
</span>
<Icons.DatasetPhysical
css={{ marginTop: theme.gridUnit * 2 }}
iconSize="l"
iconColor={theme.colors.grayscale.base}
/>
</div>
) : null}
<Resizable
onResizeStop={(evt, direction, ref, d) =>
setSidebarWidths(LocalStorageKeys.controls_width, d)
}
defaultSize={{
width: getSidebarWidths(LocalStorageKeys.controls_width),
height: '100%',
}}
minWidth={defaultSidebarsWidth[LocalStorageKeys.controls_width]}
maxWidth="33%"
enable={{ right: true }}
className="col-sm-3 explore-column controls-column"
>
<QueryAndSaveBtns
canAdd={!!(props.can_add || props.can_overwrite)}
onQuery={onQuery}
onSave={toggleModal}
onStop={onStop}
loading={props.chart.chartStatus === 'loading'}
chartIsStale={chartIsStale}
errorMessage={renderErrorMessage()}
datasourceType={props.datasource_type}
/>
<ConnectedControlPanelsContainer
exploreState={props.exploreState}
actions={props.actions}
form_data={props.form_data}
controls={props.controls}
chart={props.chart}
datasource_type={props.datasource_type}
isDatasourceMetaLoading={props.isDatasourceMetaLoading}
/>
</Resizable>
<div
className={cx(
'main-explore-content',
isCollapsed ? 'col-sm-9' : 'col-sm-7',
)}
>
{renderChartContainer()}
</div>
) : null}
<Resizable
onResizeStop={(evt, direction, ref, d) =>
setSidebarWidths(LocalStorageKeys.controls_width, d)
}
defaultSize={{
width: getSidebarWidths(LocalStorageKeys.controls_width),
height: '100%',
}}
minWidth={defaultSidebarsWidth[LocalStorageKeys.controls_width]}
maxWidth="33%"
enable={{ right: true }}
className="col-sm-3 explore-column controls-column"
>
<QueryAndSaveBtns
canAdd={!!(props.can_add || props.can_overwrite)}
onQuery={onQuery}
onSave={toggleModal}
onStop={onStop}
loading={props.chart.chartStatus === 'loading'}
chartIsStale={chartIsStale}
errorMessage={renderErrorMessage()}
datasourceType={props.datasource_type}
/>
<ConnectedControlPanelsContainer
exploreState={props.exploreState}
actions={props.actions}
form_data={props.form_data}
controls={props.controls}
chart={props.chart}
datasource_type={props.datasource_type}
isDatasourceMetaLoading={props.isDatasourceMetaLoading}
/>
</Resizable>
<div
className={cx(
'main-explore-content',
isCollapsed ? 'col-sm-9' : 'col-sm-7',
)}
>
{renderChartContainer()}
</div>
</Styles>
</ExplorePanelContainer>
</ExploreContainer>
);
}

View File

@ -59,7 +59,8 @@ const Styles = styled.div`
justify-content: space-between;
align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
padding: ${({ theme }) => 2 * theme.gridUnit}px;
padding: ${({ theme }) => 4 * theme.gridUnit}px;
padding-right: ${({ theme }) => 2 * theme.gridUnit}px;
}
.error-alert {
margin: ${({ theme }) => 2 * theme.gridUnit}px;