Skip to content
Snippets Groups Projects
Unverified Commit 93b74729 authored by lbrdnk's avatar lbrdnk Committed by GitHub
Browse files

Merge branch 'release-x.51.x' into backport-0c90ddfa

parents a532d040 889a8d99
No related merge requests found
Showing
with 858 additions and 90 deletions
......@@ -69,6 +69,36 @@ Must be less than 1048575. This environment variable also affects how many rows
This environment variable also affects how many rows Metabase includes in dashboard subscription attachments.
See also MB_UNAGGREGATED_QUERY_ROW_LIMIT.
### `MB_ALLOWED_IFRAME_HOSTS`
- Type: string
- Default: `youtube.com,
youtu.be,
loom.com,
vimeo.com,
docs.google.com,
calendar.google.com,
airtable.com,
typeform.com,
canva.com,
codepen.io,
figma.com,
grafana.com,
miro.com,
excalidraw.com,
notion.com,
atlassian.com,
trello.com,
asana.com,
gist.github.com,
linkedin.com,
twitter.com,
x.com`
- [Exported as](../installation-and-operation/serialization.md): `allowed-iframe-hosts`.
- [Configuration file name](./config-file.md): `allowed-iframe-hosts`
Allowed iframe hosts.
### `MB_ANON_TRACKING_ENABLED`
- Type: boolean
......@@ -411,15 +441,25 @@ SMTP secure connection protocol. (tls, ssl, starttls, or none).
SMTP username.
### `MB_EMBEDDING_APP_ORIGIN`
### `MB_EMBEDDING_APP_ORIGINS_INTERACTIVE`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `embedding-app-origin`
- [Configuration file name](./config-file.md): `embedding-app-origins-interactive`
Allow these space delimited origins to embed Metabase interactive.
Allow this origin to embed the full Metabase application.
### `MB_EMBEDDING_APP_ORIGINS_SDK`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
- Type: string
- Default: `localhost:*`
- [Configuration file name](./config-file.md): `embedding-app-origins-sdk`
Allow Metabase SDK access to these space delimited origins.
### `MB_EMBEDDING_HOMEPAGE`
......@@ -438,14 +478,29 @@ Embedding homepage status, indicating if its visible, hidden or has been dismiss
Secret key used to sign JSON Web Tokens for requests to `/api/embed` endpoints.
### `MB_ENABLE_EMBEDDING`
### `MB_ENABLE_EMBEDDING_INTERACTIVE`
- Type: boolean
- Default: `false`
- [Configuration file name](./config-file.md): `enable-embedding-interactive`
Allow admins to embed Metabase via interactive embedding?
### `MB_ENABLE_EMBEDDING_SDK`
- Type: boolean
- Default: `false`
- [Configuration file name](./config-file.md): `enable-embedding-sdk`
Allow admins to embed Metabase via the SDK?
### `MB_ENABLE_EMBEDDING_STATIC`
- Type: boolean
- Default: `false`
- [Exported as](../installation-and-operation/serialization.md): `enable-embedding`.
- [Configuration file name](./config-file.md): `enable-embedding`
- [Configuration file name](./config-file.md): `enable-embedding-static`
Allow admins to securely embed questions and dashboards within other applications?
Allow admins to embed Metabase via static embedding?
### `MB_ENABLE_PASSWORD_LOGIN`
......@@ -1585,7 +1640,7 @@ Must be less than 1048575, and less than the number configured in MB_AGGREGATED_
- [Exported as](../installation-and-operation/serialization.md): `update-channel`.
- [Configuration file name](./config-file.md): `update-channel`
Metabase will notify you when a new release is available for the channel you select.
Well notify you here when theres a new version of this type of release.
### `MB_UPLOADS_SETTINGS`
......@@ -1963,7 +2018,7 @@ Default page to show people when they log in.
Type: Boolean<br>
Default: True
If you want to exclude the [Usage analytics](../usage-and-performance-tools/usage-analytics.md) collection, you can set `MB_LOAD_ANALYTICS_CONTENT=false`. Setting this environment variable to false can also come in handy when migrating environments, as it can simplify the migration process.
If you want to exclude the [Metabase analytics](../usage-and-performance-tools/usage-analytics.md) collection, you can set `MB_LOAD_ANALYTICS_CONTENT=false`. Setting this environment variable to false can also come in handy when migrating environments, as it can simplify the migration process.
### `MB_NO_SURVEYS`
......
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
import {
createQuestion,
modal,
popover,
restore,
setTokenFeatures,
......@@ -98,4 +99,29 @@ describeSDK("scenarios > embedding-sdk > interactive-question", () => {
cy.icon("warning").should("not.exist");
});
it("can save a question", () => {
cy.intercept("POST", "/api/card").as("createCard");
cy.findAllByTestId("cell-data").last().click();
popover().findByText("See these Orders").click();
cy.findByRole("button", { name: "Save" }).click();
modal().within(() => {
cy.findByRole("radiogroup").findByText("Save as new question").click();
cy.findByPlaceholderText("What is the name of your question?")
.clear()
.type("Foo Bar Orders");
cy.findByRole("button", { name: "Save" }).click();
});
cy.wait("@createCard").then(({ response }) => {
expect(response?.statusCode).to.equal(200);
expect(response?.body.name).to.equal("Foo Bar Orders");
});
});
});
......@@ -8,6 +8,7 @@ import {
SdkError,
SdkLoader,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import { SaveQuestionModal } from "metabase/containers/SaveQuestionModal";
import { Box, Button, Group, Icon } from "metabase/ui";
import { InteractiveQuestion } from "../../public/InteractiveQuestion";
......@@ -58,12 +59,22 @@ export const InteractiveQuestionResult = ({
const [questionView, setQuestionView] =
useState<QuestionView>("visualization");
const { question, queryResults, isQuestionLoading } =
useInteractiveQuestionContext();
const {
question,
queryResults,
isQuestionLoading,
isSaveEnabled,
originalQuestion,
onCreate,
onSave,
} = useInteractiveQuestionContext();
const [isChartSelectorOpen, { toggle: toggleChartTypeSelector }] =
useDisclosure(false);
const [isSaveModalOpen, { open: openSaveModal, close: closeSaveModal }] =
useDisclosure(false);
if (isQuestionLoading) {
return <SdkLoader />;
}
......@@ -104,6 +115,9 @@ export const InteractiveQuestionResult = ({
)
}
/>
{isSaveEnabled && !isSaveModalOpen && (
<InteractiveQuestion.SaveButton onClick={openSaveModal} />
)}
</Group>
</Group>
......@@ -147,6 +161,19 @@ export const InteractiveQuestionResult = ({
/>
</Box>
</Box>
{/* Refer to the SaveQuestionProvider for context on why we have to do it like this */}
{isSaveModalOpen && question && (
<SaveQuestionModal
question={question}
originalQuestion={originalQuestion ?? null}
opened
closeOnSuccess
onClose={closeSaveModal}
onCreate={onCreate}
onSave={onSave}
/>
)}
</FlexibleSizeComponent>
);
};
......@@ -29,5 +29,6 @@ export const Default = {
args: {
questionId: QUESTION_ID,
isSaveEnabled: true,
},
};
import cx from "classnames";
import { useEffect, useState } from "react";
import { t } from "ttag";
import {
......@@ -7,11 +6,10 @@ import {
SdkLoader,
withPublicComponentWrapper,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import { useLoadStaticQuestion } from "embedding-sdk/hooks/private/use-load-static-question";
import { getDefaultVizHeight } from "embedding-sdk/lib/default-height";
import { loadStaticQuestion } from "embedding-sdk/lib/load-static-question";
import CS from "metabase/css/core/index.css";
import { useValidatedEntityId } from "metabase/lib/entity-id/hooks/use-validated-entity-id";
import type { GenericErrorResponse } from "metabase/lib/errors";
import { getResponseErrorMessage } from "metabase/lib/errors";
import { useSelector } from "metabase/lib/redux";
import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
......@@ -23,7 +21,7 @@ import { getMetadata } from "metabase/selectors/metadata";
import { Box, Group } from "metabase/ui";
import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
import Question from "metabase-lib/v1/Question";
import type { Card, CardEntityId, CardId, Dataset } from "metabase-types/api";
import type { CardEntityId, CardId, Dataset } from "metabase-types/api";
export type StaticQuestionProps = {
questionId: CardId | CardEntityId;
......@@ -32,13 +30,6 @@ export type StaticQuestionProps = {
parameterValues?: Record<string, string | number>;
};
type State = {
loading: boolean;
card: Card | null;
result: Dataset | null;
error: GenericErrorResponse | null;
};
type StaticQuestionVisualizationSelectorProps = {
question: Question;
result: Dataset | null;
......@@ -87,63 +78,8 @@ const StaticQuestionInner = ({
const metadata = useSelector(getMetadata);
const [{ loading, card, result, error }, setState] = useState<State>({
loading: false,
card: null,
result: null,
error: null,
});
useEffect(() => {
async function loadCardData() {
setState(prevState => ({
...prevState,
loading: true,
}));
if (!questionId) {
return;
}
try {
const { card, result } = await loadStaticQuestion({
questionId,
parameterValues,
});
setState(prevState => ({
...prevState,
card,
result,
loading: false,
error: null,
}));
} catch (error) {
if (typeof error === "object") {
setState(prevState => ({
...prevState,
result: null,
card: null,
loading: false,
error,
}));
} else {
console.error("error loading static question", error);
}
}
}
loadCardData();
}, [questionId, parameterValues]);
const changeVisualization = (newQuestion: Question) => {
setState({
card: newQuestion.card(),
result: result,
loading: false,
error: null,
});
};
const { card, loading, result, error, updateQuestion } =
useLoadStaticQuestion(questionId, parameterValues);
const isLoading = loading || (!result && !error) || isValidatingEntityId;
......@@ -173,7 +109,7 @@ const StaticQuestionInner = ({
<StaticQuestionVisualizationSelector
question={question}
result={result}
onUpdateQuestion={changeVisualization}
onUpdateQuestion={updateQuestion}
/>
)}
<QueryVisualization
......
import { act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import fetchMock from "fetch-mock";
......@@ -85,7 +86,7 @@ const setup = ({
setupCardQueryEndpoints(card, TEST_DATASET);
renderWithProviders(
return renderWithProviders(
<StaticQuestion
questionId={TEST_QUESTION_ID}
showVisualizationSelector={showVisualizationSelector}
......@@ -174,4 +175,19 @@ describe("StaticQuestion", () => {
value: 1024,
});
});
it("should cancel the request when the component unmounts", async () => {
const abortSpy = jest.spyOn(AbortController.prototype, "abort");
const { unmount } = setup();
await act(async () => unmount());
expect(abortSpy).toHaveBeenCalled();
abortSpy.mockRestore();
// sanity check that the two requests were made initially
expect(fetchMock.calls(`path:/api/card/1`).length).toBe(1);
expect(fetchMock.calls(`path:/api/card/1/query`).length).toBe(1);
});
});
import { useEffect, useState } from "react";
import { loadStaticQuestion } from "embedding-sdk/lib/load-static-question";
import type { GenericErrorResponse } from "metabase/lib/errors";
import { defer } from "metabase/lib/promise";
import type Question from "metabase-lib/v1/Question";
import type { Card, Dataset } from "metabase-types/api";
type QuestionState = {
loading: boolean;
card: Card | null;
result: Dataset | null;
error: GenericErrorResponse | null;
};
export function useLoadStaticQuestion(
questionId: number | null,
parameterValues?: Record<string, string | number>,
) {
const [questionState, setQuestionState] = useState<QuestionState>({
loading: false,
card: null,
result: null,
error: null,
});
const updateQuestion = (newQuestion: Question) =>
setQuestionState(state => ({
...state,
card: newQuestion.card(),
loading: false,
error: null,
}));
useEffect(() => {
const cancelDeferred = defer();
async function loadCardData() {
setQuestionState(state => ({ ...state, loading: true }));
if (!questionId) {
return;
}
try {
const { card, result } = await loadStaticQuestion({
questionId,
parameterValues,
cancelDeferred,
});
setQuestionState({
card,
result,
loading: false,
error: null,
});
} catch (error) {
if (typeof error === "object") {
setQuestionState({
result: null,
card: null,
loading: false,
error,
});
} else {
console.error("error loading static question", error);
}
}
}
loadCardData();
return () => {
// cancel pending requests upon unmount
cancelDeferred.resolve();
};
}, [questionId, parameterValues]);
return { ...questionState, updateQuestion };
}
import type { Deferred } from "metabase/lib/promise";
import { CardApi } from "metabase/services";
import type { Card, Dataset, ParameterQueryObject } from "metabase-types/api";
interface Options {
questionId: number;
parameterValues?: Record<string, string | number>;
cancelDeferred?: Deferred;
}
type ParameterQueryInput = { id: string } & ParameterQueryObject;
export async function loadStaticQuestion(options: Options) {
const { questionId, parameterValues } = options;
const { questionId, parameterValues, cancelDeferred } = options;
let card: Card | null;
let result: Dataset | null;
const cancelled = cancelDeferred?.promise;
[card, result] = await Promise.all([
CardApi.get({ cardId: questionId }),
CardApi.get({ cardId: questionId }, { cancelled }),
// Query the card in parallel when no parameters are provided.
!parameterValues && CardApi.query({ cardId: questionId }),
!parameterValues && CardApi.query({ cardId: questionId }, { cancelled }),
]);
if (parameterValues && card?.parameters) {
......@@ -31,10 +35,10 @@ export async function loadStaticQuestion(options: Options) {
value: parameterValues[parameter.slug],
}));
result = await CardApi.query({
cardId: questionId,
parameters,
});
result = await CardApi.query(
{ cardId: questionId, parameters },
{ cancelled },
);
}
return { card, result };
......
......@@ -20,7 +20,13 @@ export const SaveQuestionModal = ({
<SaveQuestionProvider
question={question}
originalQuestion={originalQuestion}
onCreate={onCreate}
onCreate={async question => {
await onCreate(question);
if (closeOnSuccess) {
modalProps.onClose();
}
}}
onSave={onSave}
multiStep={multiStep}
initialCollectionId={initialCollectionId}
......
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);
};
export * from "./NativeQueryRightSidebar";
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);
export * from "./StructuredQueryRightSidebar";
......@@ -6,7 +6,6 @@ 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";
......@@ -18,409 +17,26 @@ 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 { 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 DatasetEditor from "../../DatasetEditor";
import { QueryModals } from "../../QueryModals";
import { SavedQuestionIntroModal } from "../../SavedQuestionIntroModal";
import ViewSidebar from "../ViewSidebar";
import NewQuestionHeader from "./NewQuestionHeader";
import { NotebookContainer } from "./View/NotebookContainer";
import { NotebookContainer } from "./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 ViewInner = props => {
const {
question,
result,
......@@ -677,7 +293,7 @@ const mapDispatchToProps = dispatch => ({
},
});
export default _.compose(
export const View = _.compose(
ExplicitSize({ refreshMode: "debounceLeading" }),
connect(null, mapDispatchToProps),
)(View);
)(ViewInner);
......@@ -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;
......
/* 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>
);
};
export * from "./ViewHeaderContainer";
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);
export * from "./ViewLeftSidebarContainer";
/* 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>
);
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment