Compare commits
4 Commits
split-api-
...
native-edi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10e0798b43 | ||
|
|
86d513d9ef | ||
|
|
1a9ce05cc4 | ||
|
|
4bcd59e02b |
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface ToasterProps extends HTMLAttributes<HTMLDivElement> {
|
||||
confirmText?: string;
|
||||
isShown: boolean;
|
||||
fixed?: boolean;
|
||||
className: string;
|
||||
className?: string;
|
||||
onConfirm: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -7,3 +7,10 @@ export type SelectionRange = {
|
||||
start: Location;
|
||||
end: Location;
|
||||
};
|
||||
|
||||
export type Features = {
|
||||
dataReference?: boolean;
|
||||
variables?: boolean;
|
||||
snippets?: boolean;
|
||||
promptInput?: boolean;
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }, () => (
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user