Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
View.jsx 12.32 KiB
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";

import cx from "classnames";

import { isReducedMotionPreferred } from "metabase/lib/dom";

import ExplicitSize from "metabase/components/ExplicitSize";
import Popover from "metabase/components/Popover";
import DebouncedFrame from "metabase/components/DebouncedFrame";
import Subhead from "metabase/components/type/Subhead";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";

import NativeQueryEditor from "../NativeQueryEditor";
import QueryVisualization from "../QueryVisualization";
import DataReference from "../dataref/DataReference";
import TagEditorSidebar from "../template_tags/TagEditorSidebar";
import SnippetSidebar from "../template_tags/SnippetSidebar";
import SavedQuestionIntroModal from "../SavedQuestionIntroModal";

import AggregationPopover from "../AggregationPopover";
import BreakoutPopover from "../BreakoutPopover";

import QueryModals from "../QueryModals";
import { ViewTitleHeader, ViewSubHeader } from "./ViewHeader";
import NewQuestionHeader from "./NewQuestionHeader";
import ViewFooter from "./ViewFooter";
import ViewSidebar from "./ViewSidebar";
import QuestionDataSelector from "./QuestionDataSelector";

import ChartSettingsSidebar from "./sidebars/ChartSettingsSidebar";
import ChartTypeSidebar from "./sidebars/ChartTypeSidebar";
import SummarizeSidebar from "./sidebars/SummarizeSidebar";
import FilterSidebar from "./sidebars/FilterSidebar";
import QuestionDetailsSidebar from "./sidebars/QuestionDetailsSidebar";

import Notebook from "../notebook/Notebook";
import { Motion, spring } from "react-motion";

import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";

const DEFAULT_POPOVER_STATE = {
  aggregationIndex: null,
  aggregationPopoverTarget: null,
  breakoutIndex: null,
  breakoutPopoverTarget: null,
};

@ExplicitSize()
export default class View extends React.Component {
  state = {
    ...DEFAULT_POPOVER_STATE,
  };

  handleAddSeries = e => {
    this.setState({
      ...DEFAULT_POPOVER_STATE,
      aggregationPopoverTarget: e.target,
    });
  };
  handleEditSeries = (e, index) => {
    this.setState({
      ...DEFAULT_POPOVER_STATE,
      aggregationPopoverTarget: e.target,
      aggregationIndex: index,
    });
  };
  handleRemoveSeries = (e, index) => {
    const { query } = this.props;
    query.removeAggregation(index).update(null, { run: true });
  };
  handleEditBreakout = (e, index) => {
    this.setState({
      ...DEFAULT_POPOVER_STATE,
      breakoutPopoverTarget: e.target,
      breakoutIndex: index,
    });
  };
  handleClosePopover = () => {
    this.setState({
      ...DEFAULT_POPOVER_STATE,
    });
  };

