Compare commits

...

4 Commits

Author SHA1 Message Date
Uladzimir Havenchyk
10e0798b43 convert many components to typescript 2025-06-11 14:41:57 +03:00
Uladzimir Havenchyk
86d513d9ef chore: cleanup native editor after refactoring 2025-06-10 23:34:24 +03:00
Uladzimir Havenchyk
1a9ce05cc4 pass missed parameter 2025-06-10 19:52:41 +03:00
Uladzimir Havenchyk
4bcd59e02b chore: split NativeQueryEditor to components 2025-06-09 22:16:01 +03:00
26 changed files with 1231 additions and 600 deletions

View File

@@ -85,6 +85,7 @@ export const QuestionVisualization = ({
result={result}
noHeader
mode={mode}
// @ts-expect-error - double check previous card type
navigateToNewCardInsideQB={
variant === "static" ? undefined : navigateToNewCard
}

View File

@@ -16,7 +16,7 @@ export interface ToasterProps extends HTMLAttributes<HTMLDivElement> {
confirmText?: string;
isShown: boolean;
fixed?: boolean;
className: string;
className?: string;
onConfirm: () => void;
onDismiss: () => void;
}

View File

@@ -164,7 +164,15 @@ function getSidebar(
);
}
if (isShowingDataReference) {
return <DataReference {...props} onClose={toggleDataReference} />;
return (
<DataReference
dataReferenceStack={props.dataReferenceStack}
popDataReferenceStack={props.popDataReferenceStack}
pushDataReferenceStack={props.pushDataReferenceStack}
onBack={props.onBack}
onClose={toggleDataReference}
/>
);
}
if (isShowingSnippetSidebar) {
return <SnippetSidebar {...props} onClose={toggleSnippetSidebar} />;

View File

@@ -23,8 +23,6 @@ import {
import SnippetFormModal from "metabase/query_builder/components/template_tags/SnippetFormModal";
import type { QueryModalType } from "metabase/query_builder/constants";
import { useNotebookScreenSize } from "metabase/query_builder/hooks/use-notebook-screen-size";
import { Flex } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
import type {
@@ -33,33 +31,22 @@ import type {
DatabaseId,
NativeQuerySnippet,
ParameterId,
TableId,
} from "metabase-types/api";
import { ResponsiveParametersList } from "../ResponsiveParametersList";
import {
CodeMirrorEditor,
type CodeMirrorEditorProps,
type CodeMirrorEditorRef,
} from "./CodeMirrorEditor";
import DataSourceSelectors from "./DataSourceSelectors";
import S from "./NativeQueryEditor.module.css";
import type { Features as SidebarFeatures } from "./NativeQueryEditorActionButtons";
import { NativeQueryEditorActionButtons } from "./NativeQueryEditorActionButtons";
import { NativeQueryEditorRunButton } from "./NativeQueryEditorRunButton/NativeQueryEditorRunButton";
import { NativeQueryEditorTopBar } from "./NativeQueryEditorTopBar/NativeQueryEditorTopBar";
import { RightClickPopover } from "./RightClickPopover";
import { VisibilityToggler } from "./VisibilityToggler";
import { MIN_HEIGHT_LINES } from "./constants";
import type { SelectionRange } from "./types";
import {
calcInitialEditorHeight,
formatQuery,
getEditorLineHeight,
getMaxAutoSizeLines,
} from "./utils";
import type { SelectionRange, Features as SidebarFeatures } from "./types";
import { calcInitialEditorHeight, getEditorLineHeight } from "./utils";
type OwnProps = typeof NativeQueryEditor.defaultProps & {
type OwnProps = {
question: Question;
query: NativeQuery;
@@ -68,7 +55,6 @@ type OwnProps = typeof NativeQueryEditor.defaultProps & {
viewHeight: number;
highlightedLineNumbers?: number[];
isOpen?: boolean;
isInitiallyOpen?: boolean;
isNativeEditorOpen: boolean;
isRunnable: boolean;
@@ -104,7 +90,6 @@ type OwnProps = typeof NativeQueryEditor.defaultProps & {
insertSnippet: (snippet: NativeQuerySnippet) => void;
setIsNativeEditorOpen?: (isOpen: boolean) => void;
setParameterValue: (parameterId: ParameterId, value: string) => void;
setParameterValueToDefault: (parameterId: ParameterId) => void;
onOpenModal: (modalType: QueryModalType) => void;
toggleDataReference: () => void;
toggleTemplateTagsEditor: () => void;
@@ -134,8 +119,6 @@ type Props = OwnProps &
interface NativeQueryEditorState {
initialHeight: number;
isSelectedTextPopoverOpen: boolean;
mobileShowParameterList: boolean;
isPromptInputVisible: boolean;
}
class NativeQueryEditor extends Component<Props, NativeQueryEditorState> {
@@ -149,23 +132,9 @@ class NativeQueryEditor extends Component<Props, NativeQueryEditorState> {
this.state = {
initialHeight: calcInitialEditorHeight({ query, viewHeight }),
isSelectedTextPopoverOpen: false,
mobileShowParameterList: false,
isPromptInputVisible: false,
};
}
static defaultProps = {
isOpen: false,
canChangeDatabase: true,
resizable: true,
sidebarFeatures: {
dataReference: true,
variables: true,
snippets: true,
promptInput: true,
},
};
UNSAFE_componentWillMount() {
const { question, setIsNativeEditorOpen, isInitiallyOpen } = this.props;
@@ -177,6 +146,17 @@ class NativeQueryEditor extends Component<Props, NativeQueryEditorState> {
setIsNativeEditorOpen?.(!question || !question.isSaved());
}
componentDidUpdate(prevProps: Props) {
if (
this.state.isSelectedTextPopoverOpen &&
!this.props.nativeEditorSelectedText &&
prevProps.nativeEditorSelectedText
) {
// close selected text popover if text is deselected
this.setState({ isSelectedTextPopoverOpen: false });
}
}
onChange = (queryText: string) => {
const { query, setDatasetQuery } = this.props;
if (query.queryText() !== queryText) {
@@ -188,17 +168,6 @@ class NativeQueryEditor extends Component<Props, NativeQueryEditorState> {
}
};
componentDidUpdate(prevProps: Props) {
if (
this.state.isSelectedTextPopoverOpen &&
!this.props.nativeEditorSelectedText &&
prevProps.nativeEditorSelectedText
) {
// close selected text popover if text is deselected
this.setState({ isSelectedTextPopoverOpen: false });
}
}
focus() {
if (this.props.readOnly) {
return;
@@ -206,118 +175,37 @@ class NativeQueryEditor extends Component<Props, NativeQueryEditorState> {
this.editor.current?.focus();
}
// Change the Database we're currently editing a query for.
setDatabaseId = (databaseId: DatabaseId) => {
const { query, setDatasetQuery, question, onSetDatabaseId } = this.props;
if (question.databaseId() !== databaseId) {
setDatasetQuery(query.setDatabaseId(databaseId).setDefaultCollection());
onSetDatabaseId?.(databaseId);
this.focus();
}
};
setTableId = (tableId: TableId) => {
const { query, setDatasetQuery } = this.props;
const table = query.metadata().table(tableId);
if (table && table.name !== query.collection()) {
setDatasetQuery(query.setCollectionName(table.name));
}
};
setParameterIndex = (parameterId: ParameterId, parameterIndex: number) => {
const { query, setDatasetQuery } = this.props;
setDatasetQuery(query.setParameterIndex(parameterId, parameterIndex));
};
handleFilterButtonClick = () => {
this.setState({
mobileShowParameterList: !this.state.mobileShowParameterList,
});
};
togglePromptVisibility = () => {
this.setState((prev) => ({
isPromptInputVisible: !prev.isPromptInputVisible,
}));
};
handleRightClickSelection = () => {
this.setState({ isSelectedTextPopoverOpen: true });
};
_updateSize(doc: string) {
const { viewHeight } = this.props;
const element = this.resizeBox.current;
if (!doc || !element) {
return;
}
const lines = doc.split("\n").length;
const newHeight = getEditorLineHeight(
Math.max(
Math.min(lines, getMaxAutoSizeLines(viewHeight)),
MIN_HEIGHT_LINES,
),
);
if (newHeight > element.offsetHeight) {
element.style.height = `${newHeight}px`;
}
}
handleQueryGenerated = (queryText: string) => {
this.onChange(queryText);
this.focus();
};
formatQuery = async () => {
const { question } = this.props;
const query = question.query();
const engine = Lib.engine(query);
const queryText = Lib.rawNativeQuery(query);
if (!engine) {
// no engine found, do nothing
return;
}
const formattedQuery = await formatQuery(queryText, engine);
this.onChange(formattedQuery);
this.focus();
};
render() {
const {
question,
query,
setParameterValue,
readOnly,
isNativeEditorOpen,
openSnippetModalWithSelectedText,
openDataReferenceAtQuestion,
hasParametersList = true,
canChangeDatabase = true,
resizable = true,
sidebarFeatures = {
dataReference: true,
variables: true,
snippets: true,
promptInput: true,
},
hasTopBar = true,
hasEditingSidebar = true,
resizableBoxProps = {},
snippetCollections = [],
resizable,
editorContext = "question",
question,
query,
readOnly,
isNativeEditorOpen,
openSnippetModalWithSelectedText,
openDataReferenceAtQuestion,
setDatasetQuery,
setNativeEditorSelectedRange,
sidebarFeatures,
canChangeDatabase,
setParameterValueToDefault,
forwardedRef,
runQuery,
highlightedLineNumbers,
} = this.props;
const parameters = query.question().parameters();
const dragHandle = resizable ? (
<div className={S.dragHandleContainer}>
<div className={S.dragHandle} />
@@ -335,64 +223,32 @@ class NativeQueryEditor extends Component<Props, NativeQueryEditorState> {
ref={forwardedRef}
>
{hasTopBar && (
<Flex align="flex-start" data-testid="native-query-top-bar">
{canChangeDatabase && (
<DataSourceSelectors
isNativeEditorOpen={isNativeEditorOpen}
query={query}
question={question}
readOnly={readOnly}
setDatabaseId={this.setDatabaseId}
setTableId={this.setTableId}
editorContext={editorContext}
/>
)}
{hasParametersList && (
<ResponsiveParametersList
question={question}
parameters={parameters}
setParameterValue={setParameterValue}
setParameterIndex={this.setParameterIndex}
setParameterValueToDefault={setParameterValueToDefault}
enableParameterRequiredBehavior
/>
)}
<Flex ml="auto" gap="lg" mr="lg" align="center" h="55px">
{isNativeEditorOpen && hasEditingSidebar && !readOnly && (
<NativeQueryEditorActionButtons
features={sidebarFeatures}
onShowPromptInput={this.togglePromptVisibility}
onFormatQuery={this.formatQuery}
onGenerateQuery={this.onChange}
question={question}
nativeEditorSelectedText={this.props.nativeEditorSelectedText}
snippetCollections={snippetCollections}
snippets={this.props.snippets}
isRunnable={this.props.isRunnable}
isRunning={this.props.isRunning}
isResultDirty={this.props.isResultDirty}
isShowingDataReference={this.props.isShowingDataReference}
isShowingTemplateTagsEditor={
this.props.isShowingTemplateTagsEditor
}
isShowingSnippetSidebar={this.props.isShowingSnippetSidebar}
onOpenModal={this.props.onOpenModal}
toggleDataReference={this.props.toggleDataReference}
toggleTemplateTagsEditor={this.props.toggleTemplateTagsEditor}
toggleSnippetSidebar={this.props.toggleSnippetSidebar}
/>
)}
{query.hasWritePermission() &&
!query.question().isArchived() &&
this.props.setIsNativeEditorOpen && (
<VisibilityToggler
isOpen={isNativeEditorOpen}
readOnly={!!readOnly}
toggleEditor={this.props.toggleEditor}
/>
)}
</Flex>
</Flex>
<NativeQueryEditorTopBar
hasEditingSidebar={hasEditingSidebar}
question={question}
query={query}
onChange={this.onChange}
focus={this.focus}
canChangeDatabase={canChangeDatabase}
sidebarFeatures={sidebarFeatures}
isRunnable={this.props.isRunnable}
isRunning={this.props.isRunning}
hasParametersList={this.props.hasParametersList}
isResultDirty={this.props.isResultDirty}
isShowingDataReference={this.props.isShowingDataReference}
onOpenModal={this.props.onOpenModal}
isShowingTemplateTagsEditor={this.props.isShowingTemplateTagsEditor}
setIsNativeEditorOpen={this.props.setIsNativeEditorOpen}
snippets={this.props.snippets}
nativeEditorSelectedText={this.props.nativeEditorSelectedText}
editorContext={this.props.editorContext}
onSetDatabaseId={this.props.onSetDatabaseId}
isShowingSnippetSidebar={this.props.isShowingSnippetSidebar}
isNativeEditorOpen={this.props.isNativeEditorOpen}
toggleEditor={this.props.toggleEditor}
setParameterValue={this.props.setParameterValue}
setDatasetQuery={this.props.setDatasetQuery}
/>
)}
<ResizableBox
ref={this.resizeBox}

View File

@@ -12,17 +12,12 @@ import { Button, Flex, Icon, Tooltip } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import type { Collection, NativeQuerySnippet } from "metabase-types/api";
import type { Features } from "../types";
import S from "./NativeQueryEditorActionButtons.module.css";
const ICON_SIZE = 18;
export type Features = {
dataReference?: boolean;
variables?: boolean;
snippets?: boolean;
promptInput?: boolean;
};
interface NativeQueryEditorActionButtonsProps {
question: Question;
nativeEditorSelectedText?: string;
@@ -35,14 +30,9 @@ interface NativeQueryEditorActionButtonsProps {
isShowingDataReference: boolean;
isShowingTemplateTagsEditor: boolean;
isShowingSnippetSidebar: boolean;
isPromptInputVisible?: boolean;
runQuery?: () => void;
cancelQuery?: () => void;
onOpenModal: (modalType: QueryModalType) => void;
onShowPromptInput: () => void;
toggleDataReference: () => void;
toggleTemplateTagsEditor: () => void;
toggleSnippetSidebar: () => void;
onFormatQuery: () => void;
onGenerateQuery: (queryText: string) => void;
}

View File

@@ -0,0 +1,189 @@
import { useDispatch } from "metabase/lib/redux";
import { updateQuestion } from "metabase/query_builder/actions/core";
import type { QueryModalType } from "metabase/query_builder/constants";
import { Flex } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
import type {
Collection,
DatabaseId,
NativeQuerySnippet,
ParameterId,
TableId,
} from "metabase-types/api";
import { ResponsiveParametersList } from "../../ResponsiveParametersList";
import DataSourceSelectors from "../DataSourceSelectors/DataSourceSelectors";
import { NativeQueryEditorActionButtons } from "../NativeQueryEditorActionButtons/NativeQueryEditorActionButtons";
import { VisibilityToggler } from "../VisibilityToggler/VisibilityToggler";
import type { Features as SidebarFeatures } from "../types";
import { formatQuery } from "../utils";
interface NativeQueryEditorTopBarProps {
question: Question;
query: NativeQuery;
isRunnable: boolean;
isRunning: boolean;
isResultDirty: boolean;
isShowingSnippetSidebar: boolean;
isShowingDataReference: boolean;
isShowingTemplateTagsEditor: boolean;
isNativeEditorOpen: boolean;
nativeEditorSelectedText?: string;
canChangeDatabase: boolean;
hasParametersList?: boolean;
hasEditingSidebar: boolean;
readOnly?: boolean;
snippets?: NativeQuerySnippet[];
editorContext?: "question";
snippetCollections?: Collection[];
sidebarFeatures: SidebarFeatures;
toggleEditor: () => void;
setIsNativeEditorOpen?: (isOpen: boolean) => void;
onSetDatabaseId?: (id: DatabaseId) => void;
onOpenModal: (modalType: QueryModalType) => void;
onChange: (queryText: string) => void;
setParameterValue: (parameterId: ParameterId, value: string) => void;
focus: () => void;
setDatasetQuery: (query: NativeQuery) => Promise<Question>;
}
const NativeQueryEditorTopBar = (props: NativeQueryEditorTopBarProps) => {
const {
query,
question,
onChange,
canChangeDatabase,
isNativeEditorOpen,
readOnly,
editorContext = "question",
setParameterValue,
sidebarFeatures,
hasEditingSidebar,
snippetCollections,
focus: setFocus,
snippets,
isRunnable,
isRunning,
isResultDirty,
isShowingDataReference,
isShowingTemplateTagsEditor,
isShowingSnippetSidebar,
onOpenModal,
nativeEditorSelectedText,
setIsNativeEditorOpen,
toggleEditor,
onSetDatabaseId,
hasParametersList = true,
setDatasetQuery,
} = props;
const dispatch = useDispatch();
const setTableId = (tableId: TableId) => {
const table = query.metadata().table(tableId);
if (table && table.name !== query.collection()) {
setDatasetQuery(query.setCollectionName(table.name));
}
};
const setParameterIndex = (
parameterId: ParameterId,
parameterIndex: number,
) => {
const newQuery = query.setParameterIndex(parameterId, parameterIndex);
dispatch(updateQuestion(question.setDatasetQuery(newQuery.datasetQuery())));
};
// Change the Database we're currently editing a query for.
const setDatabaseId = (databaseId: DatabaseId) => {
if (question.databaseId() !== databaseId) {
setDatasetQuery(query.setDatabaseId(databaseId).setDefaultCollection());
onSetDatabaseId?.(databaseId);
setFocus();
}
};
const handleFormatQuery = async () => {
const query = question.query();
const engine = Lib.engine(query);
const queryText = Lib.rawNativeQuery(query);
if (!engine) {
// no engine found, do nothing
return;
}
const formattedQuery = await formatQuery(queryText, engine);
onChange(formattedQuery);
setFocus();
};
if (!question) {
return null;
}
const parameters = question.parameters();
return (
<Flex align="flex-start" data-testid="native-query-top-bar">
{canChangeDatabase && (
<DataSourceSelectors
isNativeEditorOpen={isNativeEditorOpen}
query={query}
question={question}
readOnly={readOnly}
setDatabaseId={setDatabaseId}
setTableId={setTableId}
editorContext={editorContext}
/>
)}
{hasParametersList && (
<ResponsiveParametersList
question={question}
parameters={parameters}
setParameterValue={setParameterValue}
setParameterIndex={setParameterIndex}
enableParameterRequiredBehavior
/>
)}
<Flex ml="auto" gap="lg" mr="lg" align="center" h="55px">
{isNativeEditorOpen && hasEditingSidebar && !readOnly && (
<NativeQueryEditorActionButtons
features={sidebarFeatures}
onFormatQuery={handleFormatQuery}
onGenerateQuery={onChange}
question={question}
nativeEditorSelectedText={nativeEditorSelectedText}
snippetCollections={snippetCollections}
snippets={snippets}
isRunnable={isRunnable}
isRunning={isRunning}
isResultDirty={isResultDirty}
isShowingDataReference={isShowingDataReference}
isShowingTemplateTagsEditor={isShowingTemplateTagsEditor}
isShowingSnippetSidebar={isShowingSnippetSidebar}
onOpenModal={onOpenModal}
/>
)}
{query.hasWritePermission() &&
!question.isArchived() &&
setIsNativeEditorOpen && (
<VisibilityToggler
isOpen={isNativeEditorOpen}
readOnly={!!readOnly}
toggleEditor={toggleEditor}
/>
)}
</Flex>
</Flex>
);
};
export { NativeQueryEditorTopBar };

View File

@@ -7,3 +7,10 @@ export type SelectionRange = {
start: Location;
end: Location;
};
export type Features = {
dataReference?: boolean;
variables?: boolean;
snippets?: boolean;
promptInput?: boolean;
};

View File

@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import cx from "classnames";
import { useState } from "react";
import { useTimeout } from "react-use";
@@ -13,8 +12,12 @@ import { useSelector } from "metabase/lib/redux";
import { getIsNativeQueryFixApplied } from "metabase/query_builder/selectors";
import { getWhiteLabeledLoadingMessageFactory } from "metabase/selectors/whitelabel";
import { Box, Flex, Stack, Text, Title } from "metabase/ui";
import type { Mode } from "metabase/visualizations/click-actions/Mode";
import type { OnChangeCardAndRunOpts } from "metabase/visualizations/types";
import * as Lib from "metabase-lib";
import { HARD_ROW_LIMIT } from "metabase-lib/v1/queries/utils";
import type Question from "metabase-lib/v1/Question";
import type { Dataset, RawSeries, TimelineEvent, VisualizationSettings } from "metabase-types/api";
import type { QueryBuilderMode } from "metabase-types/store";
import RunButtonWithTooltip from "./RunButtonWithTooltip";
import { VisualizationError } from "./VisualizationError";
@@ -23,7 +26,43 @@ import Warnings from "./Warnings";
const SLOW_MESSAGE_TIMEOUT = 4000;
export default function QueryVisualization(props) {
interface QueryVisualizationProps {
question: Question;
isDirty?: boolean;
queryBuilderMode?: QueryBuilderMode;
navigateToNewCardInsideQB?: (opts: OnChangeCardAndRunOpts) => Promise<void>;
result: Dataset | null;
rawSeries: RawSeries | null;
timelineEvents?: TimelineEvent[];
selectedTimelineEventIds?: number[];
onNavigateBack?: () => void;
isShowingSummarySidebar?: boolean;
onEditSummary?: () => void;
renderEmptyMessage?: boolean | undefined;
isRunning: boolean;
isResultDirty: boolean;
runQuestionQuery: () => void;
cancelQuery: () => void;
className?: string;
isRunnable: boolean;
isObjectDetail?: boolean;
isNativeEditorOpen?: boolean;
mode?: Mode | null | undefined;
onUpdateQuestion?: (question: Question) => void;
onVisualizationRendered?: () => void;
onUpdateVisualizationSettings?: (
changedSettings: VisualizationSettings,
question?: Question,
) => void;
selectTimelineEvents?: (events: TimelineEvent[]) => void;
deselectTimelineEvents?: () => void;
isShowingDetailsOnlyColumns?: boolean;
hasMetadataPopovers?: boolean;
}
// eslint-disable-next-line import/no-default-export
export default function QueryVisualization(props: QueryVisualizationProps) {
const {
className,
question,
@@ -33,11 +72,10 @@ export default function QueryVisualization(props) {
isResultDirty,
isNativeEditorOpen,
result,
maxTableRows = HARD_ROW_LIMIT,
} = props;
const canRun = Lib.canRun(question.query(), question.type());
const [warnings, setWarnings] = useState([]);
const [warnings, setWarnings] = useState<string[]>([]);
const isNativeQueryFixApplied = useSelector(getIsNativeQueryFixApplied);
return (
@@ -48,14 +86,17 @@ export default function QueryVisualization(props) {
<VisualizationRunningState className={cx(CS.spread, CS.z2)} />
) : null}
<VisualizationDirtyState
{...props}
isRunning={props.isRunning}
isResultDirty={props.isResultDirty}
runQuestionQuery={props.runQuestionQuery}
cancelQuery={props.cancelQuery}
hidden={
!canRun ||
!isResultDirty ||
!isRunnable ||
isRunning ||
isNativeEditorOpen ||
result?.error
Boolean(result?.error)
}
className={cx(CS.spread, CS.z2)}
/>
@@ -78,7 +119,7 @@ export default function QueryVisualization(props) {
data-testid="query-visualization-root"
>
{isNativeQueryFixApplied ? (
<VisualizationEmptyState className={CS.spread}>
<VisualizationEmptyState>
{t`Fixes applied. Run your query to view results.`}
</VisualizationEmptyState>
) : result?.error ? (
@@ -86,20 +127,41 @@ export default function QueryVisualization(props) {
className={CS.spread}
error={result.error}
errorType={result.error_type}
// @ts-expect-error - fix dataset type?
via={result.via}
question={question}
// @ts-expect-error - fix dataset type?
duration={result.duration}
/>
) : result?.data ? (
<VisualizationResult
{...props}
maxTableRows={maxTableRows}
question={props.question}
isDirty={props.isDirty}
queryBuilderMode={props.queryBuilderMode}
navigateToNewCardInsideQB={props.navigateToNewCardInsideQB}
result={props.result}
rawSeries={props.rawSeries}
timelineEvents={props.timelineEvents}
selectedTimelineEventIds={props.selectedTimelineEventIds}
onNavigateBack={props.onNavigateBack}
isRunning={props.isRunning}
isShowingSummarySidebar={props.isShowingSummarySidebar}
onEditSummary={props.onEditSummary}
renderEmptyMessage={props.renderEmptyMessage}
className={CS.spread}
onUpdateWarnings={setWarnings}
mode={props.mode}
onUpdateQuestion={props.onUpdateQuestion}
onVisualizationRendered={props.onVisualizationRendered}
isObjectDetail={props.isObjectDetail}
onUpdateVisualizationSettings={props.onUpdateVisualizationSettings}
selectTimelineEvents={props.selectTimelineEvents}
deselectTimelineEvents={props.deselectTimelineEvents}
isShowingDetailsOnlyColumns={props.isShowingDetailsOnlyColumns}
hasMetadataPopovers={props.hasMetadataPopovers}
/>
) : !isRunning ? (
<VisualizationEmptyState
className={CS.spread}
isCompact={isNativeEditorOpen}
>
{t`Here's where your results will appear`}
@@ -110,7 +172,7 @@ export default function QueryVisualization(props) {
);
}
const VisualizationEmptyState = ({ isCompact, children }) => {
const VisualizationEmptyState = ({ isCompact, children }: { isCompact?: boolean, children: React.ReactNode }) => {
const keyboardShortcut = getRunQueryShortcut();
return (
@@ -128,8 +190,8 @@ const VisualizationEmptyState = ({ isCompact, children }) => {
<Text c="text-medium">
{c("{0} refers to the keyboard shortcut")
.jt`To run your code, click on the Run button or type ${(
<b key="shortcut">({keyboardShortcut})</b>
)}`}
<b key="shortcut">({keyboardShortcut})</b>
)}`}
</Text>
<Text c="text-medium">{children}</Text>
</Stack>
@@ -137,14 +199,14 @@ const VisualizationEmptyState = ({ isCompact, children }) => {
);
};
export function VisualizationRunningState({ className = "" }) {
export function VisualizationRunningState({ className = "" }: { className?: string }) {
const [isSlow] = useTimeout(SLOW_MESSAGE_TIMEOUT);
const getLoadingMessage = useSelector(getWhiteLabeledLoadingMessageFactory);
// show the slower loading message only when the loadingMessage is
// not customized
const message = getLoadingMessage(isSlow());
const message = getLoadingMessage(isSlow() ?? undefined);
return (
<Flex
@@ -162,15 +224,23 @@ export function VisualizationRunningState({ className = "" }) {
);
}
interface VisualizationDirtyStateProps {
className?: string;
isRunning: boolean;
isResultDirty: boolean;
runQuestionQuery: () => void;
cancelQuery: () => void;
hidden: boolean;
}
export const VisualizationDirtyState = ({
className,
result,
isRunning,
isResultDirty,
runQuestionQuery,
cancelQuery,
hidden,
}) => {
}: VisualizationDirtyStateProps) => {
const keyboardShortcut = getRunQueryShortcut();
const handleClick = () => {

View File

@@ -6,7 +6,7 @@ import Button from "metabase/core/components/Button";
import useIsSmallScreen from "metabase/hooks/use-is-small-screen";
import { Box, Flex } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import type { Parameter, ParameterId } from "metabase-types/api";
import type { Parameter } from "metabase-types/api";
import ResponsiveParametersListS from "./ResponsiveParametersList.module.css";
import { SyncedParametersList } from "./SyncedParametersList";
@@ -15,7 +15,6 @@ interface ResponsiveParametersListProps {
question: Question;
parameters: Parameter[];
setParameterValue: (parameterId: string, value: string) => void;
setParameterValueToDefault: (parameterId: ParameterId) => void;
setParameterIndex: (parameterId: string, parameterIndex: number) => void;
enableParameterRequiredBehavior: boolean;
}
@@ -25,7 +24,6 @@ export const ResponsiveParametersList = ({
parameters,
setParameterValue,
setParameterIndex,
setParameterValueToDefault,
enableParameterRequiredBehavior,
}: ResponsiveParametersListProps) => {
const [mobileShowParameterList, setShowMobileParameterList] = useState(false);
@@ -81,7 +79,6 @@ export const ResponsiveParametersList = ({
parameters={parameters}
setParameterValue={setParameterValue}
setParameterIndex={setParameterIndex}
setParameterValueToDefault={setParameterValueToDefault}
enableParameterRequiredBehavior={enableParameterRequiredBehavior}
isEditing
commitImmediately

View File

@@ -1,12 +1,16 @@
import querystring from "querystring";
import { useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import { IS_EMBED_PREVIEW } from "metabase/lib/embed";
import { useDispatch } from "metabase/lib/redux";
import {
ParametersList,
type ParametersListProps,
} from "metabase/parameters/components/ParametersList";
import { getParameterValuesBySlug } from "metabase-lib/v1/parameters/utils/parameter-values";
import type { ParameterId } from "metabase-types/api";
import { setParameterValueToDefault } from "../actions";
export const SyncedParametersList = ({
parameters,
@@ -25,9 +29,9 @@ export const SyncedParametersList = ({
setParameterValue,
setParameterIndex,
setEditingParameter,
setParameterValueToDefault,
enableParameterRequiredBehavior,
}: ParametersListProps) => {
const dispatch = useDispatch();
const queryParams = useMemo(
() => getParameterValuesBySlug(parameters),
[parameters],
@@ -53,6 +57,13 @@ export const SyncedParametersList = ({
}
}, [queryParams]);
const dispatchSetParameterValueToDefault = useCallback(
(parameterId: ParameterId) => {
dispatch(setParameterValueToDefault(parameterId));
},
[dispatch],
);
return (
<ParametersList
className={className}
@@ -68,7 +79,7 @@ export const SyncedParametersList = ({
setParameterValue={setParameterValue}
setParameterIndex={setParameterIndex}
setEditingParameter={setEditingParameter}
setParameterValueToDefault={setParameterValueToDefault}
setParameterValueToDefault={dispatchSetParameterValueToDefault}
enableParameterRequiredBehavior={enableParameterRequiredBehavior}
/>
);

View File

@@ -1,4 +1,3 @@
/* eslint-disable react/prop-types */
import cx from "classnames";
import { Component } from "react";
import { jt, t } from "ttag";
@@ -8,10 +7,16 @@ import { ErrorMessage } from "metabase/components/ErrorMessage";
import ButtonsS from "metabase/css/components/buttons.module.css";
import CS from "metabase/css/core/index.css";
import { CreateOrEditQuestionAlertModal } from "metabase/notifications/modals/CreateOrEditQuestionAlertModal/CreateOrEditQuestionAlertModal";
import type { MantineTheme } from "metabase/ui";
import type { Mode } from "metabase/visualizations/click-actions/Mode";
import Visualization from "metabase/visualizations/components/Visualization";
import type { ClickObject, OnChangeCardAndRunOpts } from "metabase/visualizations/types";
import * as Lib from "metabase-lib";
import { ALERT_TYPE_ROWS } from "metabase-lib/v1/Alert";
import type Question from "metabase-lib/v1/Question";
import { datasetContainsNoResults } from "metabase-lib/v1/queries/utils/dataset";
import type { Card, DatasetColumn, DatasetQuery, RawSeries, SingleSeries, TimelineEvent } from "metabase-types/api";
import type { QueryBuilderMode } from "metabase-types/store";
const ALLOWED_VISUALIZATION_PROPS = [
// Table
@@ -25,7 +30,50 @@ const ALLOWED_VISUALIZATION_PROPS = [
"renderEmptyMessage",
];
export default class VisualizationResult extends Component {
interface VisualizationResultProps {
question: Question;
isDirty?: boolean;
onVisualizationRendered?: () => void;
isObjectDetail?: boolean;
queryBuilderMode?: QueryBuilderMode;
navigateToNewCardInsideQB?: (opts: OnChangeCardAndRunOpts) => Promise<void>;
result: any;
rawSeries: RawSeries | null;
timelineEvents?: TimelineEvent[];
selectedTimelineEventIds?: number[];
onNavigateBack?: () => void;
className?: string;
isRunning?: boolean;
isShowingSummarySidebar?: boolean;
onEditSummary?: () => void;
renderEmptyMessage?: boolean;
onUpdateVisualizationSettings?: (settings: any) => void;
onHeaderColumnReorder?: (columnName: string) => void;
onUpdateWarnings?: (warnings: string[]) => void;
onOpenChartSettings?: (data: {
initialChartSettings: { section: string };
showSidebarTitle?: boolean;
}) => void;
handleVisualizationClick?: (clicked: ClickObject | null) => void;
onOpenTimelines?: () => void;
selectTimelineEvents?: (events: TimelineEvent[]) => void;
deselectTimelineEvents?: () => void;
onUpdateQuestion?: (question: Question) => void;
isShowingDetailsOnlyColumns?: boolean;
hasMetadataPopovers?: boolean;
tableHeaderHeight?: number;
scrollToColumn?: number
renderTableHeader?: (
column: DatasetColumn,
index: number,
theme: MantineTheme,
) => React.ReactNode;
mode?: Mode | null | undefined;
}
// eslint-disable-next-line import/no-default-export
export default class VisualizationResult extends Component<VisualizationResultProps> {
state = {
showCreateAlertModal: false,
};
@@ -38,7 +86,11 @@ export default class VisualizationResult extends Component {
this.setState({ showCreateAlertModal: false });
};
getObjectDetailData = (series) => {
getObjectDetailData = (series: RawSeries | null): (SingleSeries | { card: Card<DatasetQuery> })[] => {
if (!series) {
return [];
}
return [
{
...series[0],
@@ -68,6 +120,7 @@ export default class VisualizationResult extends Component {
const noResults = datasetContainsNoResults(result.data);
if (noResults && !isRunning && !renderEmptyMessage) {
// @ts-expect-error - TODO: pass a parameter to alertType
const supportsRowsPresentAlert = question.alertType() === ALERT_TYPE_ROWS;
// successful query but there were 0 rows returned with the result
@@ -122,7 +175,7 @@ export default class VisualizationResult extends Component {
<>
<Visualization
className={className}
rawSeries={rawSeries}
rawSeries={rawSeries ?? undefined}
onChangeCardAndRun={
hasDrills ? navigateToNewCardInsideQB : undefined
}

View File

@@ -0,0 +1,65 @@
import { useCallback } from "react";
import DatabasePane from "./DatabasePane";
import FieldPane from "./FieldPane";
import MainPane from "./MainPane";
import QuestionPane from "./QuestionPane";
import SchemaPane from "./SchemaPane";
import TablePane from "./TablePane";
export type DataReferenceType = "database" | "schema" | "table" | "question" | "field";
export interface DataReferenceStackItem {
type: DataReferenceType;
// TODO: fix this type based on usage
item: unknown;
}
interface DataReferenceProps {
dataReferenceStack: DataReferenceStackItem[];
popDataReferenceStack: () => void;
pushDataReferenceStack: (item: DataReferenceStackItem) => void;
onClose?: () => void;
onBack?: () => void;
}
const PANES: Record<DataReferenceType, React.ComponentType<any>> = {
database: DatabasePane, // lists schemas, tables and models of a database
schema: SchemaPane, // lists tables of a schema
table: TablePane, // lists fields of a table
question: QuestionPane, // lists fields of a question
field: FieldPane, // field details and metadata
};
const DataReference = ({
dataReferenceStack,
popDataReferenceStack,
pushDataReferenceStack,
onClose,
onBack,
}: DataReferenceProps) => {
const onItemClick = useCallback(
(type: DataReferenceType, item: unknown) => pushDataReferenceStack({ type, item }),
[pushDataReferenceStack],
);
if (dataReferenceStack.length) {
const page = dataReferenceStack[dataReferenceStack.length - 1];
const Pane = PANES[page.type];
return (
<Pane
{...{ [page.type]: page.item }}
onItemClick={onItemClick}
onClose={onClose}
onBack={popDataReferenceStack}
/>
);
} else {
return (
<MainPane onItemClick={onItemClick} onClose={onClose} onBack={onBack} />
);
}
};
// eslint-disable-next-line import/no-default-export
export default DataReference;

View File

@@ -1,6 +1,8 @@
import cx from "classnames";
import { t } from "ttag";
import { useDispatch } from "metabase/lib/redux";
import { toggleDataReference } from "metabase/query_builder/actions";
import { Box, Icon, Tooltip } from "metabase/ui";
import DataReferenceButtonS from "./DataReferenceButton.module.css";
@@ -9,24 +11,32 @@ interface DataReferenceButtonProps {
className?: string;
isShowingDataReference: boolean;
size: number;
toggleDataReference: () => void;
}
export const DataReferenceButton = ({
className,
isShowingDataReference,
size,
toggleDataReference,
}: DataReferenceButtonProps) => (
<Tooltip label={t`Learn about your data`}>
<Box
component="a"
h={size}
className={cx(className, DataReferenceButtonS.ButtonRoot, {
[DataReferenceButtonS.isSelected]: isShowingDataReference,
})}
>
<Icon name="reference" size={size} onClick={toggleDataReference} />
</Box>
</Tooltip>
);
}: DataReferenceButtonProps) => {
const dispatch = useDispatch();
return (
<Tooltip label={t`Learn about your data`}>
<Box
component="a"
h={size}
className={cx(className, DataReferenceButtonS.ButtonRoot, {
[DataReferenceButtonS.isSelected]: isShowingDataReference,
})}
>
<Icon
name="reference"
size={size}
onClick={() => {
dispatch(toggleDataReference());
}}
/>
</Box>
</Tooltip>
);
};

View File

@@ -1,6 +1,8 @@
import cx from "classnames";
import { t } from "ttag";
import { useDispatch } from "metabase/lib/redux";
import { toggleTemplateTagsEditor } from "metabase/query_builder/actions";
import { Box, Icon, Tooltip } from "metabase/ui";
import NativeVariablesButtonS from "./NativeVariablesButton.module.css";
@@ -9,24 +11,31 @@ interface NativeVariablesButtonProps {
className?: string;
isShowingTemplateTagsEditor: boolean;
size: number;
toggleTemplateTagsEditor: () => void;
}
export const NativeVariablesButton = ({
className,
isShowingTemplateTagsEditor,
size,
toggleTemplateTagsEditor,
}: NativeVariablesButtonProps) => (
<Tooltip label={t`Variables`}>
<Box
component="a"
h={size}
className={cx(className, NativeVariablesButtonS.ButtonRoot, {
[NativeVariablesButtonS.isSelected]: isShowingTemplateTagsEditor,
})}
>
<Icon name="variable" size={size} onClick={toggleTemplateTagsEditor} />
</Box>
</Tooltip>
);
}: NativeVariablesButtonProps) => {
const dispatch = useDispatch();
return (
<Tooltip label={t`Variables`}>
<Box
component="a"
h={size}
className={cx(className, NativeVariablesButtonS.ButtonRoot, {
[NativeVariablesButtonS.isSelected]: isShowingTemplateTagsEditor,
})}
>
<Icon
name="variable"
size={size}
onClick={() => {
dispatch(toggleTemplateTagsEditor());
}}
/>
</Box>
</Tooltip>
);
};

View File

@@ -1,6 +1,8 @@
import cx from "classnames";
import { t } from "ttag";
import { useDispatch } from "metabase/lib/redux";
import { toggleSnippetSidebar } from "metabase/query_builder/actions/native";
import { Box, Icon, Tooltip } from "metabase/ui";
import SnippetSidebarButtonS from "./SnippetSidebarButton.module.css";
@@ -9,24 +11,32 @@ interface SnippetSidebarButtonProps {
className?: string;
isShowingSnippetSidebar: boolean;
size: number;
toggleSnippetSidebar: () => void;
}
export const SnippetSidebarButton = ({
className,
isShowingSnippetSidebar,
size,
toggleSnippetSidebar,
}: SnippetSidebarButtonProps) => (
<Tooltip label={t`SQL Snippets`}>
<Box
component="a"
h={size}
className={cx(className, SnippetSidebarButtonS.ButtonRoot, {
[SnippetSidebarButtonS.isSelected]: isShowingSnippetSidebar,
})}
>
<Icon name="snippet" size={size} onClick={toggleSnippetSidebar} />
</Box>
</Tooltip>
);
}: SnippetSidebarButtonProps) => {
const dispatch = useDispatch();
return (
<Tooltip label={t`SQL Snippets`}>
<Box
component="a"
h={size}
className={cx(className, SnippetSidebarButtonS.ButtonRoot, {
[SnippetSidebarButtonS.isSelected]: isShowingSnippetSidebar,
})}
>
<Icon
name="snippet"
size={size}
onClick={() => {
dispatch(toggleSnippetSidebar());
}}
/>
</Box>
</Tooltip>
);
};

View File

@@ -1,3 +1,5 @@
// eslint-disable-next-line no-restricted-imports
import type { Moment } from "moment";
import { match } from "ts-pattern";
import { PLUGIN_AI_ENTITY_ANALYSIS } from "metabase/plugins";
@@ -7,8 +9,41 @@ import { TagEditorSidebar } from "metabase/query_builder/components/template_tag
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";
import type { QueryModalType } from "metabase/query_builder/constants";
import type Question from "metabase-lib/v1/Question";
import type { Timeline, TimelineEvent } from "metabase-types/api";
export const NativeQueryRightSidebar = (props) => {
interface NativeQueryRightSidebarProps {
visibleTimelineEventIds: number[];
selectedTimelineEventIds: number[];
xDomain: [Moment, Moment] | undefined;
onOpenModal: ((modal: QueryModalType, modalContext?: unknown) => void) | undefined;
onHideTimelineEvents: (timelineEvent: TimelineEvent[]) => void;
onSelectTimelineEvents: ((timelineEvents: TimelineEvent[]) => void) | undefined;
onDeselectTimelineEvents: (() => void) | undefined;
question: Question;
timelineEvents: TimelineEvent[];
timelines: Timeline[];
toggleTemplateTagsEditor: () => void;
toggleDataReference: () => void;
toggleSnippetSidebar: () => void;
showTimelineEvents: () => void;
hideTimelineEvents: (timelineEvent: TimelineEvent[]) => void;
selectTimelineEvents: () => void;
deselectTimelineEvents?: () => void
onCloseTimelines: () => void;
onSave: (question: Question) => Promise<Question>;
isShowingTemplateTagsEditor: boolean;
isShowingDataReference: boolean;
isShowingSnippetSidebar: boolean;
isShowingTimelineSidebar: boolean;
isShowingQuestionInfoSidebar: boolean;
isShowingQuestionSettingsSidebar: boolean;
isShowingAIQuestionAnalysisSidebar: boolean;
onCloseAIQuestionAnalysisSidebar: () => void;
}
export const NativeQueryRightSidebar = (props: NativeQueryRightSidebarProps) => {
const {
question,
timelineEvents,
@@ -16,14 +51,12 @@ export const NativeQueryRightSidebar = (props) => {
toggleTemplateTagsEditor,
toggleDataReference,
toggleSnippetSidebar,
showTimelineEvent,
showTimelineEvents,
hideTimelineEvents,
selectTimelineEvents,
deselectTimelineEvents,
onCloseTimelines,
onSave,
onCloseQuestionInfo,
isShowingTemplateTagsEditor,
isShowingDataReference,
isShowingSnippetSidebar,
@@ -43,13 +76,14 @@ export const NativeQueryRightSidebar = (props) => {
isShowingQuestionSettingsSidebar,
isShowingAIQuestionAnalysisSidebar,
})
.with({ isShowingTemplateTagsEditor: true }, () => (
<TagEditorSidebar
{...props}
query={question.legacyNativeQuery()}
onClose={toggleTemplateTagsEditor}
/>
))
.with({ isShowingTemplateTagsEditor: true }, () => {
return (
<TagEditorSidebar
{...props}
query={question.legacyNativeQuery()}
onClose={toggleTemplateTagsEditor} />
);
})
.with({ isShowingDataReference: true }, () => (
<DataReference {...props} onClose={toggleDataReference} />
))
@@ -58,20 +92,23 @@ export const NativeQueryRightSidebar = (props) => {
))
.with({ isShowingTimelineSidebar: true }, () => (
<TimelineSidebar
{...props}
onShowTimelineEvent={showTimelineEvent}
question={question}
timelines={timelines}
visibleTimelineEventIds={props.visibleTimelineEventIds}
selectedTimelineEventIds={props.selectedTimelineEventIds}
xDomain={props.xDomain}
onOpenModal={props.onOpenModal}
onClose={onCloseTimelines}
onShowTimelineEvents={showTimelineEvents}
onHideTimelineEvents={hideTimelineEvents}
onSelectTimelineEvents={selectTimelineEvents}
onDeselectTimelineEvents={deselectTimelineEvents}
onClose={onCloseTimelines}
/>
))
.with({ isShowingQuestionInfoSidebar: true }, () => (
<QuestionInfoSidebar
question={question}
onSave={onSave}
onClose={onCloseQuestionInfo}
/>
))
.with({ isShowingQuestionSettingsSidebar: true }, () => (

View File

@@ -1,12 +1,12 @@
/* eslint-disable react/prop-types */
import cx from "classnames";
import { forwardRef } from "react";
import type { ConnectedProps } from "react-redux";
import { match } from "ts-pattern";
import { t } from "ttag";
import _ from "underscore";
import { deletePermanently } from "metabase/archive/actions";
import type { CollectionPickerValueItem } from "metabase/common/components/CollectionPicker";
import ExplicitSize from "metabase/components/ExplicitSize";
import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
import Toaster from "metabase/components/Toaster";
@@ -19,10 +19,14 @@ import {
rememberLastUsedDatabase,
setArchivedQuestion,
} from "metabase/query_builder/actions";
import { SIDEBAR_SIZES } from "metabase/query_builder/constants";
import { type QueryModalType, SIDEBAR_SIZES } from "metabase/query_builder/constants";
import { MetricEditor } from "metabase/querying/metrics/components/MetricEditor";
import { Flex } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { Card, CardId, Database, DatabaseId, Dataset, Field, RawSeries, VisualizationSettings } from "metabase-types/api";
import type { Dispatch, QueryBuilderMode } from "metabase-types/store";
import type { InitialChartSettingState } from "metabase-types/store/qb";
import { DatasetEditor } from "../../../DatasetEditor";
import { QueryModals } from "../../../QueryModals";
@@ -36,7 +40,67 @@ import { ViewRightSidebarContainer } from "../ViewRightSidebarContainer";
import S from "./View.module.css";
const ViewInner = forwardRef(function _ViewInner(props, ref) {
type QueryBuilderViewProps = QueryBuilderViewReduxProps & OwnProps;
interface OwnProps {
isShowingNewbModal: boolean;
closeQbNewbModal: () => void;
onDismissToast: () => void;
onConfirmToast: () => void;
isShowingToaster: boolean;
isHeaderVisible: boolean;
updateQuestion: (question: Question) => Promise<void>;
reportTimezone: string;
readOnly: boolean;
isDirty: boolean;
isRunning: boolean;
isRunnable: boolean;
isResultDirty: boolean;
hasVisualizeButton: boolean;
runQuestionQuery: () => Promise<void>;
cancelQuery: () => void;
setQueryBuilderMode: (mode: QueryBuilderMode) => void;
runDirtyQuestionQuery: () => void;
isShowingQuestionInfoSidebar: boolean;
isShowingQuestionSettingsSidebar: boolean;
cancelQuestionChanges: () => void;
onCreate: (question: Question) => Promise<void>;
onSave: (
question: Question,
config?: { rerunQuery: boolean },
) => Promise<void>;
onChangeLocation: (location: string) => void;
modal: QueryModalType;
modalContext: number;
card: Card;
onCloseModal: () => void;
onOpenModal: (modalType: QueryModalType) => void;
originalQuestion: Question;
isShowingChartSettingsSidebar: boolean;
isShowingChartTypeSidebar: boolean;
onCloseChartSettings: () => void;
addField: (field: Field) => void;
initialChartSetting: InitialChartSettingState;
onReplaceAllVisualizationSettings: (settings: VisualizationSettings) => void;
onOpenChartType: () => void;
visualizationSettings: VisualizationSettings;
showSidebarTitle: boolean;
isShowingSummarySidebar: boolean;
isShowingTemplateTagsEditor: boolean;
isShowingDataReference: boolean;
isShowingSnippetSidebar: boolean;
isShowingTimelineSidebar: boolean;
isShowingAIQuestionAnalysisSidebar: boolean;
queryBuilderMode: QueryBuilderMode;
result: Dataset;
rawSeries: RawSeries;
databases: Database[];
question: Question;
}
const ViewInner = forwardRef<HTMLDivElement, QueryBuilderViewProps>(function _ViewInner(props, ref) {
const {
question,
result,
@@ -77,13 +141,6 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
originalQuestion,
isShowingChartSettingsSidebar,
isShowingChartTypeSidebar,
onCloseChartSettings,
addField,
initialChartSetting,
onReplaceAllVisualizationSettings,
onOpenChartType,
visualizationSettings,
showSidebarTitle,
isShowingSummarySidebar,
isShowingTemplateTagsEditor,
isShowingDataReference,
@@ -93,7 +150,7 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
// 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 ref={ref} />
<LoadingAndErrorWrapper className={CS.fullHeight} loading ref={ref as any} />
);
}
@@ -107,10 +164,10 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
if ((isModel || isMetric) && queryBuilderMode === "dataset") {
return (
<>
{isModel && <DatasetEditor {...props} ref={ref} />}
{isModel && <DatasetEditor {...props} ref={ref as any} />}
{isMetric && (
<MetricEditor
ref={ref}
ref={ref as any}
question={question}
result={result}
rawSeries={rawSeries}
@@ -118,18 +175,22 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
isDirty={isDirty}
isResultDirty={isResultDirty}
isRunning={isRunning}
onChange={updateQuestion}
onCreate={async (question) => {
const result = await onCreate(question);
setQueryBuilderMode("view");
return result;
onChange={async (q) => { updateQuestion(q); }}
onCreate={async (q: Question, _options?: any): Promise<Question> => {
try {
const result = await onCreate(q);
setQueryBuilderMode("view");
return result as unknown as Question;
} catch {
return q;
}
}}
onSave={async (question) => {
await onSave(question);
onSave={async (q) => {
await onSave(q);
setQueryBuilderMode("view");
}}
onCancel={(question) => {
if (question.isSaved()) {
onCancel={async (q) => {
if (q.isSaved()) {
cancelQuestionChanges();
runDirtyQuestionQuery();
setQueryBuilderMode("view");
@@ -137,13 +198,19 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
onChangeLocation("/");
}
}}
onRunQuery={runQuestionQuery}
onCancelQuery={cancelQuery}
onRunQuery={async () => { runQuestionQuery(); }}
onCancelQuery={async () => { cancelQuery(); }}
/>
)}
<QueryModals
onSave={onSave}
onCreate={onCreate}
onSave={async (q, config) => { await onSave(q, config); }}
onCreate={async (q: Question, _options?: any): Promise<Question> => {
try {
return (await onCreate(q)) as unknown as Question;
} catch {
return q;
}
}}
modal={modal}
modalContext={modalContext}
card={card}
@@ -189,13 +256,47 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
className={cx(QueryBuilderS.QueryBuilder, S.QueryBuilderViewRoot)}
data-testid="query-builder-root"
>
{isHeaderVisible && <ViewHeaderContainer {...props} />}
{isHeaderVisible && (
<ViewHeaderContainer
question={question}
onUnarchive={props.onUnarchive}
onMove={props.onMove}
onDeletePermanently={props.onDeletePermanently}
isObjectDetail={false}
isAdditionalInfoVisible={undefined}
onOpenQuestionInfo={() => { }}
onSave={() => { }}
onOpenModal={() => { }}
isNavBarOpen={false}
originalQuestion={undefined}
result={result}
queryBuilderMode={queryBuilderMode}
updateQuestion={updateQuestion as any}
isBookmarked={false}
toggleBookmark={() => { }}
isRunnable={isRunnable}
isRunning={isRunning}
isNativeEditorOpen={false}
isShowingSummarySidebar={false}
isDirty={isDirty}
isResultDirty={isResultDirty}
isActionListVisible={false}
runQuestionQuery={() => { }}
cancelQuery={() => { }}
onEditSummary={() => { }}
onCloseSummary={() => { }}
setQueryBuilderMode={() => { }}
isShowingQuestionInfoSidebar={false}
onCloseQuestionInfo={() => { }}
className={undefined}
/>
)}
<Flex className={S.QueryBuilderContentContainer}>
{!isNative && (
<NotebookContainer
isOpen={isNotebookContainerOpen}
updateQuestion={updateQuestion}
updateQuestion={async (q) => { updateQuestion(q); }}
reportTimezone={reportTimezone}
readOnly={readOnly}
question={question}
@@ -203,8 +304,8 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
isRunnable={isRunnable}
isResultDirty={isResultDirty}
hasVisualizeButton={hasVisualizeButton}
runQuestionQuery={runQuestionQuery}
setQueryBuilderMode={setQueryBuilderMode}
runQuestionQuery={async () => { runQuestionQuery(); }}
setQueryBuilderMode={setQueryBuilderMode as any}
/>
)}
<ViewSidebar side="left" isOpen={showLeftSidebar}>
@@ -213,21 +314,42 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
result={result}
isShowingChartSettingsSidebar={isShowingChartSettingsSidebar}
isShowingChartTypeSidebar={isShowingChartTypeSidebar}
onCloseChartSettings={onCloseChartSettings}
addField={addField}
initialChartSetting={initialChartSetting}
onReplaceAllVisualizationSettings={
onReplaceAllVisualizationSettings
}
onOpenChartType={onOpenChartType}
visualizationSettings={visualizationSettings}
showSidebarTitle={showSidebarTitle}
/>
</ViewSidebar>
<ViewMainContainer
question={question}
query={question.query() as any}
viewHeight={0}
isNativeEditorOpen={false}
isRunnable={isRunnable}
isRunning={isRunning}
isResultDirty={isResultDirty}
isShowingDataReference={isShowingDataReference}
isShowingTemplateTagsEditor={isShowingTemplateTagsEditor}
isShowingSnippetSidebar={isShowingSnippetSidebar}
runQuery={() => { }}
toggleEditor={() => { }}
handleResize={() => { }}
setDatasetQuery={async () => question}
runQuestionQuery={() => { }}
setNativeEditorSelectedRange={() => { }}
openDataReferenceAtQuestion={() => { }}
openSnippetModalWithSelectedText={() => { }}
insertSnippet={() => { }}
setParameterValue={() => { }}
setParameterValueToDefault={() => { }}
onOpenModal={() => { }}
toggleDataReference={() => { }}
toggleTemplateTagsEditor={() => { }}
toggleSnippetSidebar={() => { }}
closeSnippetModal={() => { }}
queryBuilderMode={queryBuilderMode}
mode={undefined as any}
showLeftSidebar={showLeftSidebar}
showRightSidebar={showRightSidebar}
{...props}
isLiveResizable={false}
parameters={[]}
updateQuestion={updateQuestion as any}
/>
<ViewSidebar
side="right"
@@ -247,20 +369,6 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
/>
)}
<QueryModals
onSave={onSave}
onCreate={onCreate}
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}
@@ -272,25 +380,29 @@ const ViewInner = forwardRef(function _ViewInner(props, ref) {
);
});
const mapDispatchToProps = (dispatch) => ({
onSetDatabaseId: (id) => dispatch(rememberLastUsedDatabase(id)),
onUnarchive: async (question) => {
const mapDispatchToProps = (dispatch: Dispatch) => ({
onSetDatabaseId: (id: DatabaseId) => dispatch(rememberLastUsedDatabase(id)),
onUnarchive: async (question: Question) => {
await dispatch(setArchivedQuestion(question, false));
await dispatch(Bookmarks.actions.invalidateLists());
},
onMove: (question, newCollection) =>
onMove: (question: Question, newCollection: CollectionPickerValueItem) =>
dispatch(
Questions.actions.setCollection({ id: question.id() }, newCollection, {
notify: { undo: false },
}),
),
onDeletePermanently: (id) => {
onDeletePermanently: (id: CardId) => {
const deleteAction = Questions.actions.delete({ id });
dispatch(deletePermanently(deleteAction));
},
});
const connector = connect(null, mapDispatchToProps, null, { forwardRef: true });
export type QueryBuilderViewReduxProps = ConnectedProps<typeof connector>;
export const View = _.compose(
ExplicitSize({ refreshMode: "debounceLeading" }),
connect(null, mapDispatchToProps, null, { forwardRef: true }),
connector,
)(ViewInner);

View File

@@ -1,68 +0,0 @@
/* eslint-disable react/prop-types */
import { t } from "ttag";
import { ArchivedEntityBanner } from "metabase/archive/components/ArchivedEntityBanner";
import CS from "metabase/css/core/index.css";
import { Box, Flex, Transition } from "metabase/ui";
import * as Lib from "metabase-lib";
import { ViewTitleHeader } from "../../ViewHeader";
import ViewSection, { ViewHeading } from "../../ViewSection";
import ViewHeaderContainerS from "./ViewHeaderContainer.module.css";
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 (
<Box className={ViewHeaderContainerS.QueryBuilderViewHeaderContainer}>
{card.archived && (
<ArchivedEntityBanner
name={card.name}
entityType={card.type}
canMove={card.can_write}
canRestore={card.can_restore}
canDelete={card.can_delete}
onUnarchive={() => onUnarchive(question)}
onMove={(collection) => onMove(question, collection)}
onDeletePermanently={() => onDeletePermanently(card.id)}
/>
)}
<ViewTitleHeader
className={ViewHeaderContainerS.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) => (
<ViewSection
className={CS.spread}
style={{
...style,
borderBottom: "1px solid var(--mb-color-border)",
}}
>
<Flex direction="column" gap="xs">
<ViewHeading>{t`Pick your starting data`}</ViewHeading>
</Flex>
</ViewSection>
)}
</Transition>
</Box>
);
};

View File

@@ -0,0 +1,134 @@
import cx from "classnames";
import { t } from "ttag";
import { ArchivedEntityBanner } from "metabase/archive/components/ArchivedEntityBanner";
import type { CollectionPickerValueItem } from "metabase/common/components/CollectionPicker";
import CS from "metabase/css/core/index.css";
import type { QueryModalType } from "metabase/query_builder/constants";
import { Box, Flex, Transition } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { CardId, Dataset } from "metabase-types/api";
import type { DatasetEditorTab, QueryBuilderMode } from "metabase-types/store";
import { ViewTitleHeader } from "../../ViewHeader";
import ViewSection, { ViewHeading } from "../../ViewSection";
import ViewHeaderContainerS from "./ViewHeaderContainer.module.css";
const fadeIn = {
in: { opacity: 1 },
out: { opacity: 0 },
transitionProperty: "opacity",
};
interface ViewHeaderContainerProps {
isObjectDetail: boolean;
isAdditionalInfoVisible: boolean | undefined;
onOpenQuestionInfo: () => void;
onSave: (newQuestion: Question) => any;
onOpenModal: (modalType: QueryModalType) => void;
isNavBarOpen: boolean;
originalQuestion: Question | undefined;
result: Dataset;
queryBuilderMode: QueryBuilderMode;
updateQuestion: (question: Question, opts?: { run?: boolean | undefined; } | undefined) => void;
isBookmarked: boolean;
toggleBookmark: () => void;
isRunnable: boolean;
isRunning: boolean;
isNativeEditorOpen: boolean;
isShowingSummarySidebar: boolean;
isDirty: boolean;
isResultDirty: boolean;
isActionListVisible: boolean;
runQuestionQuery: (opts?: { overrideWithQuestion?: Question | undefined; shouldUpdateUrl?: boolean | undefined; ignoreCache?: boolean | undefined; } | undefined) => void;
cancelQuery: () => void;
onEditSummary: () => void;
onCloseSummary: () => void;
setQueryBuilderMode: (mode: QueryBuilderMode, opts?: { shouldUpdateUrl?: boolean | undefined; datasetEditorTab?: DatasetEditorTab | undefined; } | undefined) => void;
isShowingQuestionInfoSidebar: boolean;
onCloseQuestionInfo: () => void;
className: string | undefined;
question: Question;
onUnarchive: (question: Question) => void;
onMove: (question: Question, collection: CollectionPickerValueItem) => void;
onDeletePermanently: (id: CardId) => void;
}
export const ViewHeaderContainer = (props: ViewHeaderContainerProps) => {
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 (
<Box className={ViewHeaderContainerS.QueryBuilderViewHeaderContainer}>
{card.archived && (
<ArchivedEntityBanner
name={card.name}
entityType={card.type}
canMove={card.can_write}
canRestore={card.can_restore}
canDelete={card.can_delete}
onUnarchive={() => onUnarchive(question)}
onMove={(collection) => onMove(question, collection)}
onDeletePermanently={() => onDeletePermanently(card.id)}
/>
)}
<ViewTitleHeader
className={cx(ViewHeaderContainerS.BorderedViewTitleHeader, props.className)}
question={props.question}
isObjectDetail={props.isObjectDetail}
isAdditionalInfoVisible={props.isAdditionalInfoVisible}
onOpenQuestionInfo={props.onOpenQuestionInfo}
onSave={props.onSave}
onOpenModal={props.onOpenModal}
isNavBarOpen={props.isNavBarOpen}
originalQuestion={props.originalQuestion}
result={props.result}
queryBuilderMode={props.queryBuilderMode}
updateQuestion={props.updateQuestion}
isBookmarked={props.isBookmarked}
toggleBookmark={props.toggleBookmark}
isRunnable={props.isRunnable}
isRunning={props.isRunning}
isNativeEditorOpen={props.isNativeEditorOpen}
isShowingSummarySidebar={props.isShowingSummarySidebar}
isDirty={props.isDirty}
isResultDirty={props.isResultDirty}
isActionListVisible={props.isActionListVisible}
runQuestionQuery={props.runQuestionQuery}
cancelQuery={props.cancelQuery}
onEditSummary={props.onEditSummary}
onCloseSummary={props.onCloseSummary}
setQueryBuilderMode={props.setQueryBuilderMode}
isShowingQuestionInfoSidebar={props.isShowingQuestionInfoSidebar}
onCloseQuestionInfo={props.onCloseQuestionInfo}
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) => (
<ViewSection
className={CS.spread}
style={{
...style,
borderBottom: "1px solid var(--mb-color-border)",
}}
>
<Flex direction="column" gap="xs">
<ViewHeading>{t`Pick your starting data`}</ViewHeading>
</Flex>
</ViewSection>
)}
</Transition>
</Box>
);
};

View File

@@ -1,78 +0,0 @@
/* eslint-disable react/prop-types */
import cx from "classnames";
import DebouncedFrame from "metabase/components/DebouncedFrame";
import CS from "metabase/css/core/index.css";
import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
import { SyncedParametersList } from "metabase/query_builder/components/SyncedParametersList";
import { TimeseriesChrome } from "metabase/querying/filters/components/TimeseriesChrome";
import { Box } from "metabase/ui";
import * as Lib from "metabase-lib";
import { ViewFooter } from "../../ViewFooter";
import { ViewNativeQueryEditor } from "../ViewNativeQueryEditor";
import ViewMainContainerS from "./ViewMainContainer.module.css";
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 (
<Box
component="main"
className={cx(ViewMainContainerS.QueryBuilderMain, {
[ViewMainContainerS.isSidebarOpen]: isSidebarOpen,
})}
data-testid="query-builder-main"
>
{isNative ? (
<ViewNativeQueryEditor {...props} />
) : (
<SyncedParametersList
className={ViewMainContainerS.StyledSyncedParametersList}
parameters={parameters}
setParameterValue={setParameterValue}
commitImmediately
/>
)}
<DebouncedFrame
className={ViewMainContainerS.StyledDebouncedFrame}
enabled={!isLiveResizable}
>
<QueryVisualization
{...props}
noHeader
className={CS.spread}
mode={queryMode}
onUpdateQuestion={updateQuestion}
/>
</DebouncedFrame>
<TimeseriesChrome
question={question}
updateQuestion={updateQuestion}
className={CS.flexNoShrink}
/>
<ViewFooter className={CS.flexNoShrink} />
</Box>
);
};

View File

@@ -0,0 +1,153 @@
import cx from "classnames";
import type { ResizableBoxProps } from "react-resizable";
import DebouncedFrame from "metabase/components/DebouncedFrame";
import CS from "metabase/css/core/index.css";
import type {
SelectionRange,
Features as SidebarFeatures,
} from "metabase/query_builder/components/NativeQueryEditor/types";
import QueryVisualization from "metabase/query_builder/components/QueryVisualization";
import { SyncedParametersList } from "metabase/query_builder/components/SyncedParametersList";
import type { QueryModalType } from "metabase/query_builder/constants";
import { TimeseriesChrome } from "metabase/querying/filters/components/TimeseriesChrome";
import { Box } from "metabase/ui";
import type { Mode } from "metabase/visualizations/click-actions/Mode";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { UiParameter } from "metabase-lib/v1/parameters/types";
import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
import type {
CardId,
DatabaseId,
NativeQuerySnippet,
ParameterId,
} from "metabase-types/api";
import type { QueryBuilderMode } from "metabase-types/store";
import { ViewFooter } from "../../ViewFooter";
import { ViewNativeQueryEditor } from "../ViewNativeQueryEditor";
import ViewMainContainerS from "./ViewMainContainer.module.css";
interface ViewMainContainerProps {
question: Question;
query: NativeQuery;
nativeEditorSelectedText?: string;
modalSnippet?: NativeQuerySnippet;
viewHeight: number;
highlightedLineNumbers?: number[];
isInitiallyOpen?: boolean;
isNativeEditorOpen: boolean;
isRunnable: boolean;
isRunning: boolean;
isResultDirty: boolean;
isShowingDataReference: boolean;
isShowingTemplateTagsEditor: boolean;
isShowingSnippetSidebar: boolean;
readOnly?: boolean;
canChangeDatabase?: boolean;
hasTopBar?: boolean;
hasParametersList?: boolean;
hasEditingSidebar?: boolean;
sidebarFeatures?: SidebarFeatures;
resizable?: boolean;
resizableBoxProps?: Partial<Omit<ResizableBoxProps, "axis">>;
editorContext?: "question";
runQuery: () => void;
toggleEditor: () => void;
handleResize: () => void;
setDatasetQuery: (query: NativeQuery) => Promise<Question>;
runQuestionQuery: (opts?: {
overrideWithQuestion?: Question;
shouldUpdateUrl?: boolean;
}) => void;
setNativeEditorSelectedRange: (range: SelectionRange[]) => void;
openDataReferenceAtQuestion: (id: CardId) => void;
openSnippetModalWithSelectedText: () => void;
insertSnippet: (snippet: NativeQuerySnippet) => void;
setIsNativeEditorOpen?: (isOpen: boolean) => void;
setParameterValue: (parameterId: ParameterId, value: string) => void;
setParameterValueToDefault: (parameterId: ParameterId) => void;
onOpenModal: (modalType: QueryModalType) => void;
toggleDataReference: () => void;
toggleTemplateTagsEditor: () => void;
toggleSnippetSidebar: () => void;
cancelQuery?: () => void;
closeSnippetModal: () => void;
onSetDatabaseId?: (id: DatabaseId) => void;
queryBuilderMode: QueryBuilderMode;
mode: Mode;
showLeftSidebar: boolean;
showRightSidebar: boolean;
isLiveResizable: boolean;
parameters: UiParameter[];
updateQuestion: (question: Question, opts?: { run?: boolean }) => void;
}
export const ViewMainContainer = (props: ViewMainContainerProps) => {
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 (
<Box
component="main"
className={cx(ViewMainContainerS.QueryBuilderMain, {
[ViewMainContainerS.isSidebarOpen]: isSidebarOpen,
})}
data-testid="query-builder-main"
>
{isNative ? (
<ViewNativeQueryEditor {...props} />
) : (
<SyncedParametersList
className={ViewMainContainerS.StyledSyncedParametersList}
parameters={parameters}
setParameterValue={setParameterValue}
commitImmediately
/>
)}
<DebouncedFrame
className={ViewMainContainerS.StyledDebouncedFrame}
enabled={!isLiveResizable}
>
<QueryVisualization
{...props}
className={CS.spread}
// @ts-expect-error - fix mode type
mode={queryMode}
onUpdateQuestion={updateQuestion}
/>
</DebouncedFrame>
<TimeseriesChrome question={question} updateQuestion={updateQuestion} />
<ViewFooter className={CS.flexNoShrink} />
</Box>
);
};

View File

@@ -1,53 +0,0 @@
/* eslint-disable react/prop-types */
import { useSelector } from "metabase/lib/redux";
import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor";
import { getHighlightedNativeQueryLineNumbers } from "metabase/query_builder/selectors";
import { Box } from "metabase/ui";
import * as Lib from "metabase-lib";
import NativeQueryEditorS from "./ViewNativeQueryEditor.module.css";
export const ViewNativeQueryEditor = (props) => {
const {
question,
height,
isDirty,
isNativeEditorOpen,
card,
setParameterValueToDefault,
onSetDatabaseId,
} = props;
const legacyNativeQuery = question.legacyNativeQuery();
const highlightedLineNumbers = useSelector(
getHighlightedNativeQueryLineNumbers,
);
// 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 (
<Box className={NativeQueryEditorS.NativeQueryEditorContainer}>
<NativeQueryEditor
{...props}
query={legacyNativeQuery}
viewHeight={height}
highlightedLineNumbers={highlightedLineNumbers}
isOpen={legacyNativeQuery.isEmpty() || isDirty}
isInitiallyOpen={isNativeEditorOpen}
datasetQuery={card && card.dataset_query}
setParameterValueToDefault={setParameterValueToDefault}
onSetDatabaseId={onSetDatabaseId}
/>
</Box>
);
};

View File

@@ -0,0 +1,114 @@
import type { ResizableBoxProps } from "react-resizable";
import { useSelector } from "metabase/lib/redux";
import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor";
import type {
Features,
SelectionRange,
} from "metabase/query_builder/components/NativeQueryEditor/types";
import type { QueryModalType } from "metabase/query_builder/constants";
import { getHighlightedNativeQueryLineNumbers } from "metabase/query_builder/selectors";
import { Box } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
import type {
Card,
CardId,
DatabaseId,
NativeQuerySnippet,
ParameterId,
} from "metabase-types/api";
import NativeQueryEditorS from "./ViewNativeQueryEditor.module.css";
interface ViewNativeQueryEditorProps {
height?: number;
card?: Card;
question: Question;
query: NativeQuery;
nativeEditorSelectedText?: string;
modalSnippet?: NativeQuerySnippet;
viewHeight: number;
highlightedLineNumbers?: number[];
isInitiallyOpen?: boolean;
isNativeEditorOpen: boolean;
isRunnable: boolean;
isRunning: boolean;
isResultDirty: boolean;
isShowingDataReference: boolean;
isShowingTemplateTagsEditor: boolean;
isShowingSnippetSidebar: boolean;
readOnly?: boolean;
canChangeDatabase?: boolean;
hasTopBar?: boolean;
hasParametersList?: boolean;
hasEditingSidebar?: boolean;
sidebarFeatures?: Features;
resizable?: boolean;
resizableBoxProps?: Partial<Omit<ResizableBoxProps, "axis">>;
editorContext?: "question";
runQuery: () => void;
toggleEditor: () => void;
handleResize: () => void;
setDatasetQuery: (query: NativeQuery) => Promise<Question>;
runQuestionQuery: (opts?: {
overrideWithQuestion?: Question;
shouldUpdateUrl?: boolean;
}) => void;
setNativeEditorSelectedRange: (range: SelectionRange[]) => void;
openDataReferenceAtQuestion: (id: CardId) => void;
openSnippetModalWithSelectedText: () => void;
insertSnippet: (snippet: NativeQuerySnippet) => void;
setIsNativeEditorOpen?: (isOpen: boolean) => void;
setParameterValue: (parameterId: ParameterId, value: string) => void;
onOpenModal: (modalType: QueryModalType) => void;
toggleDataReference: () => void;
toggleTemplateTagsEditor: () => void;
toggleSnippetSidebar: () => void;
cancelQuery?: () => void;
closeSnippetModal: () => void;
onSetDatabaseId?: (id: DatabaseId) => void;
}
export const ViewNativeQueryEditor = (props: ViewNativeQueryEditorProps) => {
const { question, height, isNativeEditorOpen, card, onSetDatabaseId } = props;
const legacyNativeQuery = question.legacyNativeQuery();
const highlightedLineNumbers = useSelector(
getHighlightedNativeQueryLineNumbers,
);
// 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 (
<Box className={NativeQueryEditorS.NativeQueryEditorContainer}>
<NativeQueryEditor
{...props}
query={legacyNativeQuery}
viewHeight={height}
highlightedLineNumbers={highlightedLineNumbers}
isInitiallyOpen={isNativeEditorOpen}
datasetQuery={card && card.dataset_query}
onSetDatabaseId={onSetDatabaseId}
/>
</Box>
);
};

View File

@@ -37,8 +37,6 @@ interface ViewTitleHeaderProps {
queryBuilderMode: QueryBuilderMode;
isBookmarked: boolean;
toggleBookmark: () => void;
isSaved: boolean;
isModelOrMetric: boolean;
isRunnable: boolean;
isRunning: boolean;
isNativeEditorOpen: boolean;
@@ -62,9 +60,6 @@ interface ViewTitleHeaderProps {
datasetEditorTab?: DatasetEditorTab;
},
) => void;
areFiltersExpanded: boolean;
onExpandFilters: () => void;
onCollapseFilters: () => void;
isShowingQuestionInfoSidebar: boolean;
onCloseQuestionInfo: () => void;
@@ -209,12 +204,12 @@ export function ViewTitleHeader({
queryBuilderMode,
isObjectDetail,
}) && (
<QuestionFiltersHeader
expanded={areFiltersExpanded}
question={question}
updateQuestion={updateQuestion}
/>
)}
<QuestionFiltersHeader
expanded={areFiltersExpanded}
question={question}
updateQuestion={updateQuestion}
/>
)}
</>
);
}

View File

@@ -145,15 +145,15 @@ type VisualizationOwnProps = {
isSlow?: CardSlownessStatus;
isVisible?: boolean;
metadata?: Metadata;
mode?: ClickActionModeGetter | Mode | QueryClickActionsMode;
mode?: ClickActionModeGetter | Mode | QueryClickActionsMode | null;
onEditSummary?: () => void;
query?: NativeQuery;
rawSeries?: (
| SingleSeries
| {
card: Card<DatasetQuery>;
}
)[];
card: Card<DatasetQuery>;
}
)[] | null;
visualizerRawSeries?: RawSeries;
replacementContent?: JSX.Element | null;
selectedTimelineEventIds?: number[];
@@ -171,7 +171,7 @@ type VisualizationOwnProps = {
onChangeCardAndRun?: ((opts: OnChangeCardAndRunOpts) => void) | null;
onHeaderColumnReorder?: (columnName: string) => void;
onChangeLocation?: (location: Location) => void;
onUpdateQuestion?: () => void;
onUpdateQuestion?: (question: Question) => void;
onUpdateVisualizationSettings?: (
settings: VisualizationSettings,
question?: Question,
@@ -228,8 +228,8 @@ const deriveStateFromProps = (props: VisualizationProps) => {
const transformed = props.rawSeries
? getVisualizationTransformed(
extractRemappings(props.rawSeries as RawSeries),
)
extractRemappings(props.rawSeries as RawSeries),
)
: null;
const series = transformed?.series ?? null;
@@ -352,7 +352,7 @@ class Visualization extends PureComponent<
let warnings = state.warnings || [];
if (state.series && state.series[0].card.display !== "table") {
warnings = warnings.concat(
rawSeries
(rawSeries ?? [])
.filter(
(s): s is SingleSeries =>
"data" in s && s.data && s.data.rows_truncated != null,
@@ -403,7 +403,8 @@ class Visualization extends PureComponent<
| ClickActionModeGetter
| Mode
| QueryClickActionsMode
| undefined,
| undefined
| null,
question: Question | undefined,
) {
const modeOrQueryMode =
@@ -441,10 +442,10 @@ class Visualization extends PureComponent<
const clicked = isVisualizerDashboardCard(dashcard)
? formatVisualizerClickObject(
clickedObject,
visualizerRawSeries,
dashcard.visualization_settings.visualization.columnValuesMapping,
)
clickedObject,
visualizerRawSeries,
dashcard.visualization_settings.visualization.columnValuesMapping,
)
: clickedObject;
const card = this.findCardById(clicked.cardId);
@@ -454,15 +455,15 @@ class Visualization extends PureComponent<
return mode
? mode.actionsForClick(
{
...clicked,
extraData: {
...getExtraDataForClick(clicked),
isRawTable,
},
{
...clicked,
extraData: {
...getExtraDataForClick(clicked),
isRawTable,
},
this.state.computedSettings,
)
},
this.state.computedSettings,
)
: [];
}
@@ -470,9 +471,12 @@ class Visualization extends PureComponent<
const { dashcard, rawSeries = [], visualizerRawSeries = [] } = this.props;
const isVisualizerViz = isVisualizerDashboardCard(dashcard);
const lookupSeries = isVisualizerViz ? visualizerRawSeries : rawSeries;
if (!lookupSeries) {
return undefined;
}
return (
lookupSeries.find((series) => series.card.id === cardId)?.card ??
lookupSeries[0].card
lookupSeries[0]?.card
);
};
@@ -529,8 +533,13 @@ class Visualization extends PureComponent<
nextCard,
objectId,
}: Pick<OnChangeCardAndRunOpts, "nextCard" | "objectId">) => {
const previousCard = this.findCardById(nextCard?.id);
if (!previousCard) {
return;
}
this.props.onChangeCardAndRun?.({
previousCard: this.findCardById(nextCard?.id),
previousCard,
nextCard,
objectId,
});
@@ -625,7 +634,7 @@ class Visualization extends PureComponent<
onOpenTimelines,
onSelectTimelineEvents,
onTogglePreviewing,
onUpdateVisualizationSettings = () => {},
onUpdateVisualizationSettings = () => { },
onUpdateWarnings,
titleMenuItems,
} = this.props;

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import type React from "react";
import type { OptionsType } from "metabase/lib/formatting/types";
import type { IconName, IconProps } from "metabase/ui";
import type { IconName, IconProps, MantineTheme } from "metabase/ui";
import type { Mode } from "metabase/visualizations/click-actions/Mode";
import type {
TextHeightMeasurer,
@@ -200,11 +200,11 @@ export type VisualizationPassThroughProps = {
tableHeaderHeight?: number;
scrollToColumn?: number;
renderTableHeader?: (
column: number,
column: DatasetColumn,
index: number,
theme: unknown,
theme: MantineTheme,
) => ReactNode;
mode?: ClickActionModeGetter | Mode | QueryClickActionsMode;
mode?: ClickActionModeGetter | Mode | QueryClickActionsMode | null;
renderEmptyMessage?: boolean;
// frontend/src/metabase/dashboard/components/DashCard/DashCardVisualization.tsx