From ea8328f2bb3e892aa7d7d0ab6c7bcb77ca09ebd6 Mon Sep 17 00:00:00 2001 From: Oisin Coveney <oisin@metabase.com> Date: Fri, 25 Oct 2024 09:09:48 +0300 Subject: [PATCH] Split View components into multiple files (#49034) --- .../query_builder/components/view/View.jsx | 683 ------------------ .../NativeQueryRightSidebar.jsx | 75 ++ .../View/NativeQueryRightSidebar/index.ts | 1 + .../StructuredQueryRightSidebar.jsx | 90 +++ .../View/StructuredQueryRightSidebar/index.ts | 1 + .../components/view/View/View.jsx | 299 ++++++++ .../view/{ => View}/View.styled.tsx | 2 +- .../ViewHeaderContainer.jsx | 53 ++ .../view/View/ViewHeaderContainer/index.ts | 1 + .../ViewLeftSidebarContainer.jsx | 28 + .../View/ViewLeftSidebarContainer/index.ts | 1 + .../ViewMainContainer/ViewMainContainer.jsx | 67 ++ .../view/View/ViewMainContainer/index.ts | 1 + .../ViewNativeQueryEditor.jsx | 45 ++ .../view/View/ViewNativeQueryEditor/index.ts | 1 + .../ViewRightSidebarContainer.jsx | 56 ++ .../View/ViewRightSidebarContainer/index.ts | 1 + .../components/view/View/index.ts | 1 + .../query_builder/containers/QueryBuilder.jsx | 2 +- 19 files changed, 723 insertions(+), 685 deletions(-) delete mode 100644 frontend/src/metabase/query_builder/components/view/View.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/NativeQueryRightSidebar.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/StructuredQueryRightSidebar.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/View.jsx rename frontend/src/metabase/query_builder/components/view/{ => View}/View.styled.tsx (97%) create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/ViewHeaderContainer.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/ViewLeftSidebarContainer.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/ViewMainContainer.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/ViewNativeQueryEditor.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/ViewRightSidebarContainer.jsx create mode 100644 frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/index.ts create mode 100644 frontend/src/metabase/query_builder/components/view/View/index.ts diff --git a/frontend/src/metabase/query_builder/components/view/View.jsx b/frontend/src/metabase/query_builder/components/view/View.jsx deleted file mode 100644 index b8e5f2a1413..00000000000 --- a/frontend/src/metabase/query_builder/components/view/View.jsx +++ /dev/null @@ -1,683 +0,0 @@ -/* eslint-disable react/prop-types */ - -import { connect } from "react-redux"; -import { match } from "ts-pattern"; -import { t } from "ttag"; -import _ from "underscore"; - -import { deletePermanently } from "metabase/archive/actions"; -import { ArchivedEntityBanner } from "metabase/archive/components/ArchivedEntityBanner"; -import ExplicitSize from "metabase/components/ExplicitSize"; -import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; -import Toaster from "metabase/components/Toaster"; -import CS from "metabase/css/core/index.css"; -import QueryBuilderS from "metabase/css/query_builder.module.css"; -import Bookmarks from "metabase/entities/bookmarks"; -import Questions from "metabase/entities/questions"; -import { - rememberLastUsedDatabase, - setArchivedQuestion, -} from "metabase/query_builder/actions"; -import { SIDEBAR_SIZES } from "metabase/query_builder/constants"; -import { TimeseriesChrome } from "metabase/querying/filters/components/TimeseriesChrome"; -import { MetricEditor } from "metabase/querying/metrics/components/MetricEditor"; -import { Transition } from "metabase/ui"; -import * as Lib from "metabase-lib"; - -import DatasetEditor from "../DatasetEditor"; -import NativeQueryEditor from "../NativeQueryEditor"; -import { QueryModals } from "../QueryModals"; -import QueryVisualization from "../QueryVisualization"; -import { SavedQuestionIntroModal } from "../SavedQuestionIntroModal"; -import DataReference from "../dataref/DataReference"; -import { SnippetSidebar } from "../template_tags/SnippetSidebar"; -import { TagEditorSidebar } from "../template_tags/TagEditorSidebar"; - -import NewQuestionHeader from "./NewQuestionHeader"; -import { NotebookContainer } from "./View/NotebookContainer"; -import { - BorderedViewTitleHeader, - NativeQueryEditorContainer, - QueryBuilderContentContainer, - QueryBuilderMain, - QueryBuilderViewHeaderContainer, - QueryBuilderViewRoot, - StyledDebouncedFrame, - StyledSyncedParametersList, -} from "./View.styled"; -import { ViewFooter } from "./ViewFooter"; -import ViewSidebar from "./ViewSidebar"; -import { ChartSettingsSidebar } from "./sidebars/ChartSettingsSidebar"; -import { ChartTypeSidebar } from "./sidebars/ChartTypeSidebar"; -import { QuestionInfoSidebar } from "./sidebars/QuestionInfoSidebar"; -import { QuestionSettingsSidebar } from "./sidebars/QuestionSettingsSidebar"; -import { SummarizeSidebar } from "./sidebars/SummarizeSidebar"; -import TimelineSidebar from "./sidebars/TimelineSidebar"; - -const fadeIn = { - in: { opacity: 1 }, - out: { opacity: 0 }, - transitionProperty: "opacity", -}; - -const ViewHeaderContainer = props => { - const { question, onUnarchive, onMove, onDeletePermanently } = props; - const query = question.query(); - const card = question.card(); - const { isNative } = Lib.queryDisplayInfo(query); - - const isNewQuestion = !isNative && Lib.sourceTableOrCardId(query) === null; - - return ( - <QueryBuilderViewHeaderContainer> - {card.archived && ( - <ArchivedEntityBanner - name={card.name} - entityType={card.type} - canWrite={card.can_write} - canRestore={card.can_restore} - canDelete={card.can_delete} - onUnarchive={() => onUnarchive(question)} - onMove={collection => onMove(question, collection)} - onDeletePermanently={() => onDeletePermanently(card.id)} - /> - )} - - <BorderedViewTitleHeader - {...props} - style={{ - transition: "opacity 300ms linear", - opacity: isNewQuestion ? 0 : 1, - }} - /> - {/*This is used so that the New Question Header is unmounted after the animation*/} - <Transition mounted={isNewQuestion} transition={fadeIn} duration={300}> - {style => <NewQuestionHeader className={CS.spread} style={style} />} - </Transition> - </QueryBuilderViewHeaderContainer> - ); -}; - -const ViewMainContainer = props => { - const { - queryBuilderMode, - mode, - question, - showLeftSidebar, - showRightSidebar, - parameters, - setParameterValue, - isLiveResizable, - updateQuestion, - } = props; - - if (queryBuilderMode === "notebook") { - // we need to render main only in view mode - return; - } - - const queryMode = mode && mode.queryMode(); - const { isNative } = Lib.queryDisplayInfo(question.query()); - const isSidebarOpen = showLeftSidebar || showRightSidebar; - - return ( - <QueryBuilderMain - isSidebarOpen={isSidebarOpen} - data-testid="query-builder-main" - > - {isNative ? ( - <ViewNativeQueryEditor {...props} /> - ) : ( - <StyledSyncedParametersList - parameters={parameters} - setParameterValue={setParameterValue} - commitImmediately - /> - )} - - <StyledDebouncedFrame enabled={!isLiveResizable}> - <QueryVisualization - {...props} - noHeader - className={CS.spread} - mode={queryMode} - /> - </StyledDebouncedFrame> - <TimeseriesChrome - question={question} - updateQuestion={updateQuestion} - className={CS.flexNoShrink} - /> - <ViewFooter className={CS.flexNoShrink} /> - </QueryBuilderMain> - ); -}; - -const ViewLeftSidebarContainer = ({ - question, - result, - isShowingChartSettingsSidebar, - isShowingChartTypeSidebar, -}) => - match({ - isShowingChartSettingsSidebar, - isShowingChartTypeSidebar, - }) - .with( - { - isShowingChartSettingsSidebar: true, - }, - () => <ChartSettingsSidebar question={question} result={result} />, - ) - .with( - { - isShowingChartTypeSidebar: true, - }, - () => <ChartTypeSidebar question={question} result={result} />, - ) - .otherwise(() => null); - -const ViewNativeQueryEditor = props => { - const { - question, - height, - isDirty, - isNativeEditorOpen, - card, - setParameterValueToDefault, - onSetDatabaseId, - } = props; - - const legacyQuery = question.legacyQuery(); - - // Normally, when users open native models, - // they open an ad-hoc GUI question using the model as a data source - // (using the `/dataset` endpoint instead of the `/card/:id/query`) - // However, users without data permission open a real model as they can't use the `/dataset` endpoint - // So the model is opened as an underlying native question and the query editor becomes visible - // This check makes it hide the editor in this particular case - // More details: https://github.com/metabase/metabase/pull/20161 - const { isEditable } = Lib.queryDisplayInfo(question.query()); - if (question.type() === "model" && !isEditable) { - return null; - } - - return ( - <NativeQueryEditorContainer> - <NativeQueryEditor - {...props} - query={legacyQuery} - viewHeight={height} - isOpen={legacyQuery.isEmpty() || isDirty} - isInitiallyOpen={isNativeEditorOpen} - datasetQuery={card && card.dataset_query} - setParameterValueToDefault={setParameterValueToDefault} - onSetDatabaseId={onSetDatabaseId} - /> - </NativeQueryEditorContainer> - ); -}; - -const ViewRightSidebarContainer = props => { - const { - question, - deselectTimelineEvents, - hideTimelineEvents, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - isShowingSummarySidebar, - isShowingTimelineSidebar, - onCloseQuestionInfo, - onCloseSummary, - onCloseTimelines, - onOpenModal, - onSave, - selectTimelineEvents, - selectedTimelineEventIds, - showTimelineEvents, - timelines, - updateQuestion, - visibleTimelineEventIds, - xDomain, - } = props; - - const { isNative } = Lib.queryDisplayInfo(question.query()); - - return !isNative ? ( - <StructuredQueryRightSidebar - deselectTimelineEvents={deselectTimelineEvents} - hideTimelineEvents={hideTimelineEvents} - isShowingQuestionInfoSidebar={isShowingQuestionInfoSidebar} - isShowingQuestionSettingsSidebar={isShowingQuestionSettingsSidebar} - isShowingSummarySidebar={isShowingSummarySidebar} - isShowingTimelineSidebar={isShowingTimelineSidebar} - onCloseQuestionInfo={onCloseQuestionInfo} - onCloseSummary={onCloseSummary} - onCloseTimelines={onCloseTimelines} - onOpenModal={onOpenModal} - onSave={onSave} - question={question} - selectTimelineEvents={selectTimelineEvents} - selectedTimelineEventIds={selectedTimelineEventIds} - showTimelineEvents={showTimelineEvents} - timelines={timelines} - updateQuestion={updateQuestion} - visibleTimelineEventIds={visibleTimelineEventIds} - xDomain={xDomain} - /> - ) : ( - <NativeQueryRightSidebar {...props} /> - ); -}; - -const StructuredQueryRightSidebar = ({ - deselectTimelineEvents, - hideTimelineEvents, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - isShowingSummarySidebar, - isShowingTimelineSidebar, - onCloseQuestionInfo, - onCloseSummary, - onCloseTimelines, - onOpenModal, - onSave, - question, - selectTimelineEvents, - selectedTimelineEventIds, - showTimelineEvents, - timelines, - updateQuestion, - visibleTimelineEventIds, - xDomain, -}) => - match({ - isSaved: question.isSaved(), - isShowingSummarySidebar, - isShowingTimelineSidebar, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - }) - .with( - { - isShowingSummarySidebar: true, - }, - () => ( - <SummarizeSidebar - query={question.query()} - onQueryChange={nextQuery => { - const datesetQuery = Lib.toLegacyQuery(nextQuery); - const nextQuestion = question.setDatasetQuery(datesetQuery); - updateQuestion(nextQuestion.setDefaultDisplay(), { - run: true, - }); - }} - onClose={onCloseSummary} - /> - ), - ) - .with({ isShowingTimelineSidebar: true }, () => ( - <TimelineSidebar - question={question} - timelines={timelines} - visibleTimelineEventIds={visibleTimelineEventIds} - selectedTimelineEventIds={selectedTimelineEventIds} - xDomain={xDomain} - onShowTimelineEvents={showTimelineEvents} - onHideTimelineEvents={hideTimelineEvents} - onSelectTimelineEvents={selectTimelineEvents} - onDeselectTimelineEvents={deselectTimelineEvents} - onOpenModal={onOpenModal} - onClose={onCloseTimelines} - /> - )) - .with( - { - isSaved: true, - isShowingQuestionInfoSidebar: true, - }, - () => ( - <QuestionInfoSidebar - question={question} - onSave={onSave} - onClose={onCloseQuestionInfo} - /> - ), - ) - .with( - { - isSaved: true, - isShowingQuestionSettingsSidebar: true, - }, - () => <QuestionSettingsSidebar question={question} />, - ) - .otherwise(() => null); - -const NativeQueryRightSidebar = props => { - const { - question, - toggleTemplateTagsEditor, - toggleDataReference, - toggleSnippetSidebar, - showTimelineEvent, - showTimelineEvents, - hideTimelineEvents, - selectTimelineEvents, - deselectTimelineEvents, - onCloseTimelines, - onSave, - onCloseQuestionInfo, - isShowingTemplateTagsEditor, - isShowingDataReference, - isShowingSnippetSidebar, - isShowingTimelineSidebar, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - } = props; - - return match({ - isShowingTemplateTagsEditor, - isShowingDataReference, - isShowingSnippetSidebar, - isShowingTimelineSidebar, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - }) - .with({ isShowingTemplateTagsEditor: true }, () => ( - <TagEditorSidebar - {...props} - query={question.legacyQuery()} - onClose={toggleTemplateTagsEditor} - /> - )) - .with({ isShowingDataReference: true }, () => ( - <DataReference {...props} onClose={toggleDataReference} /> - )) - .with({ isShowingSnippetSidebar: true }, () => ( - <SnippetSidebar {...props} onClose={toggleSnippetSidebar} /> - )) - .with({ isShowingTimelineSidebar: true }, () => ( - <TimelineSidebar - {...props} - onShowTimelineEvent={showTimelineEvent} - onShowTimelineEvents={showTimelineEvents} - onHideTimelineEvents={hideTimelineEvents} - onSelectTimelineEvents={selectTimelineEvents} - onDeselectTimelineEvents={deselectTimelineEvents} - onClose={onCloseTimelines} - /> - )) - .with({ isShowingQuestionInfoSidebar: true }, () => ( - <QuestionInfoSidebar - question={question} - onSave={onSave} - onClose={onCloseQuestionInfo} - /> - )) - .with({ isShowingQuestionSettingsSidebar: true }, () => ( - <QuestionSettingsSidebar question={question} /> - )) - .otherwise(() => null); -}; - -const View = props => { - const { - question, - result, - rawSeries, - databases, - isShowingNewbModal, - isShowingTimelineSidebar, - queryBuilderMode, - closeQbNewbModal, - onDismissToast, - onConfirmToast, - isShowingToaster, - isHeaderVisible, - updateQuestion, - reportTimezone, - readOnly, - isDirty, - isRunning, - isRunnable, - isResultDirty, - hasVisualizeButton, - runQuestionQuery, - cancelQuery, - setQueryBuilderMode, - runDirtyQuestionQuery, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - cancelQuestionChanges, - onCreate, - onSave, - onChangeLocation, - questionAlerts, - user, - modal, - modalContext, - card, - onCloseModal, - onOpenModal, - originalQuestion, - isShowingChartSettingsSidebar, - isShowingChartTypeSidebar, - onCloseChartSettings, - addField, - initialChartSetting, - onReplaceAllVisualizationSettings, - onOpenChartType, - visualizationSettings, - showSidebarTitle, - isShowingSummarySidebar, - isShowingTemplateTagsEditor, - isShowingDataReference, - isShowingSnippetSidebar, - } = props; - - // if we don't have a question at all or no databases then we are initializing, so keep it simple - if (!question || !databases) { - return <LoadingAndErrorWrapper className={CS.fullHeight} loading />; - } - - const query = question.query(); - const { isNative } = Lib.queryDisplayInfo(question.query()); - - const isNewQuestion = !isNative && Lib.sourceTableOrCardId(query) === null; - const isModel = question.type() === "model"; - const isMetric = question.type() === "metric"; - - if ((isModel || isMetric) && queryBuilderMode === "dataset") { - return ( - <> - {isModel && <DatasetEditor {...props} />} - {isMetric && ( - <MetricEditor - question={question} - result={result} - rawSeries={rawSeries} - reportTimezone={reportTimezone} - isDirty={isDirty} - isResultDirty={isResultDirty} - isRunning={isRunning} - onChange={updateQuestion} - onCreate={async question => { - await onCreate(question); - setQueryBuilderMode("view"); - }} - onSave={async question => { - await onSave(question); - setQueryBuilderMode("view"); - }} - onCancel={question => { - if (question.isSaved()) { - cancelQuestionChanges(); - runDirtyQuestionQuery(); - setQueryBuilderMode("view"); - } else { - onChangeLocation("/"); - } - }} - onRunQuery={runQuestionQuery} - onCancelQuery={cancelQuery} - /> - )} - <QueryModals - questionAlerts={questionAlerts} - user={user} - onSave={onSave} - onCreate={onCreate} - updateQuestion={updateQuestion} - modal={modal} - modalContext={modalContext} - card={card} - question={question} - onCloseModal={onCloseModal} - onOpenModal={onOpenModal} - setQueryBuilderMode={setQueryBuilderMode} - originalQuestion={originalQuestion} - onChangeLocation={onChangeLocation} - /> - </> - ); - } - - const isNotebookContainerOpen = - isNewQuestion || queryBuilderMode === "notebook"; - - const showLeftSidebar = - isShowingChartSettingsSidebar || isShowingChartTypeSidebar; - const showRightSidebar = - isShowingTimelineSidebar || - isShowingQuestionInfoSidebar || - isShowingQuestionSettingsSidebar || - (!isNative && isShowingSummarySidebar) || - (isNative && - (isShowingTemplateTagsEditor || - isShowingDataReference || - isShowingSnippetSidebar)); - - const rightSidebarWidth = match({ - isShowingTimelineSidebar, - isShowingQuestionInfoSidebar, - isShowingQuestionSettingsSidebar, - }) - .with({ isShowingTimelineSidebar: true }, () => SIDEBAR_SIZES.TIMELINE) - .with({ isShowingQuestionInfoSidebar: true }, () => 0) - .with({ isShowingQuestionSettingsSidebar: true }, () => 0) - .otherwise(() => SIDEBAR_SIZES.NORMAL); - - return ( - <div className={CS.fullHeight}> - <QueryBuilderViewRoot - className={QueryBuilderS.QueryBuilder} - data-testid="query-builder-root" - > - {isHeaderVisible && <ViewHeaderContainer {...props} />} - - <QueryBuilderContentContainer> - {!isNative && ( - <NotebookContainer - isOpen={isNotebookContainerOpen} - updateQuestion={updateQuestion} - reportTimezone={reportTimezone} - readOnly={readOnly} - question={question} - isDirty={isDirty} - isRunnable={isRunnable} - isResultDirty={isResultDirty} - hasVisualizeButton={hasVisualizeButton} - runQuestionQuery={runQuestionQuery} - setQueryBuilderMode={setQueryBuilderMode} - /> - )} - <ViewSidebar side="left" isOpen={showLeftSidebar}> - <ViewLeftSidebarContainer - question={question} - result={result} - isShowingChartSettingsSidebar={isShowingChartSettingsSidebar} - isShowingChartTypeSidebar={isShowingChartTypeSidebar} - onCloseChartSettings={onCloseChartSettings} - addField={addField} - initialChartSetting={initialChartSetting} - onReplaceAllVisualizationSettings={ - onReplaceAllVisualizationSettings - } - onOpenChartType={onOpenChartType} - visualizationSettings={visualizationSettings} - showSidebarTitle={showSidebarTitle} - /> - </ViewSidebar> - <ViewMainContainer - showLeftSidebar={showLeftSidebar} - showRightSidebar={showRightSidebar} - {...props} - /> - <ViewSidebar - side="right" - isOpen={showRightSidebar} - width={rightSidebarWidth} - > - <ViewRightSidebarContainer {...props} /> - </ViewSidebar> - </QueryBuilderContentContainer> - </QueryBuilderViewRoot> - - {isShowingNewbModal && ( - <SavedQuestionIntroModal - question={question} - isShowingNewbModal={isShowingNewbModal} - onClose={() => closeQbNewbModal()} - /> - )} - - <QueryModals - questionAlerts={questionAlerts} - user={user} - onSave={onSave} - onCreate={onCreate} - updateQuestion={updateQuestion} - modal={modal} - modalContext={modalContext} - card={card} - question={question} - onCloseModal={onCloseModal} - onOpenModal={onOpenModal} - setQueryBuilderMode={setQueryBuilderMode} - originalQuestion={originalQuestion} - onChangeLocation={onChangeLocation} - /> - - <Toaster - message={t`Would you like to be notified when this question is done loading?`} - isShown={isShowingToaster} - onDismiss={onDismissToast} - onConfirm={onConfirmToast} - fixed - /> - </div> - ); -}; - -const mapDispatchToProps = dispatch => ({ - onSetDatabaseId: id => dispatch(rememberLastUsedDatabase(id)), - onUnarchive: async question => { - await dispatch(setArchivedQuestion(question, false)); - await dispatch(Bookmarks.actions.invalidateLists()); - }, - onMove: (question, newCollection) => - dispatch( - Questions.actions.setCollection({ id: question.id() }, newCollection, { - notify: { undo: false }, - }), - ), - onDeletePermanently: id => { - const deleteAction = Questions.actions.delete({ id }); - dispatch(deletePermanently(deleteAction)); - }, -}); - -export default _.compose( - ExplicitSize({ refreshMode: "debounceLeading" }), - connect(null, mapDispatchToProps), -)(View); diff --git a/frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/NativeQueryRightSidebar.jsx b/frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/NativeQueryRightSidebar.jsx new file mode 100644 index 00000000000..6703edbb6ac --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/NativeQueryRightSidebar.jsx @@ -0,0 +1,75 @@ +import { match } from "ts-pattern"; + +import DataReference from "metabase/query_builder/components/dataref/DataReference"; +import { SnippetSidebar } from "metabase/query_builder/components/template_tags/SnippetSidebar"; +import { TagEditorSidebar } from "metabase/query_builder/components/template_tags/TagEditorSidebar"; +import { QuestionInfoSidebar } from "metabase/query_builder/components/view/sidebars/QuestionInfoSidebar"; +import { QuestionSettingsSidebar } from "metabase/query_builder/components/view/sidebars/QuestionSettingsSidebar"; +import TimelineSidebar from "metabase/query_builder/components/view/sidebars/TimelineSidebar"; + +export const NativeQueryRightSidebar = props => { + const { + question, + toggleTemplateTagsEditor, + toggleDataReference, + toggleSnippetSidebar, + showTimelineEvent, + showTimelineEvents, + hideTimelineEvents, + selectTimelineEvents, + deselectTimelineEvents, + onCloseTimelines, + onSave, + onCloseQuestionInfo, + isShowingTemplateTagsEditor, + isShowingDataReference, + isShowingSnippetSidebar, + isShowingTimelineSidebar, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + } = props; + + return match({ + isShowingTemplateTagsEditor, + isShowingDataReference, + isShowingSnippetSidebar, + isShowingTimelineSidebar, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + }) + .with({ isShowingTemplateTagsEditor: true }, () => ( + <TagEditorSidebar + {...props} + query={question.legacyQuery()} + onClose={toggleTemplateTagsEditor} + /> + )) + .with({ isShowingDataReference: true }, () => ( + <DataReference {...props} onClose={toggleDataReference} /> + )) + .with({ isShowingSnippetSidebar: true }, () => ( + <SnippetSidebar {...props} onClose={toggleSnippetSidebar} /> + )) + .with({ isShowingTimelineSidebar: true }, () => ( + <TimelineSidebar + {...props} + onShowTimelineEvent={showTimelineEvent} + onShowTimelineEvents={showTimelineEvents} + onHideTimelineEvents={hideTimelineEvents} + onSelectTimelineEvents={selectTimelineEvents} + onDeselectTimelineEvents={deselectTimelineEvents} + onClose={onCloseTimelines} + /> + )) + .with({ isShowingQuestionInfoSidebar: true }, () => ( + <QuestionInfoSidebar + question={question} + onSave={onSave} + onClose={onCloseQuestionInfo} + /> + )) + .with({ isShowingQuestionSettingsSidebar: true }, () => ( + <QuestionSettingsSidebar question={question} /> + )) + .otherwise(() => null); +}; diff --git a/frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/index.ts b/frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/index.ts new file mode 100644 index 00000000000..088735e9c65 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/NativeQueryRightSidebar/index.ts @@ -0,0 +1 @@ +export * from "./NativeQueryRightSidebar"; diff --git a/frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/StructuredQueryRightSidebar.jsx b/frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/StructuredQueryRightSidebar.jsx new file mode 100644 index 00000000000..4059af6a1d7 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/StructuredQueryRightSidebar.jsx @@ -0,0 +1,90 @@ +import { match } from "ts-pattern"; + +import { QuestionInfoSidebar } from "metabase/query_builder/components/view/sidebars/QuestionInfoSidebar"; +import { QuestionSettingsSidebar } from "metabase/query_builder/components/view/sidebars/QuestionSettingsSidebar"; +import { SummarizeSidebar } from "metabase/query_builder/components/view/sidebars/SummarizeSidebar"; +import TimelineSidebar from "metabase/query_builder/components/view/sidebars/TimelineSidebar"; +import * as Lib from "metabase-lib"; + +export const StructuredQueryRightSidebar = ({ + deselectTimelineEvents, + hideTimelineEvents, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + isShowingSummarySidebar, + isShowingTimelineSidebar, + onCloseQuestionInfo, + onCloseSummary, + onCloseTimelines, + onOpenModal, + onSave, + question, + selectTimelineEvents, + selectedTimelineEventIds, + showTimelineEvents, + timelines, + updateQuestion, + visibleTimelineEventIds, + xDomain, +}) => + match({ + isSaved: question.isSaved(), + isShowingSummarySidebar, + isShowingTimelineSidebar, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + }) + .with( + { + isShowingSummarySidebar: true, + }, + () => ( + <SummarizeSidebar + query={question.query()} + onQueryChange={nextQuery => { + const datesetQuery = Lib.toLegacyQuery(nextQuery); + const nextQuestion = question.setDatasetQuery(datesetQuery); + updateQuestion(nextQuestion.setDefaultDisplay(), { + run: true, + }); + }} + onClose={onCloseSummary} + /> + ), + ) + .with({ isShowingTimelineSidebar: true }, () => ( + <TimelineSidebar + question={question} + timelines={timelines} + visibleTimelineEventIds={visibleTimelineEventIds} + selectedTimelineEventIds={selectedTimelineEventIds} + xDomain={xDomain} + onShowTimelineEvents={showTimelineEvents} + onHideTimelineEvents={hideTimelineEvents} + onSelectTimelineEvents={selectTimelineEvents} + onDeselectTimelineEvents={deselectTimelineEvents} + onOpenModal={onOpenModal} + onClose={onCloseTimelines} + /> + )) + .with( + { + isSaved: true, + isShowingQuestionInfoSidebar: true, + }, + () => ( + <QuestionInfoSidebar + question={question} + onSave={onSave} + onClose={onCloseQuestionInfo} + /> + ), + ) + .with( + { + isSaved: true, + isShowingQuestionSettingsSidebar: true, + }, + () => <QuestionSettingsSidebar question={question} />, + ) + .otherwise(() => null); diff --git a/frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/index.ts b/frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/index.ts new file mode 100644 index 00000000000..e200a841afe --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/StructuredQueryRightSidebar/index.ts @@ -0,0 +1 @@ +export * from "./StructuredQueryRightSidebar"; diff --git a/frontend/src/metabase/query_builder/components/view/View/View.jsx b/frontend/src/metabase/query_builder/components/view/View/View.jsx new file mode 100644 index 00000000000..2d77fc00a51 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/View.jsx @@ -0,0 +1,299 @@ +/* eslint-disable react/prop-types */ + +import { connect } from "react-redux"; +import { match } from "ts-pattern"; +import { t } from "ttag"; +import _ from "underscore"; + +import { deletePermanently } from "metabase/archive/actions"; +import ExplicitSize from "metabase/components/ExplicitSize"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import Toaster from "metabase/components/Toaster"; +import CS from "metabase/css/core/index.css"; +import QueryBuilderS from "metabase/css/query_builder.module.css"; +import Bookmarks from "metabase/entities/bookmarks"; +import Questions from "metabase/entities/questions"; +import { + rememberLastUsedDatabase, + setArchivedQuestion, +} from "metabase/query_builder/actions"; +import { ViewHeaderContainer } from "metabase/query_builder/components/view/View/ViewHeaderContainer/ViewHeaderContainer"; +import { ViewLeftSidebarContainer } from "metabase/query_builder/components/view/View/ViewLeftSidebarContainer/ViewLeftSidebarContainer"; +import { ViewMainContainer } from "metabase/query_builder/components/view/View/ViewMainContainer/ViewMainContainer"; +import { ViewRightSidebarContainer } from "metabase/query_builder/components/view/View/ViewRightSidebarContainer/ViewRightSidebarContainer"; +import { SIDEBAR_SIZES } from "metabase/query_builder/constants"; +import { MetricEditor } from "metabase/querying/metrics/components/MetricEditor"; +import * as Lib from "metabase-lib"; + +import DatasetEditor from "../../DatasetEditor"; +import { QueryModals } from "../../QueryModals"; +import { SavedQuestionIntroModal } from "../../SavedQuestionIntroModal"; +import ViewSidebar from "../ViewSidebar"; + +import { NotebookContainer } from "./NotebookContainer"; +import { + QueryBuilderContentContainer, + QueryBuilderViewRoot, +} from "./View.styled"; + +const ViewInner = props => { + const { + question, + result, + rawSeries, + databases, + isShowingNewbModal, + isShowingTimelineSidebar, + queryBuilderMode, + closeQbNewbModal, + onDismissToast, + onConfirmToast, + isShowingToaster, + isHeaderVisible, + updateQuestion, + reportTimezone, + readOnly, + isDirty, + isRunning, + isRunnable, + isResultDirty, + hasVisualizeButton, + runQuestionQuery, + cancelQuery, + setQueryBuilderMode, + runDirtyQuestionQuery, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + cancelQuestionChanges, + onCreate, + onSave, + onChangeLocation, + questionAlerts, + user, + modal, + modalContext, + card, + onCloseModal, + onOpenModal, + originalQuestion, + isShowingChartSettingsSidebar, + isShowingChartTypeSidebar, + onCloseChartSettings, + addField, + initialChartSetting, + onReplaceAllVisualizationSettings, + onOpenChartType, + visualizationSettings, + showSidebarTitle, + isShowingSummarySidebar, + isShowingTemplateTagsEditor, + isShowingDataReference, + isShowingSnippetSidebar, + } = props; + + // if we don't have a question at all or no databases then we are initializing, so keep it simple + if (!question || !databases) { + return <LoadingAndErrorWrapper className={CS.fullHeight} loading />; + } + + const query = question.query(); + const { isNative } = Lib.queryDisplayInfo(question.query()); + + const isNewQuestion = !isNative && Lib.sourceTableOrCardId(query) === null; + const isModel = question.type() === "model"; + const isMetric = question.type() === "metric"; + + if ((isModel || isMetric) && queryBuilderMode === "dataset") { + return ( + <> + {isModel && <DatasetEditor {...props} />} + {isMetric && ( + <MetricEditor + question={question} + result={result} + rawSeries={rawSeries} + reportTimezone={reportTimezone} + isDirty={isDirty} + isResultDirty={isResultDirty} + isRunning={isRunning} + onChange={updateQuestion} + onCreate={async question => { + await onCreate(question); + setQueryBuilderMode("view"); + }} + onSave={async question => { + await onSave(question); + setQueryBuilderMode("view"); + }} + onCancel={question => { + if (question.isSaved()) { + cancelQuestionChanges(); + runDirtyQuestionQuery(); + setQueryBuilderMode("view"); + } else { + onChangeLocation("/"); + } + }} + onRunQuery={runQuestionQuery} + onCancelQuery={cancelQuery} + /> + )} + <QueryModals + questionAlerts={questionAlerts} + user={user} + onSave={onSave} + onCreate={onCreate} + updateQuestion={updateQuestion} + modal={modal} + modalContext={modalContext} + card={card} + question={question} + onCloseModal={onCloseModal} + onOpenModal={onOpenModal} + setQueryBuilderMode={setQueryBuilderMode} + originalQuestion={originalQuestion} + onChangeLocation={onChangeLocation} + /> + </> + ); + } + + const isNotebookContainerOpen = + isNewQuestion || queryBuilderMode === "notebook"; + + const showLeftSidebar = + isShowingChartSettingsSidebar || isShowingChartTypeSidebar; + const showRightSidebar = + isShowingTimelineSidebar || + isShowingQuestionInfoSidebar || + isShowingQuestionSettingsSidebar || + (!isNative && isShowingSummarySidebar) || + (isNative && + (isShowingTemplateTagsEditor || + isShowingDataReference || + isShowingSnippetSidebar)); + + const rightSidebarWidth = match({ + isShowingTimelineSidebar, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + }) + .with({ isShowingTimelineSidebar: true }, () => SIDEBAR_SIZES.TIMELINE) + .with({ isShowingQuestionInfoSidebar: true }, () => 0) + .with({ isShowingQuestionSettingsSidebar: true }, () => 0) + .otherwise(() => SIDEBAR_SIZES.NORMAL); + + return ( + <div className={CS.fullHeight}> + <QueryBuilderViewRoot + className={QueryBuilderS.QueryBuilder} + data-testid="query-builder-root" + > + {isHeaderVisible && <ViewHeaderContainer {...props} />} + + <QueryBuilderContentContainer> + {!isNative && ( + <NotebookContainer + isOpen={isNotebookContainerOpen} + updateQuestion={updateQuestion} + reportTimezone={reportTimezone} + readOnly={readOnly} + question={question} + isDirty={isDirty} + isRunnable={isRunnable} + isResultDirty={isResultDirty} + hasVisualizeButton={hasVisualizeButton} + runQuestionQuery={runQuestionQuery} + setQueryBuilderMode={setQueryBuilderMode} + /> + )} + <ViewSidebar side="left" isOpen={showLeftSidebar}> + <ViewLeftSidebarContainer + question={question} + result={result} + isShowingChartSettingsSidebar={isShowingChartSettingsSidebar} + isShowingChartTypeSidebar={isShowingChartTypeSidebar} + onCloseChartSettings={onCloseChartSettings} + addField={addField} + initialChartSetting={initialChartSetting} + onReplaceAllVisualizationSettings={ + onReplaceAllVisualizationSettings + } + onOpenChartType={onOpenChartType} + visualizationSettings={visualizationSettings} + showSidebarTitle={showSidebarTitle} + /> + </ViewSidebar> + <ViewMainContainer + showLeftSidebar={showLeftSidebar} + showRightSidebar={showRightSidebar} + {...props} + /> + <ViewSidebar + side="right" + isOpen={showRightSidebar} + width={rightSidebarWidth} + > + <ViewRightSidebarContainer {...props} /> + </ViewSidebar> + </QueryBuilderContentContainer> + </QueryBuilderViewRoot> + + {isShowingNewbModal && ( + <SavedQuestionIntroModal + question={question} + isShowingNewbModal={isShowingNewbModal} + onClose={() => closeQbNewbModal()} + /> + )} + + <QueryModals + questionAlerts={questionAlerts} + user={user} + onSave={onSave} + onCreate={onCreate} + updateQuestion={updateQuestion} + modal={modal} + modalContext={modalContext} + card={card} + question={question} + onCloseModal={onCloseModal} + onOpenModal={onOpenModal} + setQueryBuilderMode={setQueryBuilderMode} + originalQuestion={originalQuestion} + onChangeLocation={onChangeLocation} + /> + + <Toaster + message={t`Would you like to be notified when this question is done loading?`} + isShown={isShowingToaster} + onDismiss={onDismissToast} + onConfirm={onConfirmToast} + fixed + /> + </div> + ); +}; + +const mapDispatchToProps = dispatch => ({ + onSetDatabaseId: id => dispatch(rememberLastUsedDatabase(id)), + onUnarchive: async question => { + await dispatch(setArchivedQuestion(question, false)); + await dispatch(Bookmarks.actions.invalidateLists()); + }, + onMove: (question, newCollection) => + dispatch( + Questions.actions.setCollection({ id: question.id() }, newCollection, { + notify: { undo: false }, + }), + ), + onDeletePermanently: id => { + const deleteAction = Questions.actions.delete({ id }); + dispatch(deletePermanently(deleteAction)); + }, +}); + +export const View = _.compose( + ExplicitSize({ refreshMode: "debounceLeading" }), + connect(null, mapDispatchToProps), +)(ViewInner); diff --git a/frontend/src/metabase/query_builder/components/view/View.styled.tsx b/frontend/src/metabase/query_builder/components/view/View/View.styled.tsx similarity index 97% rename from frontend/src/metabase/query_builder/components/view/View.styled.tsx rename to frontend/src/metabase/query_builder/components/view/View/View.styled.tsx index 5163cb76505..c4d4978f2b5 100644 --- a/frontend/src/metabase/query_builder/components/view/View.styled.tsx +++ b/frontend/src/metabase/query_builder/components/view/View/View.styled.tsx @@ -5,7 +5,7 @@ import DebouncedFrame from "metabase/components/DebouncedFrame"; import { SyncedParametersList } from "metabase/query_builder/components/SyncedParametersList"; import { breakpointMaxSmall } from "metabase/styled-components/theme/media-queries"; -import { ViewTitleHeader } from "./ViewHeader"; +import { ViewTitleHeader } from "../ViewHeader"; export const QueryBuilderViewRoot = styled.div` display: flex; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/ViewHeaderContainer.jsx b/frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/ViewHeaderContainer.jsx new file mode 100644 index 00000000000..d3aaad00943 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/ViewHeaderContainer.jsx @@ -0,0 +1,53 @@ +/* eslint-disable react/prop-types */ +import { ArchivedEntityBanner } from "metabase/archive/components/ArchivedEntityBanner"; +import CS from "metabase/css/core/index.css"; +import NewQuestionHeader from "metabase/query_builder/components/view/NewQuestionHeader"; +import { + BorderedViewTitleHeader, + QueryBuilderViewHeaderContainer, +} from "metabase/query_builder/components/view/View/View.styled"; +import { Transition } from "metabase/ui"; +import * as Lib from "metabase-lib"; + +const fadeIn = { + in: { opacity: 1 }, + out: { opacity: 0 }, + transitionProperty: "opacity", +}; +export const ViewHeaderContainer = props => { + const { question, onUnarchive, onMove, onDeletePermanently } = props; + const query = question.query(); + const card = question.card(); + const { isNative } = Lib.queryDisplayInfo(query); + + const isNewQuestion = !isNative && Lib.sourceTableOrCardId(query) === null; + + return ( + <QueryBuilderViewHeaderContainer> + {card.archived && ( + <ArchivedEntityBanner + name={card.name} + entityType={card.type} + canWrite={card.can_write} + canRestore={card.can_restore} + canDelete={card.can_delete} + onUnarchive={() => onUnarchive(question)} + onMove={collection => onMove(question, collection)} + onDeletePermanently={() => onDeletePermanently(card.id)} + /> + )} + + <BorderedViewTitleHeader + {...props} + style={{ + transition: "opacity 300ms linear", + opacity: isNewQuestion ? 0 : 1, + }} + /> + {/*This is used so that the New Question Header is unmounted after the animation*/} + <Transition mounted={isNewQuestion} transition={fadeIn} duration={300}> + {style => <NewQuestionHeader className={CS.spread} style={style} />} + </Transition> + </QueryBuilderViewHeaderContainer> + ); +}; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/index.ts b/frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/index.ts new file mode 100644 index 00000000000..435beb477f7 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewHeaderContainer/index.ts @@ -0,0 +1 @@ +export * from "./ViewHeaderContainer"; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/ViewLeftSidebarContainer.jsx b/frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/ViewLeftSidebarContainer.jsx new file mode 100644 index 00000000000..bb06e7f3c40 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/ViewLeftSidebarContainer.jsx @@ -0,0 +1,28 @@ +import { match } from "ts-pattern"; + +import { ChartSettingsSidebar } from "metabase/query_builder/components/view/sidebars/ChartSettingsSidebar"; +import { ChartTypeSidebar } from "metabase/query_builder/components/view/sidebars/ChartTypeSidebar"; + +export const ViewLeftSidebarContainer = ({ + question, + result, + isShowingChartSettingsSidebar, + isShowingChartTypeSidebar, +}) => + match({ + isShowingChartSettingsSidebar, + isShowingChartTypeSidebar, + }) + .with( + { + isShowingChartSettingsSidebar: true, + }, + () => <ChartSettingsSidebar question={question} result={result} />, + ) + .with( + { + isShowingChartTypeSidebar: true, + }, + () => <ChartTypeSidebar question={question} result={result} />, + ) + .otherwise(() => null); diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/index.ts b/frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/index.ts new file mode 100644 index 00000000000..fec1b66f337 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewLeftSidebarContainer/index.ts @@ -0,0 +1 @@ +export * from "./ViewLeftSidebarContainer"; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/ViewMainContainer.jsx b/frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/ViewMainContainer.jsx new file mode 100644 index 00000000000..d53f71afe06 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/ViewMainContainer.jsx @@ -0,0 +1,67 @@ +/* eslint-disable react/prop-types */ +import CS from "metabase/css/core/index.css"; +import QueryVisualization from "metabase/query_builder/components/QueryVisualization"; +import { + QueryBuilderMain, + StyledDebouncedFrame, + StyledSyncedParametersList, +} from "metabase/query_builder/components/view/View/View.styled"; +import { ViewNativeQueryEditor } from "metabase/query_builder/components/view/View/ViewNativeQueryEditor/ViewNativeQueryEditor"; +import { ViewFooter } from "metabase/query_builder/components/view/ViewFooter"; +import { TimeseriesChrome } from "metabase/querying/filters/components/TimeseriesChrome"; +import * as Lib from "metabase-lib"; + +export const ViewMainContainer = props => { + const { + queryBuilderMode, + mode, + question, + showLeftSidebar, + showRightSidebar, + parameters, + setParameterValue, + isLiveResizable, + updateQuestion, + } = props; + + if (queryBuilderMode === "notebook") { + // we need to render main only in view mode + return; + } + + const queryMode = mode && mode.queryMode(); + const { isNative } = Lib.queryDisplayInfo(question.query()); + const isSidebarOpen = showLeftSidebar || showRightSidebar; + + return ( + <QueryBuilderMain + isSidebarOpen={isSidebarOpen} + data-testid="query-builder-main" + > + {isNative ? ( + <ViewNativeQueryEditor {...props} /> + ) : ( + <StyledSyncedParametersList + parameters={parameters} + setParameterValue={setParameterValue} + commitImmediately + /> + )} + + <StyledDebouncedFrame enabled={!isLiveResizable}> + <QueryVisualization + {...props} + noHeader + className={CS.spread} + mode={queryMode} + /> + </StyledDebouncedFrame> + <TimeseriesChrome + question={question} + updateQuestion={updateQuestion} + className={CS.flexNoShrink} + /> + <ViewFooter className={CS.flexNoShrink} /> + </QueryBuilderMain> + ); +}; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/index.ts b/frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/index.ts new file mode 100644 index 00000000000..38494b7effb --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewMainContainer/index.ts @@ -0,0 +1 @@ +export * from "./ViewMainContainer"; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/ViewNativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/ViewNativeQueryEditor.jsx new file mode 100644 index 00000000000..138daae3e6d --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/ViewNativeQueryEditor.jsx @@ -0,0 +1,45 @@ +/* eslint-disable react/prop-types */ +import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor"; +import { NativeQueryEditorContainer } from "metabase/query_builder/components/view/View/View.styled"; +import * as Lib from "metabase-lib"; + +export const ViewNativeQueryEditor = props => { + const { + question, + height, + isDirty, + isNativeEditorOpen, + card, + setParameterValueToDefault, + onSetDatabaseId, + } = props; + + const legacyQuery = question.legacyQuery(); + + // Normally, when users open native models, + // they open an ad-hoc GUI question using the model as a data source + // (using the `/dataset` endpoint instead of the `/card/:id/query`) + // However, users without data permission open a real model as they can't use the `/dataset` endpoint + // So the model is opened as an underlying native question and the query editor becomes visible + // This check makes it hide the editor in this particular case + // More details: https://github.com/metabase/metabase/pull/20161 + const { isEditable } = Lib.queryDisplayInfo(question.query()); + if (question.type() === "model" && !isEditable) { + return null; + } + + return ( + <NativeQueryEditorContainer> + <NativeQueryEditor + {...props} + query={legacyQuery} + viewHeight={height} + isOpen={legacyQuery.isEmpty() || isDirty} + isInitiallyOpen={isNativeEditorOpen} + datasetQuery={card && card.dataset_query} + setParameterValueToDefault={setParameterValueToDefault} + onSetDatabaseId={onSetDatabaseId} + /> + </NativeQueryEditorContainer> + ); +}; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/index.ts b/frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/index.ts new file mode 100644 index 00000000000..3d215be6885 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewNativeQueryEditor/index.ts @@ -0,0 +1 @@ +export * from "./ViewNativeQueryEditor"; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/ViewRightSidebarContainer.jsx b/frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/ViewRightSidebarContainer.jsx new file mode 100644 index 00000000000..aa46b55bcb8 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/ViewRightSidebarContainer.jsx @@ -0,0 +1,56 @@ +/* eslint-disable react/prop-types */ +import { NativeQueryRightSidebar } from "metabase/query_builder/components/view/View/NativeQueryRightSidebar/NativeQueryRightSidebar"; +import { StructuredQueryRightSidebar } from "metabase/query_builder/components/view/View/StructuredQueryRightSidebar/StructuredQueryRightSidebar"; +import * as Lib from "metabase-lib"; + +export const ViewRightSidebarContainer = props => { + const { + question, + deselectTimelineEvents, + hideTimelineEvents, + isShowingQuestionInfoSidebar, + isShowingQuestionSettingsSidebar, + isShowingSummarySidebar, + isShowingTimelineSidebar, + onCloseQuestionInfo, + onCloseSummary, + onCloseTimelines, + onOpenModal, + onSave, + selectTimelineEvents, + selectedTimelineEventIds, + showTimelineEvents, + timelines, + updateQuestion, + visibleTimelineEventIds, + xDomain, + } = props; + + const { isNative } = Lib.queryDisplayInfo(question.query()); + + return !isNative ? ( + <StructuredQueryRightSidebar + deselectTimelineEvents={deselectTimelineEvents} + hideTimelineEvents={hideTimelineEvents} + isShowingQuestionInfoSidebar={isShowingQuestionInfoSidebar} + isShowingQuestionSettingsSidebar={isShowingQuestionSettingsSidebar} + isShowingSummarySidebar={isShowingSummarySidebar} + isShowingTimelineSidebar={isShowingTimelineSidebar} + onCloseQuestionInfo={onCloseQuestionInfo} + onCloseSummary={onCloseSummary} + onCloseTimelines={onCloseTimelines} + onOpenModal={onOpenModal} + onSave={onSave} + question={question} + selectTimelineEvents={selectTimelineEvents} + selectedTimelineEventIds={selectedTimelineEventIds} + showTimelineEvents={showTimelineEvents} + timelines={timelines} + updateQuestion={updateQuestion} + visibleTimelineEventIds={visibleTimelineEventIds} + xDomain={xDomain} + /> + ) : ( + <NativeQueryRightSidebar {...props} /> + ); +}; diff --git a/frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/index.ts b/frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/index.ts new file mode 100644 index 00000000000..abcf94d87f4 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/ViewRightSidebarContainer/index.ts @@ -0,0 +1 @@ +export * from "./ViewRightSidebarContainer"; diff --git a/frontend/src/metabase/query_builder/components/view/View/index.ts b/frontend/src/metabase/query_builder/components/view/View/index.ts new file mode 100644 index 00000000000..94d4027d3d4 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/View/index.ts @@ -0,0 +1 @@ +export { View } from "./View"; diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx index 6871bfc2401..585ee9dfc7b 100644 --- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx +++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx @@ -28,7 +28,7 @@ import { } from "metabase/selectors/user"; import * as actions from "../actions"; -import View from "../components/view/View"; +import { View } from "../components/view/View"; import { VISUALIZATION_SLOW_TIMEOUT } from "../constants"; import { getAutocompleteResultsFn, -- GitLab