This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 602afbaa31 feat(explore): Move chart header to top of the page (#19529)
602afbaa31 is described below
commit 602afbaa31d72eefd213d85649eee494e72add7a
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Tue Apr 5 15:20:29 2022 +0200
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
---
.../src/components/FaveStar/index.tsx | 10 +-
.../explore/components/DatasourcePanel/index.tsx | 2 +-
.../ChartEditableTitle/ChartEditableTitle.test.tsx | 68 ++++
.../ChartEditableTitle/index.tsx | 213 +++++++++++
.../components/ExploreChartHeader/index.jsx | 135 +++----
.../src/explore/components/ExploreChartPanel.jsx | 51 +--
.../components/ExploreViewContainer/index.jsx | 420 ++++++++++++---------
.../controls/DatasourceControl/index.jsx | 3 +-
8 files changed, 602 insertions(+), 300 deletions(-)
diff --git a/superset-frontend/src/components/FaveStar/index.tsx
b/superset-frontend/src/components/FaveStar/index.tsx
index ac5bb6065e..5953075854 100644
--- a/superset-frontend/src/components/FaveStar/index.tsx
+++ b/superset-frontend/src/components/FaveStar/index.tsx
@@ -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 = ({
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
index ebed661be9..c38c1b59ae 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
@@ -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;
}
diff --git
a/superset-frontend/src/explore/components/ExploreChartHeader/ChartEditableTitle/ChartEditableTitle.test.tsx
b/superset-frontend/src/explore/components/ExploreChartHeader/ChartEditableTitle/ChartEditableTitle.test.tsx
new file mode 100644
index 0000000000..dd98518c8c
--- /dev/null
+++
b/superset-frontend/src/explore/components/ExploreChartHeader/ChartEditableTitle/ChartEditableTitle.test.tsx
@@ -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();
+ });
+});
diff --git
a/superset-frontend/src/explore/components/ExploreChartHeader/ChartEditableTitle/index.tsx
b/superset-frontend/src/explore/components/ExploreChartHeader/ChartEditableTitle/index.tsx
new file mode 100644
index 0000000000..0e2761b6a9
--- /dev/null
+++
b/superset-frontend/src/explore/components/ExploreChartHeader/ChartEditableTitle/index.tsx
@@ -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,
+ ' ',
+ );
+ }
+ }, [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>
+ );
+};
diff --git
a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index 665a2512ef..d9d615dc1f 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -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;
- }
+ span[role='button'] {
+ display: flex;
+ height: 100%;
+ }
- .right-button-panel {
- display: flex;
- align-items: center;
+ .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 }) => theme.gridUnit}px;
+ > .btn-group {
+ flex: 0 0 auto;
+ margin-left: ${theme.gridUnit}px;
+ }
}
- }
- .action-button {
- color: ${({ theme }) => theme.colors.grayscale.base};
- margin: 0 ${({ theme }) => theme.gridUnit * 1.5}px 0
- ${({ theme }) => 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()}
/>
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx
b/superset-frontend/src/explore/components/ExploreChartPanel.jsx
index 2067d853c7..61d03101bf 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx
@@ -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
) : (
diff --git
a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index 527392275c..57437ea99f 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -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>
);
}
diff --git
a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index f7cda8b752..b011901eb0 100644
---
a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++
b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -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;