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,
+        '&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>
+  );
+};
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;

Reply via email to