  render() {
    const {
      question,
      query,
      card,
      isDirty,
      isResultDirty,
      isLiveResizable,
      runQuestionQuery,
      databases,
      isShowingTemplateTagsEditor,
      isShowingDataReference,
      isShowingNewbModal,
      isShowingChartTypeSidebar,
      isShowingChartSettingsSidebar,
      isShowingSummarySidebar,
      isShowingFilterSidebar,
      isShowingSnippetSidebar,
      isShowingQuestionDetailsSidebar,
      queryBuilderMode,
      mode,
      fitClassNames,
      height,
      onOpenModal,
    } = this.props;
    const {
      aggregationIndex,
      aggregationPopoverTarget,
      breakoutIndex,
      breakoutPopoverTarget,
    } = this.state;

    // if we don't have a card at all or no databases then we are initializing, so keep it simple
    if (!card || !databases) {
      return <LoadingAndErrorWrapper className={fitClassNames} loading />;
    }
    const queryMode = mode && mode.queryMode();
    const ModeFooter = queryMode && queryMode.ModeFooter;
    const isStructured = query instanceof StructuredQuery;
    const isNative = query instanceof NativeQuery;

    const isNewQuestion =
      query instanceof StructuredQuery &&
      !query.sourceTableId() &&
      !query.sourceQuery();

    if (isNewQuestion && queryBuilderMode === "view") {
      return (
        <div className={fitClassNames}>
          <div className="p4 mx2">
            <QuestionDataSelector
              query={query}
              triggerElement={
                <Subhead className="mb2">{t`Pick your data`}</Subhead>
              }
            />
          </div>
        </div>
      );
    }

    const topQuery = isStructured && query.topLevelQuery();

    // only allow editing of series for structured queries
    const onAddSeries = topQuery ? this.handleAddSeries : null;
    const onEditSeries = topQuery ? this.handleEditSeries : null;
    const onRemoveSeries =
      topQuery && topQuery.hasAggregations() ? this.handleRemoveSeries : null;
    const onEditBreakout =
      topQuery && topQuery.hasBreakouts() ? this.handleEditBreakout : null;

    const leftSideBar = isShowingChartSettingsSidebar ? (
      <ChartSettingsSidebar
        {...this.props}
        onClose={this.props.onCloseChartSettings}
      />
    ) : isShowingChartTypeSidebar ? (
      <ChartTypeSidebar {...this.props} onClose={this.props.onCloseChartType} />
    ) : isShowingQuestionDetailsSidebar ? (
      <QuestionDetailsSidebar question={question} onOpenModal={onOpenModal} />
    ) : null;

    const rightSideBar =
      isStructured && isShowingSummarySidebar ? (
        <SummarizeSidebar
          question={question}
          onClose={this.props.onCloseSummary}
          isResultDirty={isResultDirty}
          runQuestionQuery={runQuestionQuery}
        />
      ) : isStructured && isShowingFilterSidebar ? (
        <FilterSidebar question={question} onClose={this.props.onCloseFilter} />
      ) : isNative && isShowingTemplateTagsEditor ? (
        <TagEditorSidebar
          {...this.props}
          onClose={this.props.toggleTemplateTagsEditor}
        />
      ) : isNative && isShowingDataReference ? (
        <DataReference
          {...this.props}
          onClose={this.props.toggleDataReference}
        />
      ) : isNative && isShowingSnippetSidebar ? (
        <SnippetSidebar
          {...this.props}
          onClose={this.props.toggleSnippetSidebar}
        />
      ) : null;

    const isSidebarOpen = leftSideBar || rightSideBar;

    const MOTION_Y = -100;

    const preferReducedMotion = isReducedMotionPreferred();

    const springOpts = preferReducedMotion
      ? { stiffness: 500 }
      : { stiffness: 170 };

    return (
      <div className={fitClassNames}>
        <div className={cx("QueryBuilder flex flex-column bg-white spread")}>
          <Motion
            defaultStyle={isNewQuestion ? { opacity: 0 } : { opacity: 1 }}
            style={
              isNewQuestion ? { opacity: spring(0) } : { opacity: spring(1) }
            }
          >
            {({ opacity }) => (
              <div className="flex-no-shrink z3 bg-white relative">
                <ViewTitleHeader
                  {...this.props}
                  style={{ opacity }}
                  py={1}
                  className="border-bottom"
                />
                {opacity < 1 && (
                  <NewQuestionHeader
                    className="spread"
                    style={{ opacity: 1 - opacity }}
                  />
                )}
              </div>
            )}
          </Motion>

          <div className="flex flex-full relative">
            {query instanceof StructuredQuery && (
              <Motion
                defaultStyle={
                  isNewQuestion
                    ? { opacity: 1, translateY: 0 }
                    : { opacity: 0, translateY: MOTION_Y }
                }
                style={
                  queryBuilderMode === "notebook"
                    ? {
                        opacity: spring(1, springOpts),
                        translateY: spring(0, springOpts),
                      }
                    : {
                        opacity: spring(0, springOpts),
                        translateY: spring(MOTION_Y, springOpts),
                      }
                }
              >
                {({ opacity, translateY }) => {
                  const snapY = translateY < MOTION_Y / 2 ? MOTION_Y : 0;
                  const shiftY = preferReducedMotion ? snapY : translateY;
                  return opacity > 0 ? (
                    // note the `bg-white class here is necessary to obscure the other layer
                    <div
                      className="spread bg-white scroll-y z2 border-top border-bottom"
                      style={{
                        // opacity: opacity,
                        transform: `translateY(${shiftY}%)`,
                      }}
                    >
                      <Notebook {...this.props} />
                    </div>
                  ) : null;
                }}
              </Motion>
            )}

            <ViewSidebar side="left" isOpen={!!leftSideBar}>
              {leftSideBar}
            </ViewSidebar>

            <div
              className={cx("flex-full flex flex-column flex-basis-none", {
                "hide sm-show": isSidebarOpen,
              })}
            >
              {isNative && (
                <div className="z2 hide sm-show border-bottom mb2">
                  <NativeQueryEditor
                    {...this.props}
                    viewHeight={height}
                    isOpen={!card.dataset_query.native.query || isDirty}
                    datasetQuery={card && card.dataset_query}
                  />
                </div>
              )}

              <ViewSubHeader {...this.props} />

              <DebouncedFrame
                className="flex-full"
                style={{ flexGrow: 1 }}
                enabled={!isLiveResizable}
              >
                <QueryVisualization
                  {...this.props}
                  onAddSeries={onAddSeries}
                  onEditSeries={onEditSeries}
                  onRemoveSeries={onRemoveSeries}
                  onEditBreakout={onEditBreakout}
                  noHeader
                  className="spread"
                />
              </DebouncedFrame>

              {ModeFooter && (
                <ModeFooter {...this.props} className="flex-no-shrink" />
              )}

              <ViewFooter {...this.props} className="flex-no-shrink" />
            </div>

            <ViewSidebar side="right" isOpen={!!rightSideBar}>
              {rightSideBar}
            </ViewSidebar>
          </div>
        </div>

        {isShowingNewbModal && (
          <SavedQuestionIntroModal
            onClose={() => this.props.closeQbNewbModal()}
          />
        )}

        <QueryModals {...this.props} />

        {isStructured && (
          <Popover
            isOpen={!!aggregationPopoverTarget}
            target={aggregationPopoverTarget}
            onClose={this.handleClosePopover}
          >
            <AggregationPopover
              query={query}
              aggregation={
                aggregationIndex >= 0
                  ? query.aggregations()[aggregationIndex]
                  : 0
              }
              onChangeAggregation={aggregation => {
                if (aggregationIndex != null) {
                  query
                    .updateAggregation(aggregationIndex, aggregation)
                    .update(null, { run: true });
                } else {
                  query.aggregate(aggregation).update(null, { run: true });
                }
                this.handleClosePopover();
              }}
              onClose={this.handleClosePopover}
            />
          </Popover>
        )}
        {isStructured && (
          <Popover
            isOpen={!!breakoutPopoverTarget}
            onClose={this.handleClosePopover}
            target={breakoutPopoverTarget}
          >
            <BreakoutPopover
              query={query}
              breakout={
                breakoutIndex >= 0 ? query.breakouts()[breakoutIndex] : 0
              }
              onChangeBreakout={breakout => {
                if (breakoutIndex != null) {
                  query
                    .updateBreakout(breakoutIndex, breakout)
                    .update(null, { run: true });
                } else {
                  query.breakout(breakout).update(null, { run: true });
                }
                this.handleClosePopover();
              }}
              onClose={this.handleClosePopover}
            />
          </Popover>
        )}
      </div>
    );
  }
}