Compare commits
8 Commits
python-run
...
query-buil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4f410fe2e | ||
|
|
2032155e01 | ||
|
|
bab2613614 | ||
|
|
cda59efae8 | ||
|
|
bf8eff4b81 | ||
|
|
820c4a3526 | ||
|
|
efda4aa47e | ||
|
|
4614779a95 |
@@ -403,8 +403,11 @@ class Question {
|
||||
* The `visualization_settings` in card object doesn't contain default settings,
|
||||
* so you can provide the complete visualization settings object to `alertType`
|
||||
* for taking those into account
|
||||
*
|
||||
*/
|
||||
alertType(visualizationSettings): NotificationTriggerType | null {
|
||||
alertType(
|
||||
visualizationSettings?: VisualizationSettings,
|
||||
): NotificationTriggerType | null {
|
||||
const display = this.display();
|
||||
|
||||
if (!this.canRun()) {
|
||||
|
||||
@@ -696,6 +696,11 @@ export type MetricCreatedEvent = ValidateEvent<{
|
||||
target_id: number | null;
|
||||
}>;
|
||||
|
||||
export type ChartGeneratedEvent = ValidateEvent<{
|
||||
event: "chart_generated";
|
||||
event_detail: VisualizationDisplay;
|
||||
}>;
|
||||
|
||||
export type DataStudioEvent =
|
||||
| DataStudioOpenedEvent
|
||||
| DataStudioLibraryCreatedEvent
|
||||
@@ -783,4 +788,5 @@ export type SimpleEvent =
|
||||
| RemoteSyncEvent
|
||||
| ClickActionPerformedEvent
|
||||
| DataStudioEvent
|
||||
| UnsavedChangesWarningDisplayedEvent;
|
||||
| UnsavedChangesWarningDisplayedEvent
|
||||
| ChartGeneratedEvent;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const createMockQueryBuilderUIControlsState = (
|
||||
isShowingRawTable: false,
|
||||
isNativeEditorOpen: false,
|
||||
queryBuilderMode: "view",
|
||||
previousQueryBuilderMode: false,
|
||||
previousQueryBuilderMode: null,
|
||||
snippetCollectionId: null,
|
||||
datasetEditorTab: "query",
|
||||
isShowingNotebookNativePreview: false,
|
||||
@@ -35,6 +35,8 @@ export const createMockQueryBuilderUIControlsState = (
|
||||
dataReferenceStack: null,
|
||||
highlightedNativeQueryLineNumbers: [],
|
||||
isShowingListViewConfiguration: false,
|
||||
questionDetailsTimelineDrawerState: null,
|
||||
nativeEditorSelectedRange: [],
|
||||
...opts,
|
||||
});
|
||||
|
||||
@@ -76,6 +78,7 @@ export const createMockQueryBuilderState = (
|
||||
selectedTimelineEventIds: [],
|
||||
|
||||
metadataDiff: {},
|
||||
visibleTimelineEventIds: [],
|
||||
|
||||
currentState: null,
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Deferred } from "metabase/lib/promise";
|
||||
import type { DataReferenceItem } from "metabase/query_builder/components/dataref/types";
|
||||
import type { QueryModalType } from "metabase/query_builder/constants";
|
||||
import type { Widget } from "metabase/visualizations/types";
|
||||
import type {
|
||||
@@ -6,6 +7,7 @@ import type {
|
||||
CollectionItemModel,
|
||||
Dataset,
|
||||
Field,
|
||||
NativeQuerySnippet,
|
||||
ParameterValuesMap,
|
||||
TimelineEventId,
|
||||
} from "metabase-types/api";
|
||||
@@ -23,6 +25,16 @@ export type ForeignKeyReference = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
type Position = {
|
||||
row: number;
|
||||
column: number;
|
||||
};
|
||||
|
||||
export type Range = {
|
||||
start: Position;
|
||||
end: Position;
|
||||
};
|
||||
|
||||
export interface QueryBuilderUIControls {
|
||||
isModifiedFromNotebook: boolean;
|
||||
isShowingDataReference: boolean;
|
||||
@@ -40,8 +52,8 @@ export interface QueryBuilderUIControls {
|
||||
isShowingAIQuestionAnalysisSidebar: boolean;
|
||||
initialChartSetting: InitialChartSettingState;
|
||||
isShowingRawTable: boolean;
|
||||
queryBuilderMode: QueryBuilderMode | false;
|
||||
previousQueryBuilderMode: boolean;
|
||||
queryBuilderMode: QueryBuilderMode;
|
||||
previousQueryBuilderMode: QueryBuilderMode | null;
|
||||
snippetCollectionId: number | null;
|
||||
datasetEditorTab: DatasetEditorTab;
|
||||
isShowingNotebookNativePreview: boolean;
|
||||
@@ -49,9 +61,16 @@ export interface QueryBuilderUIControls {
|
||||
showSidebarTitle: boolean;
|
||||
modal: QueryModalType | null;
|
||||
modalContext: TimelineEventId | null;
|
||||
dataReferenceStack: null;
|
||||
modalSnippet?:
|
||||
| NativeQuerySnippet
|
||||
| Partial<Omit<NativeQuerySnippet, "id">>
|
||||
| null;
|
||||
dataReferenceStack: DataReferenceItem[] | null;
|
||||
highlightedNativeQueryLineNumbers: number[];
|
||||
isShowingListViewConfiguration: boolean;
|
||||
scrollToLastColumn?: boolean;
|
||||
questionDetailsTimelineDrawerState: string | null;
|
||||
nativeEditorSelectedRange: Range[];
|
||||
}
|
||||
|
||||
export interface QueryBuilderLoadingControls {
|
||||
@@ -94,4 +113,6 @@ export interface QueryBuilderState {
|
||||
cardId?: number;
|
||||
serializedCard: string;
|
||||
} | null;
|
||||
|
||||
visibleTimelineEventIds: TimelineEventId[];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { t } from "ttag";
|
||||
|
||||
import { Button } from "metabase/common/components/Button";
|
||||
import { DataReference } from "metabase/query_builder/components/dataref/DataReference";
|
||||
import type { DataReferenceItem } from "metabase/query_builder/components/dataref/types";
|
||||
import { Tooltip } from "metabase/ui";
|
||||
|
||||
export const DataReferenceInline = ({
|
||||
@@ -12,10 +13,10 @@ export const DataReferenceInline = ({
|
||||
onClose?: () => void;
|
||||
onBack?: () => void;
|
||||
}) => {
|
||||
const [dataRefStack, setDataRefStack] = useState<any[]>([]);
|
||||
const [dataRefStack, setDataRefStack] = useState<DataReferenceItem[]>([]);
|
||||
|
||||
const pushRefStack = (ref: any) => {
|
||||
setDataRefStack([...dataRefStack, ref]);
|
||||
const pushRefStack = (item: DataReferenceItem) => {
|
||||
setDataRefStack([...dataRefStack, item]);
|
||||
};
|
||||
|
||||
const popRefStack = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as Lib from "metabase-lib";
|
||||
import type Field from "metabase-lib/v1/metadata/Field";
|
||||
import { isDate, isID, isNumber } from "metabase-lib/v1/types/utils/isa";
|
||||
import type { DatasetColumn } from "metabase-types/api";
|
||||
import type { Field as ApiField, DatasetColumn } from "metabase-types/api";
|
||||
|
||||
import { DateTimeFingerprint } from "./DateTimeFingerprint";
|
||||
import { GlobalFingerprint } from "./GlobalFingerprint";
|
||||
@@ -14,7 +14,7 @@ type BaseProps = {
|
||||
};
|
||||
|
||||
type TableColumnFingerprintInfoProps = BaseProps & {
|
||||
field: Field | DatasetColumn;
|
||||
field: Field | DatasetColumn | ApiField;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { t } from "ttag";
|
||||
|
||||
import * as Lib from "metabase-lib";
|
||||
import type Field from "metabase-lib/v1/metadata/Field";
|
||||
import type { DatasetColumn } from "metabase-types/api";
|
||||
import type { Field as ApiField, DatasetColumn } from "metabase-types/api";
|
||||
|
||||
import {
|
||||
QueryColumnFingerprintInfo,
|
||||
@@ -15,7 +15,7 @@ import { InfoContainer, Small } from "./ColumnInfo.styled";
|
||||
|
||||
export type TableColumnInfoProps = {
|
||||
className?: string;
|
||||
field: Field | DatasetColumn;
|
||||
field: Field | DatasetColumn | ApiField;
|
||||
timezone?: string;
|
||||
showAllFieldValues?: boolean;
|
||||
showFingerprintInfo?: boolean;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
listModelIndexes,
|
||||
} from "metabase/api";
|
||||
import type Question from "metabase-lib/v1/Question";
|
||||
import type { Field, FieldReference } from "metabase-types/api";
|
||||
import type { DatasetColumn, Field, FieldReference } from "metabase-types/api";
|
||||
import type { ModelIndex } from "metabase-types/api/modelIndexes";
|
||||
import type { Dispatch } from "metabase-types/store";
|
||||
|
||||
@@ -125,14 +125,14 @@ function getIndexIdsToRemove(
|
||||
return indexIdsToRemove;
|
||||
}
|
||||
|
||||
export function cleanIndexFlags(fields: Field[] = []) {
|
||||
export function cleanIndexFlags(fields: (DatasetColumn | Field)[] = []) {
|
||||
const indexesToClean = fields.reduce(
|
||||
(
|
||||
indexesToClean: number[],
|
||||
field: FieldWithMaybeIndex,
|
||||
field: FieldWithMaybeIndex | DatasetColumn,
|
||||
thisIndex: number,
|
||||
) => {
|
||||
if (field.should_index !== undefined) {
|
||||
if ("should_index" in field && field.should_index !== undefined) {
|
||||
indexesToClean.push(thisIndex);
|
||||
}
|
||||
return indexesToClean;
|
||||
|
||||
@@ -67,7 +67,7 @@ export const softReloadCard = createThunkAction(SOFT_RELOAD_CARD, () => {
|
||||
const outdatedCard = getCard(getState());
|
||||
|
||||
const action = await dispatch(
|
||||
Questions.actions.fetch({ id: outdatedCard.id }, { reload: true }),
|
||||
Questions.actions.fetch({ id: outdatedCard?.id }, { reload: true }),
|
||||
);
|
||||
|
||||
return Questions.HACK_getObjectFromAction(action);
|
||||
@@ -295,7 +295,7 @@ export const apiUpdateQuestion = (
|
||||
const { isNative } = Lib.queryDisplayInfo(question.query());
|
||||
|
||||
if (!isNative) {
|
||||
rerunQuery = rerunQuery ?? isResultDirty;
|
||||
rerunQuery = rerunQuery ?? isResultDirty ?? false;
|
||||
}
|
||||
|
||||
const submittableQuestion = getSubmittableQuestion(getState(), question);
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from "metabase-types/api";
|
||||
import type { Dispatch, GetState } from "metabase-types/store";
|
||||
|
||||
import type { DataReferenceItem } from "../components/dataref/types";
|
||||
import {
|
||||
getDataReferenceStack,
|
||||
getNativeEditorCursorOffset,
|
||||
@@ -44,11 +45,10 @@ export const PUSH_DATA_REFERENCE_STACK =
|
||||
"metabase/qb/PUSH_DATA_REFERENCE_STACK";
|
||||
export const pushDataReferenceStack = createThunkAction(
|
||||
PUSH_DATA_REFERENCE_STACK,
|
||||
(item: { type: string; item: unknown }) =>
|
||||
(dispatch: Dispatch, getState: GetState) => {
|
||||
const stack = getDataReferenceStack(getState());
|
||||
dispatch(setDataReferenceStack(stack.concat([item])));
|
||||
},
|
||||
(item: DataReferenceItem) => (dispatch: Dispatch, getState: GetState) => {
|
||||
const stack = getDataReferenceStack(getState());
|
||||
dispatch(setDataReferenceStack(stack.concat([item])));
|
||||
},
|
||||
);
|
||||
|
||||
export const OPEN_DATA_REFERENCE_AT_QUESTION =
|
||||
@@ -143,14 +143,17 @@ export const insertSnippet =
|
||||
return;
|
||||
}
|
||||
const query = question.legacyNativeQuery() as NativeQuery;
|
||||
const nativeEditorCursorOffset = getNativeEditorCursorOffset(getState());
|
||||
const nativeEditorSelectedText = getNativeEditorSelectedText(getState());
|
||||
const queryText = query.queryText();
|
||||
const nativeEditorCursorOffset =
|
||||
getNativeEditorCursorOffset(getState()) ?? queryText.length;
|
||||
const nativeEditorSelectedText =
|
||||
getNativeEditorSelectedText(getState()) ?? "";
|
||||
const selectionStart =
|
||||
nativeEditorCursorOffset - (nativeEditorSelectedText || "").length;
|
||||
nativeEditorCursorOffset - nativeEditorSelectedText.length;
|
||||
const newText =
|
||||
query.queryText().slice(0, selectionStart) +
|
||||
queryText.slice(0, selectionStart) +
|
||||
`{{snippet: ${name}}}` +
|
||||
query.queryText().slice(nativeEditorCursorOffset);
|
||||
queryText.slice(nativeEditorCursorOffset);
|
||||
const datasetQuery = query.setQueryText(newText).datasetQuery();
|
||||
dispatch(updateQuestion(question.setDatasetQuery(datasetQuery)));
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export const loadObjectDetailFKReferences = createThunkAction(
|
||||
return null;
|
||||
}
|
||||
|
||||
const card: Card = getCard(state);
|
||||
const card = getCard(state);
|
||||
const queryResult = getFirstQueryResult(state);
|
||||
|
||||
async function getFKCount(
|
||||
@@ -174,18 +174,20 @@ export const loadObjectDetailFKReferences = createThunkAction(
|
||||
|
||||
// run a query on FK origin table where FK origin field = objectDetailIdValue
|
||||
const fkReferences: Record<FieldId, FKInfo | undefined> = {};
|
||||
for (let i = 0; i < tableForeignKeys.length; i++) {
|
||||
const fk = tableForeignKeys[i];
|
||||
const info = await getFKCount(card, fk);
|
||||
if (card) {
|
||||
for (let i = 0; i < tableForeignKeys.length; i++) {
|
||||
const fk = tableForeignKeys[i];
|
||||
const info = await getFKCount(card, fk);
|
||||
|
||||
fkReferences[fk.origin_id] = info;
|
||||
fkReferences[fk.origin_id] = info;
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible that while we were running those queries, the object
|
||||
// detail id changed. If so, these fk reference are stale and we shouldn't
|
||||
// put them in state. The detail id is used in the query so we check that.
|
||||
const updatedQueryResult = getFirstQueryResult(getState());
|
||||
if (!_.isEqual(queryResult.json_query, updatedQueryResult.json_query)) {
|
||||
if (!_.isEqual(queryResult?.json_query, updatedQueryResult?.json_query)) {
|
||||
return null;
|
||||
}
|
||||
return fkReferences;
|
||||
|
||||
@@ -205,14 +205,16 @@ export const queryCompleted = (question: Question, queryResults: Dataset[]) => {
|
||||
|
||||
const originalQuestion = getOriginalQuestionWithParameterValues(getState());
|
||||
const { isEditable } = Lib.queryDisplayInfo(question.query());
|
||||
const isDirty = isEditable && question.isDirtyComparedTo(originalQuestion);
|
||||
const isDirty =
|
||||
isEditable &&
|
||||
(!originalQuestion || question.isDirtyComparedTo(originalQuestion));
|
||||
|
||||
if (isDirty) {
|
||||
const series = [{ card: question.card(), data, error }];
|
||||
const previousSeries =
|
||||
prevCard && (prevData || prevError)
|
||||
prevCard && prevData
|
||||
? [{ card: prevCard, data: prevData, error: prevError }]
|
||||
: null;
|
||||
: undefined;
|
||||
if (series && previousSeries) {
|
||||
question = question.setSettings(
|
||||
syncVizSettingsWithSeries(
|
||||
|
||||
@@ -54,43 +54,56 @@ export const setQueryBuilderMode =
|
||||
}
|
||||
};
|
||||
|
||||
export const onEditSummary = createAction("metabase/qb/EDIT_SUMMARY");
|
||||
export const onCloseSummary = createAction("metabase/qb/CLOSE_SUMMARY");
|
||||
export const ON_EDIT_SUMMARY = "metabase/qb/ON_EDIT_SUMMARY";
|
||||
export const onEditSummary = createAction(ON_EDIT_SUMMARY);
|
||||
|
||||
export const ON_CLOSE_SUMMARY = "metabase/qb/ON_CLOSE_SUMMARY";
|
||||
export const onCloseSummary = createAction(ON_CLOSE_SUMMARY);
|
||||
|
||||
export const OPEN_AI_QUESTION_ANALYSIS_SIDEBAR =
|
||||
"metabase/qb/OPEN_AI_QUESTION_ANALYSIS_SIDEBAR";
|
||||
export const onOpenAIQuestionAnalysisSidebar = createAction(
|
||||
"metabase/qb/OPEN_AI_QUESTION_ANALYSIS_SIDEBAR",
|
||||
OPEN_AI_QUESTION_ANALYSIS_SIDEBAR,
|
||||
);
|
||||
|
||||
export const CLOSE_AI_QUESTION_ANALYSIS_SIDEBAR =
|
||||
"metabase/qb/CLOSE_AI_QUESTION_ANALYSIS_SIDEBAR";
|
||||
export const onCloseAIQuestionAnalysisSidebar = createAction(
|
||||
"metabase/qb/CLOSE_AI_QUESTION_ANALYSIS_SIDEBAR",
|
||||
CLOSE_AI_QUESTION_ANALYSIS_SIDEBAR,
|
||||
);
|
||||
|
||||
export const onOpenChartSettings = createAction(
|
||||
"metabase/qb/OPEN_CHART_SETTINGS",
|
||||
);
|
||||
export const onCloseChartSettings = createAction(
|
||||
"metabase/qb/CLOSE_CHART_SETTINGS",
|
||||
);
|
||||
export const onOpenChartType = createAction("metabase/qb/OPEN_CHART_TYPE");
|
||||
export const OPEN_CHART_SETTINGS = "metabase/qb/OPEN_CHART_SETTINGS";
|
||||
export const onOpenChartSettings = createAction(OPEN_CHART_SETTINGS);
|
||||
|
||||
export const onOpenQuestionInfo = createAction(
|
||||
"metabase/qb/OPEN_QUESTION_INFO",
|
||||
);
|
||||
export const onCloseQuestionInfo = createAction(
|
||||
"metabase/qb/CLOSE_QUESTION_INFO",
|
||||
);
|
||||
export const CLOSE_CHART_SETTINGS = "metabase/qb/CLOSE_CHART_SETTINGS";
|
||||
export const onCloseChartSettings = createAction(CLOSE_CHART_SETTINGS);
|
||||
|
||||
export const onOpenQuestionSettings = createAction(
|
||||
"metabase/qb/OPEN_QUESTION_SETTINGS",
|
||||
);
|
||||
export const onCloseQuestionSettings = createAction(
|
||||
"metabase/qb/CLOSE_QUESTION_SETTINGS",
|
||||
);
|
||||
export const OPEN_CHART_TYPE = "metabase/qb/OPEN_CHART_TYPE";
|
||||
export const onOpenChartType = createAction(OPEN_CHART_TYPE);
|
||||
|
||||
export const onOpenTimelines = createAction("metabase/qb/OPEN_TIMELINES");
|
||||
export const onCloseTimelines = createAction("metabase/qb/CLOSE_TIMELINES");
|
||||
export const OPEN_QUESTION_INFO = "metabase/qb/OPEN_QUESTION_INFO";
|
||||
export const onOpenQuestionInfo = createAction(OPEN_QUESTION_INFO);
|
||||
|
||||
export const onCloseChartType = createAction("metabase/qb/CLOSE_CHART_TYPE");
|
||||
export const onCloseSidebars = createAction("metabase/qb/CLOSE_SIDEBARS");
|
||||
export const CLOSE_QUESTION_INFO = "metabase/qb/CLOSE_QUESTION_INFO";
|
||||
export const onCloseQuestionInfo = createAction(CLOSE_QUESTION_INFO);
|
||||
|
||||
export const OPEN_QUESTION_SETTINGS = "metabase/qb/OPEN_QUESTION_SETTINGS";
|
||||
export const onOpenQuestionSettings = createAction(OPEN_QUESTION_SETTINGS);
|
||||
|
||||
export const CLOSE_QUESTION_SETTINGS = "metabase/qb/CLOSE_QUESTION_SETTINGS";
|
||||
export const onCloseQuestionSettings = createAction(CLOSE_QUESTION_SETTINGS);
|
||||
|
||||
export const OPEN_TIMELINES = "metabase/qb/OPEN_TIMELINES";
|
||||
export const onOpenTimelines = createAction(OPEN_TIMELINES);
|
||||
|
||||
export const CLOSE_TIMELINES = "metabase/qb/CLOSE_TIMELINES";
|
||||
export const onCloseTimelines = createAction(CLOSE_TIMELINES);
|
||||
|
||||
export const CLOSE_CHART_TYPE = "metabase/qb/CLOSE_CHART_TYPE";
|
||||
export const onCloseChartType = createAction(CLOSE_CHART_TYPE);
|
||||
|
||||
export const CLOSE_SIDEBARS = "metabase/qb/CLOSE_SIDEBARS";
|
||||
export const onCloseSidebars = createAction(CLOSE_SIDEBARS);
|
||||
|
||||
export const SHOW_CHART_SETTINGS = "metabase/qb/SHOW_CHART_SETTINGS";
|
||||
export const showChartSettings = createAction(SHOW_CHART_SETTINGS);
|
||||
|
||||
@@ -78,7 +78,9 @@ export const updateUrl = createThunkAction(
|
||||
datasetEditorTab = getDatasetEditorTab(getState());
|
||||
}
|
||||
|
||||
const card = isAdHocModelOrMetric ? getCard(getState()) : question.card();
|
||||
const card = isAdHocModelOrMetric
|
||||
? getCard(getState())!
|
||||
: question.card();
|
||||
const newState = {
|
||||
card,
|
||||
cardId: question.id(),
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
import { trackSchemaEvent, trackSimpleEvent } from "metabase/lib/analytics";
|
||||
import * as Lib from "metabase-lib";
|
||||
import type Question from "metabase-lib/v1/Question";
|
||||
import type { NewQuestionSavedEvent } from "metabase-types/analytics";
|
||||
import type { Card } from "metabase-types/api";
|
||||
|
||||
export const trackNewQuestionSaved = (
|
||||
draftQuestion,
|
||||
createdQuestion,
|
||||
isBasedOnExistingQuestion,
|
||||
draftQuestion: Question,
|
||||
createdQuestion: Question,
|
||||
isBasedOnExistingQuestion?: boolean,
|
||||
) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: "new_question_saved",
|
||||
question_id: createdQuestion.id(),
|
||||
database_id: createdQuestion.databaseId(),
|
||||
visualization_type: createdQuestion.display(),
|
||||
type: draftQuestion.creationType(),
|
||||
source: isBasedOnExistingQuestion ? "existing_question" : "from_scratch",
|
||||
type: draftQuestion.creationType() as NewQuestionSavedEvent["type"],
|
||||
method: isBasedOnExistingQuestion ? "existing_question" : "from_scratch",
|
||||
});
|
||||
};
|
||||
|
||||
export const trackTurnIntoModelClicked = (question) => {
|
||||
export const trackTurnIntoModelClicked = (question: Question) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: "turn_into_model_clicked",
|
||||
question_id: question.id(),
|
||||
});
|
||||
};
|
||||
|
||||
export const trackNotebookNativePreviewShown = (question, isShown) => {
|
||||
export const trackNotebookNativePreviewShown = (
|
||||
question: Question,
|
||||
isShown?: boolean,
|
||||
) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: isShown
|
||||
? "notebook_native_preview_shown"
|
||||
@@ -33,7 +39,10 @@ export const trackNotebookNativePreviewShown = (question, isShown) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const trackColumnCombineViaShortcut = (query, question) => {
|
||||
export const trackColumnCombineViaShortcut = (
|
||||
query: Lib.Query,
|
||||
question?: Question,
|
||||
) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: "column_combine_via_shortcut",
|
||||
custom_expressions_used: ["concat"],
|
||||
@@ -42,7 +51,10 @@ export const trackColumnCombineViaShortcut = (query, question) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const trackColumnCombineViaPlusModal = (query, question) => {
|
||||
export const trackColumnCombineViaPlusModal = (
|
||||
query: Lib.Query,
|
||||
question?: Question,
|
||||
) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: "column_combine_via_plus_modal",
|
||||
custom_expressions_used: ["concat"],
|
||||
@@ -52,10 +64,10 @@ export const trackColumnCombineViaPlusModal = (query, question) => {
|
||||
};
|
||||
|
||||
export const trackColumnExtractViaShortcut = (
|
||||
query,
|
||||
stageIndex,
|
||||
extraction,
|
||||
question,
|
||||
query: Lib.Query,
|
||||
stageIndex: number,
|
||||
extraction: Lib.ColumnExtraction,
|
||||
question?: Question,
|
||||
) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: "column_extract_via_shortcut",
|
||||
@@ -70,10 +82,10 @@ export const trackColumnExtractViaShortcut = (
|
||||
};
|
||||
|
||||
export const trackColumnExtractViaPlusModal = (
|
||||
query,
|
||||
stageIndex,
|
||||
extraction,
|
||||
question,
|
||||
query: Lib.Query,
|
||||
stageIndex: number,
|
||||
extraction: Lib.ColumnExtraction,
|
||||
question?: Question,
|
||||
) => {
|
||||
trackSchemaEvent("question", {
|
||||
event: "column_extract_via_plus_modal",
|
||||
@@ -87,14 +99,14 @@ export const trackColumnExtractViaPlusModal = (
|
||||
});
|
||||
};
|
||||
|
||||
export const trackFirstNonTableChartGenerated = (card) => {
|
||||
export const trackFirstNonTableChartGenerated = (card: Card) => {
|
||||
trackSimpleEvent({
|
||||
event: "chart_generated",
|
||||
event_detail: card.display,
|
||||
});
|
||||
};
|
||||
|
||||
export const trackCardBookmarkAdded = (card) => {
|
||||
export const trackCardBookmarkAdded = (card: Card) => {
|
||||
trackSimpleEvent({
|
||||
event: "bookmark_added",
|
||||
event_detail: card.type,
|
||||
@@ -61,6 +61,9 @@ const defaultDatasetEditorProps = {
|
||||
toggleSnippetSidebar: noop,
|
||||
toggleTemplateTagsEditor: noop,
|
||||
runDirtyQuestionQuery: noop,
|
||||
dataReferenceStack: [],
|
||||
pushDataReferenceStack: noop,
|
||||
popDataReferenceStack: noop,
|
||||
};
|
||||
|
||||
const renderDatasetEditor = async (card: Card | UnsavedCard) => {
|
||||
|
||||
@@ -74,7 +74,13 @@ import type {
|
||||
VisualizationDisplay,
|
||||
VisualizationSettings,
|
||||
} from "metabase-types/api";
|
||||
import type { DatasetEditorTab, QueryBuilderMode } from "metabase-types/store";
|
||||
import type {
|
||||
DatasetEditorTab,
|
||||
QueryBuilderMode,
|
||||
State,
|
||||
} from "metabase-types/store";
|
||||
|
||||
import type { DataReferenceItem } from "../dataref/types";
|
||||
|
||||
import DatasetEditorS from "./DatasetEditor.module.css";
|
||||
import {
|
||||
@@ -95,11 +101,11 @@ export type DatasetEditorInnerProps = {
|
||||
metadata?: Metadata;
|
||||
metadataDiff: MetadataDiff;
|
||||
resultsMetadata?: ResultsMetadata | null;
|
||||
isMetadataDirty: boolean;
|
||||
isMetadataDirty?: boolean;
|
||||
result?: { error?: unknown } | null;
|
||||
height?: number;
|
||||
isDirty: boolean;
|
||||
isResultDirty: boolean;
|
||||
isResultDirty?: boolean;
|
||||
isRunning: boolean;
|
||||
setQueryBuilderMode: (
|
||||
mode: QueryBuilderMode,
|
||||
@@ -129,12 +135,16 @@ export type DatasetEditorInnerProps = {
|
||||
toggleDataReference: () => void;
|
||||
toggleSnippetSidebar: () => void;
|
||||
forwardedRef?: React.Ref<HTMLDivElement>;
|
||||
|
||||
dataReferenceStack: DataReferenceItem[];
|
||||
pushDataReferenceStack: (item: DataReferenceItem) => void;
|
||||
popDataReferenceStack: () => void;
|
||||
};
|
||||
|
||||
const INITIAL_NOTEBOOK_EDITOR_HEIGHT = 500;
|
||||
const TABLE_HEADER_HEIGHT = 45;
|
||||
|
||||
function mapStateToProps(state: any) {
|
||||
function mapStateToProps(state: State) {
|
||||
return {
|
||||
metadata: getMetadata(state),
|
||||
metadataDiff: getMetadataDiff(state),
|
||||
@@ -753,7 +763,9 @@ const DatasetEditorInnerView = (props: DatasetEditorInnerProps) => {
|
||||
isShowingDetailsOnlyColumns={datasetEditorTab !== "metadata"}
|
||||
hasMetadataPopovers={false}
|
||||
handleVisualizationClick={handleTableElementClick}
|
||||
tableHeaderHeight={isEditingColumns && TABLE_HEADER_HEIGHT}
|
||||
tableHeaderHeight={
|
||||
isEditingColumns ? TABLE_HEADER_HEIGHT : undefined
|
||||
}
|
||||
renderTableHeader={renderTableHeader}
|
||||
scrollToColumn={focusedFieldIndex + scrollToColumnModifier}
|
||||
renderEmptyMessage={isEditingColumns}
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
getInitialEditorHeight,
|
||||
} from "./utils";
|
||||
|
||||
type NativeQueryEditorProps = Omit<CodeMirrorEditorProps, "query"> & {
|
||||
export type NativeQueryEditorProps = Omit<CodeMirrorEditorProps, "query"> & {
|
||||
availableHeight?: number;
|
||||
canChangeDatabase?: boolean;
|
||||
cancelQuery?: () => void;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import type { ChangeEvent } from "react";
|
||||
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { DataSourceSelectors } from "metabase/query_builder/components/NativeQueryEditor/DataSourceSelectors";
|
||||
import { SyncedParametersList } from "metabase/query_builder/components/SyncedParametersList";
|
||||
import type { DatabaseId, TableId } from "metabase-types/api";
|
||||
|
||||
import type { NativeQueryEditorProps } from "../NativeQueryEditor";
|
||||
|
||||
export const NativeQueryEditor = ({
|
||||
canChangeDatabase = true,
|
||||
@@ -12,18 +16,18 @@ export const NativeQueryEditor = ({
|
||||
readOnly,
|
||||
setDatasetQuery,
|
||||
setParameterValue,
|
||||
}) => {
|
||||
const onChange = (evt) => {
|
||||
}: NativeQueryEditorProps) => {
|
||||
const onChange = (evt: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDatasetQuery(query.setQueryText(evt.target.value));
|
||||
};
|
||||
|
||||
const onDatabaseIdChange = (databaseId) => {
|
||||
const onDatabaseIdChange = (databaseId: DatabaseId) => {
|
||||
if (question.databaseId() !== databaseId) {
|
||||
setDatasetQuery(query.setDatabaseId(databaseId).setDefaultCollection());
|
||||
}
|
||||
};
|
||||
|
||||
const onTableIdChange = (tableId) => {
|
||||
const onTableIdChange = (tableId: TableId) => {
|
||||
const table = query.metadata().table(tableId);
|
||||
if (table && table.name !== query.collection()) {
|
||||
setDatasetQuery(query.setCollectionName(table.name));
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import cx from "classnames";
|
||||
import { useState } from "react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { useTimeout } from "react-use";
|
||||
import { c, t } from "ttag";
|
||||
|
||||
import EmptyCodeResult from "assets/img/empty-states/code.svg";
|
||||
import type { NavigateToNewCardParams } from "embedding-sdk-bundle/types";
|
||||
import { LoadingSpinner } from "metabase/common/components/LoadingSpinner";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import QueryBuilderS from "metabase/css/query_builder.module.css";
|
||||
@@ -13,8 +13,16 @@ import { SERVER_ERROR_TYPES } from "metabase/lib/errors";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import { getWhiteLabeledLoadingMessageFactory } from "metabase/selectors/whitelabel";
|
||||
import { Box, Flex, Stack, Text, Title } from "metabase/ui";
|
||||
import type {
|
||||
ClickActionsMode,
|
||||
QueryClickActionsMode,
|
||||
} from "metabase/visualizations/types";
|
||||
import * as Lib from "metabase-lib";
|
||||
import type Question from "metabase-lib/v1/Question";
|
||||
import { HARD_ROW_LIMIT } from "metabase-lib/v1/queries/utils";
|
||||
import type { Dataset, DatasetColumn, RawSeries } from "metabase-types/api";
|
||||
import type { EntityToken } from "metabase-types/api/entity";
|
||||
import type { QueryBuilderMode } from "metabase-types/store";
|
||||
|
||||
import { RunButtonWithTooltip } from "./RunButtonWithTooltip";
|
||||
import { VisualizationError } from "./VisualizationError";
|
||||
@@ -23,7 +31,47 @@ import { Warnings } from "./Warnings";
|
||||
|
||||
const SLOW_MESSAGE_TIMEOUT = 4000;
|
||||
|
||||
export function QueryVisualization(props) {
|
||||
type QueryVisualizationProps = {
|
||||
className?: string;
|
||||
question: Question;
|
||||
isRunnable?: boolean;
|
||||
isRunning?: boolean;
|
||||
isObjectDetail?: boolean;
|
||||
isDirty?: boolean;
|
||||
isResultDirty?: boolean;
|
||||
isNativeEditorOpen?: boolean;
|
||||
isDirtyStateShownForError?: boolean;
|
||||
runQuestionQuery?: () => void;
|
||||
cancelQuery?: () => void;
|
||||
queryBuilderMode?: QueryBuilderMode;
|
||||
result?: Dataset | Pick<Dataset, "error" | "error_type"> | null;
|
||||
rawSeries?: RawSeries | null;
|
||||
maxTableRows?: number;
|
||||
navigateToNewCardInsideQB?:
|
||||
| ((params: NavigateToNewCardParams) => Promise<void>)
|
||||
| null;
|
||||
onNavigateBack?: () => void;
|
||||
onHeaderColumnReorder?: (colIndex: number) => void;
|
||||
onUpdateQuestion?: () => void;
|
||||
isShowingDetailsOnlyColumns?: boolean;
|
||||
hasMetadataPopovers?: boolean;
|
||||
handleVisualizationClick?: (clicked: {
|
||||
element: unknown;
|
||||
column: DatasetColumn;
|
||||
}) => void;
|
||||
tableHeaderHeight?: number;
|
||||
renderTableHeader?: (
|
||||
column: DatasetColumn,
|
||||
columnIndex: number,
|
||||
) => JSX.Element;
|
||||
noHeader?: boolean;
|
||||
mode?: QueryClickActionsMode | ClickActionsMode | null | undefined;
|
||||
token?: EntityToken | null | undefined;
|
||||
scrollToColumn?: number;
|
||||
renderEmptyMessage?: boolean;
|
||||
};
|
||||
|
||||
export function QueryVisualization(props: QueryVisualizationProps) {
|
||||
const {
|
||||
className,
|
||||
question,
|
||||
@@ -38,7 +86,7 @@ export function QueryVisualization(props) {
|
||||
} = props;
|
||||
|
||||
const canRun = Lib.canRun(question.query(), question.type());
|
||||
const [warnings, setWarnings] = useState([]);
|
||||
const [warnings, setWarnings] = useState<string[]>([]);
|
||||
const isDirtyStateShown =
|
||||
canRun &&
|
||||
isResultDirty &&
|
||||
@@ -91,12 +139,14 @@ export function QueryVisualization(props) {
|
||||
) : result?.data ? (
|
||||
<VisualizationResult
|
||||
{...props}
|
||||
result={result as Dataset}
|
||||
rawSeries={props.rawSeries ?? undefined}
|
||||
maxTableRows={maxTableRows}
|
||||
className={CS.spread}
|
||||
onUpdateWarnings={setWarnings}
|
||||
/>
|
||||
) : !isRunning && !isDirtyStateShown ? (
|
||||
<VisualizationEmptyState className={CS.spread}>
|
||||
<VisualizationEmptyState>
|
||||
{t`Here's where your results will appear`}
|
||||
</VisualizationEmptyState>
|
||||
) : null}
|
||||
@@ -105,7 +155,7 @@ export function QueryVisualization(props) {
|
||||
);
|
||||
}
|
||||
|
||||
const VisualizationEmptyState = ({ children }) => {
|
||||
const VisualizationEmptyState = ({ children }: { children: ReactNode }) => {
|
||||
const keyboardShortcut = getRunQueryShortcut();
|
||||
|
||||
return (
|
||||
@@ -133,7 +183,7 @@ export function VisualizationRunningState({ className = "" }) {
|
||||
|
||||
// show the slower loading message only when the loadingMessage is
|
||||
// not customized
|
||||
const message = getLoadingMessage(isSlow());
|
||||
const message = getLoadingMessage(isSlow() ?? false);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -153,12 +203,18 @@ export function VisualizationRunningState({ className = "" }) {
|
||||
|
||||
export const VisualizationDirtyState = ({
|
||||
className,
|
||||
result,
|
||||
isRunning,
|
||||
isResultDirty,
|
||||
runQuestionQuery,
|
||||
cancelQuery,
|
||||
hidden,
|
||||
}: {
|
||||
className?: string;
|
||||
isRunning?: boolean;
|
||||
isResultDirty?: boolean;
|
||||
runQuestionQuery: () => void;
|
||||
cancelQuery: () => void;
|
||||
hidden?: boolean;
|
||||
}) => {
|
||||
const keyboardShortcut = getRunQueryShortcut();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import { t } from "ttag";
|
||||
|
||||
import { duration } from "metabase/lib/formatting";
|
||||
@@ -1,177 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import cx from "classnames";
|
||||
import { Component } from "react";
|
||||
import { jt, t } from "ttag";
|
||||
import _ from "underscore";
|
||||
|
||||
import { ErrorMessage } from "metabase/common/components/ErrorMessage";
|
||||
import ButtonsS from "metabase/css/components/buttons.module.css";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { isEmbeddingSdk } from "metabase/embedding-sdk/config";
|
||||
import { CreateOrEditQuestionAlertModalWithQuestion } from "metabase/notifications/modals/CreateOrEditQuestionAlertModal/CreateOrEditQuestionAlertModal";
|
||||
import Visualization from "metabase/visualizations/components/Visualization";
|
||||
import * as Lib from "metabase-lib";
|
||||
import { ALERT_TYPE_ROWS } from "metabase-lib/v1/Alert";
|
||||
import { datasetContainsNoResults } from "metabase-lib/v1/queries/utils/dataset";
|
||||
|
||||
const ALLOWED_VISUALIZATION_PROPS = [
|
||||
// Table
|
||||
"isShowingDetailsOnlyColumns",
|
||||
// Table Interactive
|
||||
"hasMetadataPopovers",
|
||||
"tableHeaderHeight",
|
||||
"scrollToColumn",
|
||||
"renderTableHeader",
|
||||
"mode",
|
||||
"renderEmptyMessage",
|
||||
"zoomedRowIndex",
|
||||
// Legend
|
||||
"hideLegend",
|
||||
];
|
||||
|
||||
export class VisualizationResult extends Component {
|
||||
state = {
|
||||
showCreateAlertModal: false,
|
||||
};
|
||||
|
||||
showCreateAlertModal = () => {
|
||||
this.setState({ showCreateAlertModal: true });
|
||||
};
|
||||
|
||||
onCloseCreateAlertModal = () => {
|
||||
this.setState({ showCreateAlertModal: false });
|
||||
};
|
||||
|
||||
getObjectDetailData = (series) => {
|
||||
return [
|
||||
{
|
||||
...series[0],
|
||||
card: { ...series[0].card, display: "object" },
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
question,
|
||||
token,
|
||||
isDirty,
|
||||
queryBuilderMode,
|
||||
navigateToNewCardInsideQB,
|
||||
result,
|
||||
rawSeries,
|
||||
timelineEvents,
|
||||
selectedTimelineEventIds,
|
||||
onNavigateBack,
|
||||
className,
|
||||
isRunning,
|
||||
isShowingSummarySidebar,
|
||||
onEditSummary,
|
||||
renderEmptyMessage,
|
||||
} = this.props;
|
||||
const { showCreateAlertModal } = this.state;
|
||||
|
||||
const noResults = datasetContainsNoResults(result.data);
|
||||
if (noResults && !isRunning && !renderEmptyMessage) {
|
||||
const supportsRowsPresentAlert = question.alertType() === ALERT_TYPE_ROWS;
|
||||
|
||||
const supportsBackToPreviousResult =
|
||||
!isEmbeddingSdk() || !!onNavigateBack;
|
||||
|
||||
// successful query but there were 0 rows returned with the result
|
||||
return (
|
||||
<div className={cx(className, CS.flex)}>
|
||||
<ErrorMessage
|
||||
type="noRows"
|
||||
title={t`No results!`}
|
||||
message={t`This may be the answer you’re looking for. If not, try removing or changing your filters to make them less specific.`}
|
||||
action={
|
||||
<div>
|
||||
{supportsRowsPresentAlert && !isDirty && (
|
||||
<p>
|
||||
{jt`You can also ${(
|
||||
<a
|
||||
className={CS.link}
|
||||
key="link"
|
||||
onClick={this.showCreateAlertModal}
|
||||
>
|
||||
{t`get an alert`}
|
||||
</a>
|
||||
)} when there are some results.`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{supportsBackToPreviousResult && (
|
||||
<button
|
||||
className={ButtonsS.Button}
|
||||
onClick={() =>
|
||||
onNavigateBack ? onNavigateBack() : window.history.back()
|
||||
}
|
||||
>
|
||||
{t`Back to previous results`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{showCreateAlertModal && (
|
||||
<CreateOrEditQuestionAlertModalWithQuestion
|
||||
onClose={this.onCloseCreateAlertModal}
|
||||
onAlertCreated={this.onCloseCreateAlertModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const vizSpecificProps = _.pick(
|
||||
this.props,
|
||||
...ALLOWED_VISUALIZATION_PROPS,
|
||||
);
|
||||
const { isEditable } = Lib.queryDisplayInfo(question.query());
|
||||
const hasDrills = isEditable;
|
||||
return (
|
||||
<>
|
||||
<Visualization
|
||||
className={className}
|
||||
rawSeries={rawSeries}
|
||||
onChangeCardAndRun={
|
||||
hasDrills ? navigateToNewCardInsideQB : undefined
|
||||
}
|
||||
isEditing={true}
|
||||
isObjectDetail={false}
|
||||
isQueryBuilder={true}
|
||||
isShowingSummarySidebar={isShowingSummarySidebar}
|
||||
isRunning={isRunning}
|
||||
onEditSummary={onEditSummary}
|
||||
queryBuilderMode={queryBuilderMode}
|
||||
showTitle={false}
|
||||
canToggleSeriesVisibility
|
||||
metadata={question.metadata()}
|
||||
timelineEvents={timelineEvents}
|
||||
token={token}
|
||||
selectedTimelineEventIds={selectedTimelineEventIds}
|
||||
handleVisualizationClick={this.props.handleVisualizationClick}
|
||||
onOpenTimelines={this.props.onOpenTimelines}
|
||||
onSelectTimelineEvents={this.props.selectTimelineEvents}
|
||||
onDeselectTimelineEvents={this.props.deselectTimelineEvents}
|
||||
onOpenChartSettings={this.props.onOpenChartSettings}
|
||||
onUpdateQuestion={this.props.onUpdateQuestion}
|
||||
onUpdateWarnings={this.props.onUpdateWarnings}
|
||||
onHeaderColumnReorder={this.props.onHeaderColumnReorder}
|
||||
onUpdateVisualizationSettings={
|
||||
this.props.onUpdateVisualizationSettings
|
||||
}
|
||||
onVisualizationRendered={this.props.onVisualizationRendered}
|
||||
{...vizSpecificProps}
|
||||
/>
|
||||
{this.props.isObjectDetail && (
|
||||
<Visualization
|
||||
isObjectDetail={true}
|
||||
rawSeries={this.getObjectDetailData(rawSeries)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import cx from "classnames";
|
||||
import { useState } from "react";
|
||||
import { jt, t } from "ttag";
|
||||
import _ from "underscore";
|
||||
|
||||
import type { NavigateToNewCardParams } from "embedding-sdk-bundle/types";
|
||||
import { ErrorMessage } from "metabase/common/components/ErrorMessage";
|
||||
import ButtonsS from "metabase/css/components/buttons.module.css";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { isEmbeddingSdk } from "metabase/embedding-sdk/config";
|
||||
import { CreateOrEditQuestionAlertModalWithQuestion } from "metabase/notifications/modals/CreateOrEditQuestionAlertModal/CreateOrEditQuestionAlertModal";
|
||||
import Visualization from "metabase/visualizations/components/Visualization";
|
||||
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 {
|
||||
Dataset,
|
||||
DatasetColumn,
|
||||
RawSeries,
|
||||
TimelineEvent,
|
||||
TimelineEventId,
|
||||
} from "metabase-types/api";
|
||||
import type { QueryBuilderMode } from "metabase-types/store";
|
||||
|
||||
const ALLOWED_VISUALIZATION_PROPS = [
|
||||
// Table
|
||||
"isShowingDetailsOnlyColumns",
|
||||
// Table Interactive
|
||||
"hasMetadataPopovers",
|
||||
"tableHeaderHeight",
|
||||
"scrollToColumn",
|
||||
"renderTableHeader",
|
||||
"mode",
|
||||
"renderEmptyMessage",
|
||||
"zoomedRowIndex",
|
||||
// Legend
|
||||
"hideLegend",
|
||||
];
|
||||
|
||||
type VisualizationResultProps = {
|
||||
className?: string;
|
||||
isDirty?: boolean;
|
||||
isObjectDetail?: boolean;
|
||||
isRunning?: boolean;
|
||||
navigateToNewCardInsideQB?:
|
||||
| ((opts: NavigateToNewCardParams) => Promise<void>)
|
||||
| null;
|
||||
queryBuilderMode?: QueryBuilderMode;
|
||||
question: Question;
|
||||
rawSeries?: RawSeries | null;
|
||||
renderEmptyMessage?: boolean;
|
||||
result: Dataset;
|
||||
maxTableRows?: number;
|
||||
|
||||
deselectTimelineEvents?: () => void;
|
||||
onOpenTimelines?: () => void;
|
||||
selectTimelineEvents?: (events: TimelineEvent[]) => void;
|
||||
selectedTimelineEventIds?: TimelineEventId[];
|
||||
timelineEvents?: TimelineEvent[];
|
||||
|
||||
isShowingSummarySidebar?: boolean;
|
||||
onEditSummary?: () => void;
|
||||
|
||||
handleVisualizationClick?: (
|
||||
clicked: {
|
||||
element: unknown;
|
||||
column: DatasetColumn;
|
||||
} | null,
|
||||
) => void;
|
||||
onHeaderColumnReorder?: (columnName: string) => void;
|
||||
onNavigateBack?: () => void;
|
||||
onOpenChartSettings?: () => void;
|
||||
onUpdateQuestion?: () => void;
|
||||
onUpdateVisualizationSettings?: () => void;
|
||||
onUpdateWarnings?: (warnings: string[]) => void;
|
||||
onVisualizationRendered?: () => void;
|
||||
};
|
||||
|
||||
const getObjectDetailData = (series: RawSeries): RawSeries => [
|
||||
{
|
||||
...series[0],
|
||||
card: { ...series[0].card, display: "object" },
|
||||
},
|
||||
];
|
||||
|
||||
export function VisualizationResult(props: VisualizationResultProps) {
|
||||
const {
|
||||
question,
|
||||
isDirty,
|
||||
queryBuilderMode,
|
||||
navigateToNewCardInsideQB,
|
||||
result,
|
||||
rawSeries,
|
||||
timelineEvents,
|
||||
selectedTimelineEventIds,
|
||||
onNavigateBack,
|
||||
className,
|
||||
isRunning,
|
||||
isShowingSummarySidebar,
|
||||
onEditSummary,
|
||||
renderEmptyMessage,
|
||||
handleVisualizationClick,
|
||||
onOpenTimelines,
|
||||
selectTimelineEvents,
|
||||
deselectTimelineEvents,
|
||||
onOpenChartSettings,
|
||||
onUpdateQuestion,
|
||||
onUpdateWarnings,
|
||||
onHeaderColumnReorder,
|
||||
onUpdateVisualizationSettings,
|
||||
onVisualizationRendered,
|
||||
isObjectDetail,
|
||||
} = props;
|
||||
|
||||
const [showCreateAlertModal, setShowCreateAlertModal] = useState(false);
|
||||
|
||||
const noResults = datasetContainsNoResults(result.data);
|
||||
if (noResults && !isRunning && !renderEmptyMessage) {
|
||||
const supportsRowsPresentAlert = question.alertType() === ALERT_TYPE_ROWS;
|
||||
|
||||
const supportsBackToPreviousResult = !isEmbeddingSdk() || !!onNavigateBack;
|
||||
|
||||
// successful query but there were 0 rows returned with the result
|
||||
return (
|
||||
<div className={cx(className, CS.flex)}>
|
||||
<ErrorMessage
|
||||
type="noRows"
|
||||
title={t`No results!`}
|
||||
message={t`This may be the answer you’re looking for. If not, try removing or changing your filters to make them less specific.`}
|
||||
action={
|
||||
<div>
|
||||
{supportsRowsPresentAlert && !isDirty && (
|
||||
<p>
|
||||
{jt`You can also ${(
|
||||
<a
|
||||
className={CS.link}
|
||||
key="link"
|
||||
onClick={() => setShowCreateAlertModal(true)}
|
||||
>
|
||||
{t`get an alert`}
|
||||
</a>
|
||||
)} when there are some results.`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{supportsBackToPreviousResult && (
|
||||
<button
|
||||
className={ButtonsS.Button}
|
||||
onClick={() =>
|
||||
onNavigateBack ? onNavigateBack() : window.history.back()
|
||||
}
|
||||
>
|
||||
{t`Back to previous results`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{showCreateAlertModal && (
|
||||
<CreateOrEditQuestionAlertModalWithQuestion
|
||||
onClose={() => setShowCreateAlertModal(false)}
|
||||
onAlertCreated={() => setShowCreateAlertModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const vizSpecificProps = _.pick(props, ...ALLOWED_VISUALIZATION_PROPS);
|
||||
const { isEditable } = Lib.queryDisplayInfo(question.query());
|
||||
const hasDrills = isEditable;
|
||||
return (
|
||||
<>
|
||||
<Visualization
|
||||
className={className}
|
||||
rawSeries={rawSeries ?? undefined}
|
||||
onChangeCardAndRun={hasDrills ? navigateToNewCardInsideQB : undefined}
|
||||
isEditing={true}
|
||||
isObjectDetail={false}
|
||||
isQueryBuilder={true}
|
||||
isShowingSummarySidebar={isShowingSummarySidebar}
|
||||
isRunning={isRunning}
|
||||
onEditSummary={onEditSummary}
|
||||
queryBuilderMode={queryBuilderMode}
|
||||
showTitle={false}
|
||||
canToggleSeriesVisibility
|
||||
metadata={question.metadata()}
|
||||
timelineEvents={timelineEvents}
|
||||
selectedTimelineEventIds={selectedTimelineEventIds}
|
||||
onDeselectTimelineEvents={deselectTimelineEvents}
|
||||
handleVisualizationClick={handleVisualizationClick}
|
||||
onOpenTimelines={onOpenTimelines}
|
||||
onSelectTimelineEvents={selectTimelineEvents}
|
||||
onOpenChartSettings={onOpenChartSettings}
|
||||
onUpdateQuestion={onUpdateQuestion}
|
||||
onUpdateWarnings={onUpdateWarnings}
|
||||
onHeaderColumnReorder={onHeaderColumnReorder}
|
||||
onUpdateVisualizationSettings={onUpdateVisualizationSettings}
|
||||
onVisualizationRendered={onVisualizationRendered}
|
||||
{...vizSpecificProps}
|
||||
/>
|
||||
{isObjectDetail && (
|
||||
<Visualization
|
||||
isObjectDetail={true}
|
||||
rawSeries={rawSeries ? getObjectDetailData(rawSeries) : undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/* eslint "react/prop-types": "warn" */
|
||||
import PropTypes from "prop-types";
|
||||
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";
|
||||
|
||||
const PANES = {
|
||||
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 DataReferencePropTypes = {
|
||||
dataReferenceStack: PropTypes.array.isRequired,
|
||||
popDataReferenceStack: PropTypes.func.isRequired,
|
||||
pushDataReferenceStack: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
export const DataReference = ({
|
||||
dataReferenceStack,
|
||||
popDataReferenceStack,
|
||||
pushDataReferenceStack,
|
||||
onClose,
|
||||
onBack,
|
||||
}) => {
|
||||
const onItemClick = useCallback(
|
||||
(type, item) => 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} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DataReference.propTypes = DataReferencePropTypes;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { MainPane } from "./MainPane";
|
||||
import {
|
||||
type DataReferenceItem,
|
||||
type DataReferencePaneProps,
|
||||
PANES,
|
||||
} from "./types";
|
||||
|
||||
export const DataReference = ({
|
||||
dataReferenceStack,
|
||||
popDataReferenceStack,
|
||||
pushDataReferenceStack,
|
||||
onClose,
|
||||
onBack,
|
||||
}: {
|
||||
dataReferenceStack: DataReferenceItem[];
|
||||
popDataReferenceStack: () => void;
|
||||
pushDataReferenceStack: (item: DataReferenceItem) => void;
|
||||
onClose?: () => void;
|
||||
onBack?: () => void;
|
||||
}) => {
|
||||
if (dataReferenceStack.length) {
|
||||
const page = dataReferenceStack[dataReferenceStack.length - 1];
|
||||
const Pane = PANES[page.type] as ComponentType<
|
||||
DataReferencePaneProps<typeof page>
|
||||
>;
|
||||
return (
|
||||
<Pane
|
||||
{...page}
|
||||
onItemClick={pushDataReferenceStack}
|
||||
onClose={onClose}
|
||||
onBack={popDataReferenceStack}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MainPane
|
||||
onItemClick={pushDataReferenceStack}
|
||||
onClose={onClose}
|
||||
onBack={onBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -17,11 +17,13 @@ import type {
|
||||
} from "metabase/common/components/tree/types";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { SidebarContent } from "metabase/query_builder/components/SidebarContent";
|
||||
import type Database from "metabase-lib/v1/metadata/Database";
|
||||
import type {
|
||||
CollectionId,
|
||||
SchemaName,
|
||||
SearchResult,
|
||||
import {
|
||||
type Card,
|
||||
type CollectionId,
|
||||
type SchemaName,
|
||||
type SearchResult,
|
||||
type Table,
|
||||
isConcreteTableId,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import {
|
||||
@@ -34,6 +36,11 @@ import {
|
||||
NodeListTitleText,
|
||||
} from "./NodeList";
|
||||
import { ResourceTreeNode } from "./ResourceTreeNode";
|
||||
import type {
|
||||
DataReferenceDatabaseItem,
|
||||
DataReferencePaneProps,
|
||||
OnItemClick,
|
||||
} from "./types";
|
||||
|
||||
const groupModelsByCollection = (models: SearchResult[]) => {
|
||||
const grouped = _.groupBy(
|
||||
@@ -77,21 +84,13 @@ const groupTablesBySchema = (tables: SearchResult[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export interface TablesListProps {
|
||||
export type TablesListProps = {
|
||||
schemas: string[];
|
||||
tables: SearchResult[];
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
}
|
||||
onItemClick: OnItemClick;
|
||||
};
|
||||
|
||||
const TablesList = ({
|
||||
schemas,
|
||||
tables,
|
||||
onItemClick,
|
||||
}: {
|
||||
schemas: string[];
|
||||
tables: SearchResult[];
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
}) => {
|
||||
const TablesList = ({ schemas, tables, onItemClick }: TablesListProps) => {
|
||||
const hasMultipleSchemas = schemas.length > 1;
|
||||
|
||||
if (hasMultipleSchemas) {
|
||||
@@ -117,10 +116,15 @@ const TablesList = ({
|
||||
</NodeListTitle>
|
||||
<Tree
|
||||
data={tablesBySchema}
|
||||
TreeNode={(props: TreeNodeProps<ITreeNodeItem>) => (
|
||||
<ResourceTreeNode
|
||||
TreeNode={(props: TreeNodeProps<Table>) => (
|
||||
<ResourceTreeNode<Table>
|
||||
{...props}
|
||||
onItemClick={() => onItemClick("table", props.item.data)}
|
||||
onItemClick={() => {
|
||||
const { id } = props.item.data ?? {};
|
||||
if (isConcreteTableId(id)) {
|
||||
onItemClick({ type: "table", id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -145,7 +149,11 @@ const TablesList = ({
|
||||
<li key={table.id}>
|
||||
<NodeListItemLink
|
||||
disabled={table.initial_sync_status !== "complete"}
|
||||
onClick={() => onItemClick("table", table)}
|
||||
onClick={() => {
|
||||
if (isConcreteTableId(table.id)) {
|
||||
onItemClick({ type: "table", id: table.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<NodeListItemIcon
|
||||
disabled={table.initial_sync_status !== "complete"}
|
||||
@@ -164,18 +172,12 @@ const TablesList = ({
|
||||
);
|
||||
};
|
||||
|
||||
export interface CollectionsListProps {
|
||||
export type CollectionsListProps = {
|
||||
models: SearchResult[];
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
}
|
||||
onItemClick: OnItemClick;
|
||||
};
|
||||
|
||||
const CollectionsList = ({
|
||||
models,
|
||||
onItemClick,
|
||||
}: {
|
||||
models: SearchResult[];
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
}) => {
|
||||
const CollectionsList = ({ models, onItemClick }: CollectionsListProps) => {
|
||||
if (models.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -202,10 +204,14 @@ const CollectionsList = ({
|
||||
</NodeListTitle>
|
||||
<Tree
|
||||
data={modelsByCollection}
|
||||
TreeNode={(props: TreeNodeProps<ITreeNodeItem>) => (
|
||||
<ResourceTreeNode
|
||||
TreeNode={(props: TreeNodeProps<Card>) => (
|
||||
<ResourceTreeNode<Card>
|
||||
{...props}
|
||||
onItemClick={() => onItemClick("question", props.item.data)}
|
||||
onItemClick={() => {
|
||||
if (props.item.data) {
|
||||
onItemClick({ type: "question", id: props.item.data.id });
|
||||
}
|
||||
}}
|
||||
displayId
|
||||
/>
|
||||
)}
|
||||
@@ -214,21 +220,15 @@ const CollectionsList = ({
|
||||
);
|
||||
};
|
||||
|
||||
export interface DatabasePaneProps {
|
||||
database: Database;
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
}
|
||||
export type DatabasePaneProps =
|
||||
DataReferencePaneProps<DataReferenceDatabaseItem>;
|
||||
|
||||
export const DatabasePane = ({
|
||||
database,
|
||||
id: databaseId,
|
||||
onBack,
|
||||
onClose,
|
||||
onItemClick,
|
||||
}: DatabasePaneProps) => {
|
||||
const databaseId = database.id;
|
||||
|
||||
const {
|
||||
data: databaseData,
|
||||
isLoading: isLoadingDatabase,
|
||||
|
||||
@@ -50,7 +50,8 @@ const completeTableSearchResult = createMockSearchResult({
|
||||
});
|
||||
|
||||
const defaultProps: DatabasePaneProps = {
|
||||
database,
|
||||
type: "database",
|
||||
id: database.id,
|
||||
onBack: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
onItemClick: jest.fn(),
|
||||
@@ -161,7 +162,7 @@ describe("DatabasePane", () => {
|
||||
initial_sync_status: "complete",
|
||||
});
|
||||
|
||||
setup({ database: singleSchemaDatabase });
|
||||
setup({ id: singleSchemaDatabase.id });
|
||||
setupDatabaseEndpoints(singleSchemaDb);
|
||||
setupSearchEndpoints([table1]);
|
||||
|
||||
@@ -211,7 +212,7 @@ describe("DatabasePane", () => {
|
||||
initial_sync_status: "complete",
|
||||
});
|
||||
|
||||
setup({ database: multiSchemaDatabase });
|
||||
setup({ id: multiSchemaDatabase.id });
|
||||
setupDatabaseEndpoints(multiSchemaDb);
|
||||
setupSearchEndpoints([table1, table2]);
|
||||
|
||||
@@ -232,7 +233,7 @@ describe("DatabasePane", () => {
|
||||
emptySchemasMetadata.database(emptySchemasDb.id),
|
||||
);
|
||||
|
||||
setup({ database: emptySchemasDatabase });
|
||||
setup({ id: emptySchemasDatabase.id });
|
||||
setupDatabaseEndpoints(emptySchemasDb);
|
||||
setupSearchEndpoints([]);
|
||||
|
||||
@@ -314,7 +315,7 @@ describe("DatabasePane", () => {
|
||||
initial_sync_status: "complete",
|
||||
});
|
||||
|
||||
setup({ database: multiSchemaDatabase });
|
||||
setup({ id: multiSchemaDatabase.id });
|
||||
setupDatabaseEndpoints(multiSchemaDb);
|
||||
setupSearchEndpoints([table1, table2]);
|
||||
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
import { useGetFieldQuery } from "metabase/api";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import { TableColumnInfo } from "metabase/common/components/MetadataInfo/ColumnInfo";
|
||||
import { SidebarContent } from "metabase/query_builder/components/SidebarContent";
|
||||
import type Field from "metabase-lib/v1/metadata/Field";
|
||||
|
||||
interface FieldPaneProps {
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
field: Field;
|
||||
}
|
||||
import type { DataReferenceFieldItem, DataReferencePaneProps } from "./types";
|
||||
|
||||
export const FieldPane = ({
|
||||
onBack,
|
||||
onClose,
|
||||
id,
|
||||
}: DataReferencePaneProps<DataReferenceFieldItem>) => {
|
||||
const { data: field, isLoading, error } = useGetFieldQuery({ id });
|
||||
|
||||
if (!field || isLoading || error) {
|
||||
return (
|
||||
<LoadingAndErrorWrapper loading={isLoading || !field} error={error} />
|
||||
);
|
||||
}
|
||||
|
||||
export const FieldPane = ({ onBack, onClose, field }: FieldPaneProps) => {
|
||||
return (
|
||||
<SidebarContent
|
||||
title={field.name}
|
||||
icon={"field"}
|
||||
icon="field"
|
||||
onBack={onBack}
|
||||
onClose={onClose}
|
||||
>
|
||||
<SidebarContent.Pane>
|
||||
<TableColumnInfo
|
||||
field={field}
|
||||
timezone={field.table?.database?.timezone}
|
||||
// TODO
|
||||
// timezone={field.table?.database?.timezone}
|
||||
showAllFieldValues
|
||||
showFingerprintInfo
|
||||
/>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/* eslint "react/prop-types": "warn" */
|
||||
import cx from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
import { t } from "ttag";
|
||||
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { Databases } from "metabase/entities/databases";
|
||||
import { SidebarContent } from "metabase/query_builder/components/SidebarContent";
|
||||
|
||||
import {
|
||||
NodeListItemIcon,
|
||||
NodeListItemLink,
|
||||
NodeListItemName,
|
||||
} from "./NodeList";
|
||||
|
||||
const MainPaneInner = ({ databases, onClose, onItemClick, onBack }) => (
|
||||
<SidebarContent title={t`Data Reference`} onClose={onClose} onBack={onBack}>
|
||||
<SidebarContent.Pane>
|
||||
<p className={cx(CS.mt2, CS.mb3, CS.textSpaced)}>
|
||||
{t`Browse the contents of your databases, tables, and columns. Pick a database to get started.`}
|
||||
</p>
|
||||
<ul>
|
||||
{databases &&
|
||||
databases.map((database) => (
|
||||
<li key={database.id}>
|
||||
<NodeListItemLink
|
||||
onClick={() => onItemClick("database", database)}
|
||||
>
|
||||
<NodeListItemIcon name="database" />
|
||||
<NodeListItemName>{database.name}</NodeListItemName>
|
||||
</NodeListItemLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</SidebarContent.Pane>
|
||||
</SidebarContent>
|
||||
);
|
||||
|
||||
MainPaneInner.propTypes = {
|
||||
databases: PropTypes.array,
|
||||
onClose: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const MainPane = Databases.loadList()(MainPaneInner);
|
||||
@@ -0,0 +1,50 @@
|
||||
import cx from "classnames";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useListDatabasesQuery } from "metabase/api";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import { SidebarContent } from "metabase/query_builder/components/SidebarContent";
|
||||
|
||||
import {
|
||||
NodeListItemIcon,
|
||||
NodeListItemLink,
|
||||
NodeListItemName,
|
||||
} from "./NodeList";
|
||||
import type { DataReferencePaneProps } from "./types";
|
||||
|
||||
export const MainPane = ({
|
||||
onClose,
|
||||
onItemClick,
|
||||
onBack,
|
||||
}: DataReferencePaneProps) => {
|
||||
const { data, isLoading, error } = useListDatabasesQuery();
|
||||
|
||||
if (isLoading || error) {
|
||||
return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarContent title={t`Data Reference`} onClose={onClose} onBack={onBack}>
|
||||
<SidebarContent.Pane>
|
||||
<p className={cx(CS.mt2, CS.mb3, CS.textSpaced)}>
|
||||
{t`Browse the contents of your databases, tables, and columns. Pick a database to get started.`}
|
||||
</p>
|
||||
<ul>
|
||||
{data?.data?.map((database) => (
|
||||
<li key={database.id}>
|
||||
<NodeListItemLink
|
||||
onClick={() =>
|
||||
onItemClick({ type: "database", id: database.id })
|
||||
}
|
||||
>
|
||||
<NodeListItemIcon name="database" />
|
||||
<NodeListItemName>{database.name}</NodeListItemName>
|
||||
</NodeListItemLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</SidebarContent.Pane>
|
||||
</SidebarContent>
|
||||
);
|
||||
};
|
||||
@@ -20,24 +20,16 @@ import { Box, Flex, Icon, type IconName } from "metabase/ui";
|
||||
import type Question from "metabase-lib/v1/Question";
|
||||
import { getQuestionVirtualTableId } from "metabase-lib/v1/metadata/utils/saved-questions";
|
||||
import * as ML_Urls from "metabase-lib/v1/urls";
|
||||
import type { CardId } from "metabase-types/api";
|
||||
|
||||
import { FieldList } from "../FieldList";
|
||||
import { NodeListTitleText } from "../NodeList";
|
||||
import type {
|
||||
DataReferencePaneProps,
|
||||
DataReferenceQuestionItem,
|
||||
} from "../types";
|
||||
|
||||
import S from "./QuestionPane.module.css";
|
||||
|
||||
type QuestionItem = {
|
||||
id: CardId;
|
||||
};
|
||||
|
||||
type QuestionPaneProps = {
|
||||
question: QuestionItem;
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const getIcon = (question: Question): IconName => {
|
||||
return match(question.type())
|
||||
.returnType<IconName>()
|
||||
@@ -48,11 +40,11 @@ const getIcon = (question: Question): IconName => {
|
||||
};
|
||||
|
||||
export const QuestionPane = ({
|
||||
question: { id },
|
||||
id,
|
||||
onBack,
|
||||
onItemClick,
|
||||
onClose,
|
||||
}: QuestionPaneProps) => {
|
||||
}: DataReferencePaneProps<DataReferenceQuestionItem>) => {
|
||||
const {
|
||||
data: card,
|
||||
isLoading: isLoadingCard,
|
||||
@@ -147,7 +139,11 @@ export const QuestionPane = ({
|
||||
{table.fields && (
|
||||
<FieldList
|
||||
fields={table.fields}
|
||||
onFieldClick={(f) => onItemClick("field", f)}
|
||||
onFieldClick={(field) => {
|
||||
if (typeof field.id === "number") {
|
||||
onItemClick({ type: "field", id: field.id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
ITreeNodeItem,
|
||||
TreeNodeProps,
|
||||
} from "metabase/common/components/tree/types";
|
||||
import type { TreeNodeProps } from "metabase/common/components/tree/types";
|
||||
import type { IconName } from "metabase/ui";
|
||||
|
||||
import {
|
||||
@@ -12,14 +9,14 @@ import {
|
||||
} from "./NodeList";
|
||||
import S from "./ResourceTreeNode.module.css";
|
||||
|
||||
export const ResourceTreeNode = ({
|
||||
export const ResourceTreeNode = <TData = unknown,>({
|
||||
item,
|
||||
depth,
|
||||
isExpanded,
|
||||
displayId = false,
|
||||
onToggleExpand,
|
||||
onItemClick,
|
||||
}: TreeNodeProps<ITreeNodeItem> & {
|
||||
}: TreeNodeProps<TData> & {
|
||||
displayId?: boolean;
|
||||
onItemClick: () => void;
|
||||
}) => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { msgid, ngettext } from "ttag";
|
||||
|
||||
import { Schemas } from "metabase/entities/schemas";
|
||||
import { useListDatabaseSchemaTablesQuery } from "metabase/api";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import { SidebarContent } from "metabase/query_builder/components/SidebarContent";
|
||||
import type Schema from "metabase-lib/v1/metadata/Schema";
|
||||
import type { State } from "metabase-types/store";
|
||||
import { isConcreteTableId } from "metabase-types/api";
|
||||
|
||||
import {
|
||||
NodeListContainer,
|
||||
@@ -15,27 +14,30 @@ import {
|
||||
NodeListTitle,
|
||||
NodeListTitleText,
|
||||
} from "./NodeList";
|
||||
import type { DataReferencePaneProps, DataReferenceSchemaItem } from "./types";
|
||||
|
||||
interface SchemaPaneProps {
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
schema: Schema;
|
||||
}
|
||||
type SchemaPaneProps = DataReferencePaneProps<DataReferenceSchemaItem>;
|
||||
|
||||
const SchemaPaneInner = ({
|
||||
export const SchemaPane = ({
|
||||
onBack,
|
||||
onClose,
|
||||
onItemClick,
|
||||
schema,
|
||||
schemaName,
|
||||
databaseId,
|
||||
}: SchemaPaneProps) => {
|
||||
const tables = useMemo(
|
||||
() => schema.getTables().sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[schema],
|
||||
);
|
||||
const {
|
||||
data: tables = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useListDatabaseSchemaTablesQuery({ id: databaseId, schema: schemaName });
|
||||
|
||||
if (isLoading || error) {
|
||||
return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarContent
|
||||
title={schema.name}
|
||||
title={schemaName}
|
||||
icon={"folder"}
|
||||
onBack={onBack}
|
||||
onClose={onClose}
|
||||
@@ -53,21 +55,22 @@ const SchemaPaneInner = ({
|
||||
</NodeListTitleText>
|
||||
</NodeListTitle>
|
||||
<ul>
|
||||
{tables.map((table) => (
|
||||
<li key={table.id}>
|
||||
<NodeListItemLink onClick={() => onItemClick("table", table)}>
|
||||
<NodeListItemIcon name="table" />
|
||||
<NodeListItemName>{table.name}</NodeListItemName>
|
||||
</NodeListItemLink>
|
||||
</li>
|
||||
))}
|
||||
{tables.map(
|
||||
({ id, name }) =>
|
||||
isConcreteTableId(id) && (
|
||||
<li key={id}>
|
||||
<NodeListItemLink
|
||||
onClick={() => onItemClick({ type: "table", id })}
|
||||
>
|
||||
<NodeListItemIcon name="table" />
|
||||
<NodeListItemName>{name}</NodeListItemName>
|
||||
</NodeListItemLink>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</NodeListContainer>
|
||||
</SidebarContent.Pane>
|
||||
</SidebarContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaPane = Schemas.load({
|
||||
id: (_state: State, props: SchemaPaneProps) => props.schema.id,
|
||||
})(SchemaPaneInner);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useSelector } from "metabase/lib/redux";
|
||||
import { SidebarContent } from "metabase/query_builder/components/SidebarContent";
|
||||
import { ConnectedTableList } from "metabase/query_builder/components/dataref/ConnectedTableList";
|
||||
import { getMetadata } from "metabase/selectors/metadata";
|
||||
import type { ConcreteTableId } from "metabase-types/api";
|
||||
import { isConcreteTableId } from "metabase-types/api";
|
||||
|
||||
import { FieldList } from "./FieldList";
|
||||
import {
|
||||
@@ -24,24 +24,14 @@ import {
|
||||
NodeListTitleText,
|
||||
} from "./NodeList";
|
||||
import { TableInfoLoader } from "./TableInfoLoader";
|
||||
|
||||
type TableItem = {
|
||||
id: ConcreteTableId;
|
||||
};
|
||||
|
||||
type TablePaneProps = {
|
||||
table: TableItem;
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
onItemClick: (type: string, item: unknown) => void;
|
||||
};
|
||||
import type { DataReferencePaneProps, DataReferenceTableItem } from "./types";
|
||||
|
||||
export function TablePane({
|
||||
table: { id },
|
||||
id,
|
||||
onItemClick,
|
||||
onBack,
|
||||
onClose,
|
||||
}: TablePaneProps) {
|
||||
}: DataReferencePaneProps<DataReferenceTableItem>) {
|
||||
const { isLoading, error } = useGetTableQuery({ id });
|
||||
const metadata = useSelector(getMetadata);
|
||||
|
||||
@@ -75,12 +65,20 @@ export function TablePane({
|
||||
<>
|
||||
<FieldList
|
||||
fields={table.fields}
|
||||
onFieldClick={(f) => onItemClick("field", f)}
|
||||
onFieldClick={(field) => {
|
||||
if (typeof field.id === "number") {
|
||||
onItemClick({ type: "field", id: field.id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{table.connectedTables() && (
|
||||
<ConnectedTableList
|
||||
tables={table.connectedTables()}
|
||||
onTableClick={(t) => onItemClick("table", t)}
|
||||
onTableClick={(table) => {
|
||||
if (isConcreteTableId(table.id)) {
|
||||
onItemClick({ type: "table", id: table.id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -101,7 +99,12 @@ export function TablePane({
|
||||
{table.metrics?.map((metric) => (
|
||||
<li key={metric.card().id}>
|
||||
<NodeListItemLink
|
||||
onClick={() => onItemClick("question", metric.card())}
|
||||
onClick={() =>
|
||||
onItemClick({
|
||||
type: "question",
|
||||
id: metric.card().id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<NodeListItemIcon name="metric" />
|
||||
<NodeListItemName>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type {
|
||||
CardId,
|
||||
ConcreteTableId,
|
||||
DatabaseId,
|
||||
FieldId,
|
||||
SchemaName,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import { DatabasePane } from "./DatabasePane";
|
||||
import { FieldPane } from "./FieldPane";
|
||||
import { QuestionPane } from "./QuestionPane";
|
||||
import { SchemaPane } from "./SchemaPane";
|
||||
import { TablePane } from "./TablePane";
|
||||
|
||||
export const PANES = {
|
||||
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
|
||||
};
|
||||
|
||||
export type OnItemClick = (item: DataReferenceItem) => void;
|
||||
export type DataReferencePaneProps<TItem = unknown> = {
|
||||
onClose?: () => void;
|
||||
onBack?: () => void;
|
||||
onItemClick: OnItemClick;
|
||||
} & TItem;
|
||||
|
||||
export type DataReferenceItem =
|
||||
| DataReferenceDatabaseItem
|
||||
| DataReferenceSchemaItem
|
||||
| DataReferenceTableItem
|
||||
| DataReferenceQuestionItem
|
||||
| DataReferenceFieldItem;
|
||||
|
||||
export type DataReferenceDatabaseItem = {
|
||||
type: "database";
|
||||
id: DatabaseId;
|
||||
};
|
||||
|
||||
export type DataReferenceSchemaItem = {
|
||||
type: "schema";
|
||||
schemaName: SchemaName;
|
||||
databaseId: DatabaseId;
|
||||
};
|
||||
|
||||
export type DataReferenceTableItem = {
|
||||
type: "table";
|
||||
id: ConcreteTableId;
|
||||
};
|
||||
|
||||
export type DataReferenceQuestionItem = {
|
||||
type: "question";
|
||||
id: CardId;
|
||||
};
|
||||
|
||||
export type DataReferenceFieldItem = {
|
||||
type: "field";
|
||||
id: FieldId;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { FixedSizeIcon, Flex, Tooltip } from "metabase/ui";
|
||||
import type { Dataset } from "metabase-types/api";
|
||||
|
||||
import { formatDuration } from "./utils";
|
||||
|
||||
@@ -34,5 +35,5 @@ export const ExecutionTime = ({ time }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
ExecutionTime.shouldRender = ({ result }: { result: any }) =>
|
||||
result && !result.cached && result.running_time !== undefined;
|
||||
ExecutionTime.shouldRender = ({ result }: { result: Dataset | null }) =>
|
||||
Boolean(result && !result.cached && result.running_time !== undefined);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { match } from "ts-pattern";
|
||||
import { PLUGIN_AI_ENTITY_ANALYSIS } from "metabase/plugins";
|
||||
import type { EmbeddingParameterVisibility } from "metabase/public/lib/types";
|
||||
import { DataReference } from "metabase/query_builder/components/dataref/DataReference";
|
||||
import type { DataReferenceItem } from "metabase/query_builder/components/dataref/types";
|
||||
import { SnippetSidebar } from "metabase/query_builder/components/template_tags/SnippetSidebar";
|
||||
import { TagEditorSidebar } from "metabase/query_builder/components/template_tags/TagEditorSidebar";
|
||||
import { QuestionInfoSidebar } from "metabase/query_builder/components/view/sidebars/QuestionInfoSidebar";
|
||||
@@ -51,6 +52,10 @@ interface NativeQueryRightSidebarProps {
|
||||
getEmbeddedParameterVisibility: (
|
||||
slug: string,
|
||||
) => EmbeddingParameterVisibility;
|
||||
|
||||
dataReferenceStack: DataReferenceItem[];
|
||||
pushDataReferenceStack: (item: DataReferenceItem) => void;
|
||||
popDataReferenceStack: () => void;
|
||||
}
|
||||
|
||||
export const NativeQueryRightSidebar = (
|
||||
|
||||
@@ -28,11 +28,11 @@ export const RightViewFooterButtonGroup = () => {
|
||||
result,
|
||||
isObjectDetail,
|
||||
}) && <QuestionRowCount />}
|
||||
{ExecutionTime.shouldRender({ result }) && (
|
||||
{result && ExecutionTime.shouldRender({ result }) && (
|
||||
<ExecutionTime time={result.running_time} />
|
||||
)}
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
{QuestionLastUpdated.shouldRender({ result }) && (
|
||||
{result && QuestionLastUpdated.shouldRender({ result }) && (
|
||||
<QuestionLastUpdated
|
||||
className={cx(CS.hide, CS.smShow)}
|
||||
result={result}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const ViewFooterDownloadWidget = () => {
|
||||
|
||||
return (
|
||||
question &&
|
||||
result &&
|
||||
QuestionDownloadPopover.shouldRender({ result }) && (
|
||||
<QuestionDownloadPopover
|
||||
className={cx(CS.hide, CS.smShow)}
|
||||
|
||||
@@ -41,9 +41,12 @@ export function QueryBuilderBackButton({
|
||||
onClick?.();
|
||||
};
|
||||
|
||||
const url = Urls.modelToUrl(parent);
|
||||
if (!parent.model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!parent.model || !url) {
|
||||
const url = Urls.modelToUrl(parent as Urls.UrlableModel);
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +269,9 @@ function QueryBuilderInner(props: QueryBuilderInnerProps) {
|
||||
!didTrackFirstNonTableChartGeneratedRef.current &&
|
||||
isNonTable
|
||||
) {
|
||||
setDidFirstNonTableChartRender(card);
|
||||
if (card) {
|
||||
setDidFirstNonTableChartRender(card);
|
||||
}
|
||||
didTrackFirstNonTableChartGeneratedRef.current = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DEFAULT_UI_CONTROLS: QueryBuilderUIControls = {
|
||||
initialChartSetting: {},
|
||||
isShowingRawTable: false, // table/viz toggle
|
||||
queryBuilderMode: "view", // "view" | "notebook" | "dataset"
|
||||
previousQueryBuilderMode: false,
|
||||
previousQueryBuilderMode: null,
|
||||
snippetCollectionId: null,
|
||||
datasetEditorTab: "query",
|
||||
isShowingQuestionDetailsSidebar: false,
|
||||
@@ -33,6 +33,8 @@ export const DEFAULT_UI_CONTROLS: QueryBuilderUIControls = {
|
||||
modalContext: null,
|
||||
highlightedNativeQueryLineNumbers: [],
|
||||
isShowingListViewConfiguration: false,
|
||||
nativeEditorSelectedRange: [],
|
||||
questionDetailsTimelineDrawerState: null,
|
||||
};
|
||||
|
||||
export const DEFAULT_LOADING_CONTROLS: QueryBuilderLoadingControls = {
|
||||
|
||||
@@ -1,533 +0,0 @@
|
||||
import { assoc, merge } from "icepick";
|
||||
import { handleActions } from "redux-actions";
|
||||
import _ from "underscore";
|
||||
|
||||
import {
|
||||
EDIT_QUESTION,
|
||||
NAVIGATE_TO_NEW_CARD,
|
||||
} from "metabase/dashboard/actions";
|
||||
import { TimelineEvents } from "metabase/entities/timeline-events";
|
||||
import { copy } from "metabase/lib/utils";
|
||||
|
||||
import {
|
||||
API_CREATE_QUESTION,
|
||||
API_UPDATE_QUESTION,
|
||||
CANCEL_QUERY,
|
||||
CANCEL_QUESTION_CHANGES,
|
||||
CLEAR_OBJECT_DETAIL_FK_REFERENCES,
|
||||
CLEAR_QUERY_RESULT,
|
||||
CLOSE_QB,
|
||||
CLOSE_QB_NEWB_MODAL,
|
||||
DESELECT_TIMELINE_EVENTS,
|
||||
HIDE_TIMELINE_EVENTS,
|
||||
INITIALIZE_QB,
|
||||
LOAD_OBJECT_DETAIL_FK_REFERENCES,
|
||||
OPEN_DATA_REFERENCE_AT_QUESTION,
|
||||
QUERY_COMPLETED,
|
||||
QUERY_ERRORED,
|
||||
RELOAD_CARD,
|
||||
RESET_QB,
|
||||
RESET_ROW_ZOOM,
|
||||
RESET_UI_CONTROLS,
|
||||
RUN_QUERY,
|
||||
SELECT_TIMELINE_EVENTS,
|
||||
SET_CARD_AND_RUN,
|
||||
SET_CURRENT_STATE,
|
||||
SET_DATA_REFERENCE_STACK,
|
||||
SET_DOCUMENT_TITLE,
|
||||
SET_DOCUMENT_TITLE_TIMEOUT_ID,
|
||||
SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
|
||||
SET_METADATA_DIFF,
|
||||
SET_MODAL_SNIPPET,
|
||||
SET_NATIVE_EDITOR_SELECTED_RANGE,
|
||||
SET_PARAMETER_VALUE,
|
||||
SET_SHOW_LOADING_COMPLETE_FAVICON,
|
||||
SET_SNIPPET_COLLECTION_ID,
|
||||
SET_UI_CONTROLS,
|
||||
SHOW_CHART_SETTINGS,
|
||||
SHOW_TIMELINE_EVENTS,
|
||||
TOGGLE_DATA_REFERENCE,
|
||||
TOGGLE_SNIPPET_SIDEBAR,
|
||||
TOGGLE_TEMPLATE_TAGS_EDITOR,
|
||||
UPDATE_QUESTION,
|
||||
ZOOM_IN_ROW,
|
||||
onCloseAIQuestionAnalysisSidebar,
|
||||
onCloseChartSettings,
|
||||
onCloseChartType,
|
||||
onCloseQuestionInfo,
|
||||
onCloseQuestionSettings,
|
||||
onCloseSidebars,
|
||||
onCloseSummary,
|
||||
onCloseTimelines,
|
||||
onEditSummary,
|
||||
onOpenAIQuestionAnalysisSidebar,
|
||||
onOpenChartSettings,
|
||||
onOpenChartType,
|
||||
onOpenQuestionInfo,
|
||||
onOpenQuestionSettings,
|
||||
onOpenTimelines,
|
||||
} from "./actions";
|
||||
import {
|
||||
CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
DEFAULT_LOADING_CONTROLS,
|
||||
DEFAULT_PARENT_ENTITY_STATE,
|
||||
DEFAULT_QUERY_STATUS,
|
||||
DEFAULT_UI_CONTROLS,
|
||||
UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
} from "./defaults";
|
||||
|
||||
function setUIControls(state, changes) {
|
||||
const { queryBuilderMode: currentQBMode, ...currentState } = state;
|
||||
const { queryBuilderMode: nextQBMode, ...nextStateChanges } = changes;
|
||||
|
||||
const isChangingQBMode = nextQBMode && currentQBMode !== nextQBMode;
|
||||
const isOpeningEditingQBMode = isChangingQBMode && nextQBMode !== "view";
|
||||
|
||||
const queryBuilderMode = nextQBMode || currentQBMode;
|
||||
const previousQueryBuilderMode = isChangingQBMode
|
||||
? currentQBMode
|
||||
: state.previousQueryBuilderMode;
|
||||
|
||||
// Close all the sidebars when entering notebook/dataset QB modes
|
||||
const extraState = isOpeningEditingQBMode ? UI_CONTROLS_SIDEBAR_DEFAULTS : {};
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
...extraState,
|
||||
...nextStateChanges,
|
||||
queryBuilderMode,
|
||||
previousQueryBuilderMode,
|
||||
};
|
||||
}
|
||||
|
||||
export const uiControls = handleActions(
|
||||
{
|
||||
[SET_UI_CONTROLS]: {
|
||||
next: (state, { payload }) => setUIControls(state, payload),
|
||||
},
|
||||
|
||||
[RESET_UI_CONTROLS]: {
|
||||
next: (state) => ({
|
||||
...DEFAULT_UI_CONTROLS,
|
||||
isRunning: state.isRunning,
|
||||
}),
|
||||
},
|
||||
|
||||
[INITIALIZE_QB]: {
|
||||
next: (state, { payload }) => {
|
||||
return {
|
||||
...state,
|
||||
...DEFAULT_UI_CONTROLS,
|
||||
...payload.uiControls,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
[UPDATE_QUESTION]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
highlightedNativeQueryLineNumbers:
|
||||
DEFAULT_UI_CONTROLS.highlightedNativeQueryLineNumbers,
|
||||
}),
|
||||
},
|
||||
[TOGGLE_DATA_REFERENCE]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingDataReference: !state.isShowingDataReference,
|
||||
}),
|
||||
},
|
||||
[SET_DATA_REFERENCE_STACK]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
dataReferenceStack: payload,
|
||||
}),
|
||||
},
|
||||
[OPEN_DATA_REFERENCE_AT_QUESTION]: {
|
||||
next: (state, { payload }) => {
|
||||
return payload
|
||||
? {
|
||||
...state,
|
||||
dataReferenceStack: payload,
|
||||
isShowingDataReference: true,
|
||||
}
|
||||
: state;
|
||||
},
|
||||
},
|
||||
[TOGGLE_TEMPLATE_TAGS_EDITOR]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingTemplateTagsEditor: !state.isShowingTemplateTagsEditor,
|
||||
}),
|
||||
},
|
||||
[TOGGLE_SNIPPET_SIDEBAR]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingSnippetSidebar: !state.isShowingSnippetSidebar,
|
||||
snippetCollectionId: null,
|
||||
}),
|
||||
},
|
||||
[SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR]: {
|
||||
next: (state, { isShowingTemplateTagsEditor }) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingTemplateTagsEditor,
|
||||
}),
|
||||
},
|
||||
[SET_NATIVE_EDITOR_SELECTED_RANGE]: (state, { payload }) => ({
|
||||
...state,
|
||||
nativeEditorSelectedRange: payload,
|
||||
}),
|
||||
[SET_MODAL_SNIPPET]: (state, { payload }) => ({
|
||||
...state,
|
||||
modalSnippet: payload,
|
||||
}),
|
||||
[SET_SNIPPET_COLLECTION_ID]: (state, { payload }) => ({
|
||||
...state,
|
||||
snippetCollectionId: payload,
|
||||
}),
|
||||
[CLOSE_QB_NEWB_MODAL]: {
|
||||
next: (state, { payload }) => ({ ...state, isShowingNewbModal: false }),
|
||||
},
|
||||
|
||||
[RUN_QUERY]: (state) => ({
|
||||
...state,
|
||||
isRunning: true,
|
||||
highlightedNativeQueryLineNumbers:
|
||||
DEFAULT_UI_CONTROLS.highlightedNativeQueryLineNumbers,
|
||||
}),
|
||||
[CANCEL_QUERY]: {
|
||||
next: (state, { payload }) => ({ ...state, isRunning: false }),
|
||||
},
|
||||
[QUERY_COMPLETED]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
isRunning: false,
|
||||
}),
|
||||
},
|
||||
[QUERY_ERRORED]: {
|
||||
next: (state, { payload }) => ({ ...state, isRunning: false }),
|
||||
},
|
||||
|
||||
[SHOW_CHART_SETTINGS]: {
|
||||
next: (state, { payload }) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingChartSettingsSidebar: true,
|
||||
initialChartSetting: payload,
|
||||
}),
|
||||
},
|
||||
[CANCEL_QUESTION_CHANGES]: {
|
||||
next: (state) => ({
|
||||
...state,
|
||||
isModifiedFromNotebook: false,
|
||||
}),
|
||||
},
|
||||
// AGGREGATION
|
||||
[onEditSummary]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingSummarySidebar: true,
|
||||
}),
|
||||
[onCloseSummary]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}),
|
||||
[onOpenAIQuestionAnalysisSidebar]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingAIQuestionAnalysisSidebar: true,
|
||||
}),
|
||||
[onCloseAIQuestionAnalysisSidebar]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}),
|
||||
[onOpenChartSettings]: (
|
||||
state,
|
||||
{ payload: { initialChartSettings, showSidebarTitle = false } = {} },
|
||||
) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingChartSettingsSidebar: true,
|
||||
initialChartSetting: initialChartSettings,
|
||||
showSidebarTitle: showSidebarTitle,
|
||||
}),
|
||||
[onCloseChartSettings]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}),
|
||||
[onOpenChartType]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingChartTypeSidebar: true,
|
||||
}),
|
||||
[onCloseChartType]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}),
|
||||
[onOpenQuestionInfo]: (state) =>
|
||||
setUIControls(state, {
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingQuestionInfoSidebar: true,
|
||||
queryBuilderMode: "view",
|
||||
}),
|
||||
[onCloseQuestionInfo]: (state) => ({
|
||||
...state,
|
||||
isShowingQuestionInfoSidebar: false,
|
||||
}),
|
||||
[onOpenQuestionSettings]: (state) =>
|
||||
setUIControls(state, {
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingQuestionSettingsSidebar: true,
|
||||
queryBuilderMode: "view",
|
||||
}),
|
||||
[onCloseQuestionSettings]: (state) => ({
|
||||
...state,
|
||||
isShowingQuestionSettingsSidebar: false,
|
||||
}),
|
||||
[onOpenTimelines]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingTimelineSidebar: true,
|
||||
}),
|
||||
[onCloseTimelines]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}),
|
||||
[onCloseSidebars]: (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}),
|
||||
},
|
||||
DEFAULT_UI_CONTROLS,
|
||||
);
|
||||
|
||||
export const loadingControls = handleActions(
|
||||
{
|
||||
[SET_DOCUMENT_TITLE]: (state, { payload }) => ({
|
||||
...state,
|
||||
documentTitle: payload,
|
||||
}),
|
||||
[SET_SHOW_LOADING_COMPLETE_FAVICON]: (state, { payload }) => ({
|
||||
...state,
|
||||
showLoadCompleteFavicon: payload,
|
||||
}),
|
||||
[SET_DOCUMENT_TITLE_TIMEOUT_ID]: (state, { payload }) => ({
|
||||
...state,
|
||||
timeoutId: payload,
|
||||
}),
|
||||
},
|
||||
DEFAULT_LOADING_CONTROLS,
|
||||
);
|
||||
|
||||
export const queryStatus = handleActions(
|
||||
{
|
||||
[RUN_QUERY]: (state) => "running",
|
||||
[QUERY_COMPLETED]: (state) => "complete",
|
||||
[CANCEL_QUERY]: (state) => "idle",
|
||||
},
|
||||
DEFAULT_QUERY_STATUS,
|
||||
);
|
||||
|
||||
export const zoomedRowObjectId = handleActions(
|
||||
{
|
||||
[INITIALIZE_QB]: {
|
||||
next: (state, { payload }) => payload?.objectId ?? null,
|
||||
},
|
||||
[ZOOM_IN_ROW]: {
|
||||
next: (state, { payload }) => payload.objectId,
|
||||
},
|
||||
[RESET_ROW_ZOOM]: { next: () => null },
|
||||
[RESET_QB]: { next: () => null },
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// a copy of the card being worked on at it's last known saved state. if the card is NEW then this should be null.
|
||||
// NOTE: we use JSON serialization/deserialization to ensure a deep clone of the object which is required
|
||||
// because we can't have any links between the active card being modified and the "originalCard" for testing dirtiness
|
||||
// ALSO: we consistently check for payload.id because an unsaved card has no "originalCard"
|
||||
export const originalCard = handleActions(
|
||||
{
|
||||
[INITIALIZE_QB]: {
|
||||
next: (state, { payload }) =>
|
||||
payload.originalCard ? copy(payload.originalCard) : null,
|
||||
},
|
||||
[RELOAD_CARD]: {
|
||||
next: (state, { payload }) => (payload.id ? copy(payload) : null),
|
||||
},
|
||||
[SET_CARD_AND_RUN]: {
|
||||
next: (state, { payload }) =>
|
||||
payload.originalCard ? copy(payload.originalCard) : null,
|
||||
},
|
||||
[API_CREATE_QUESTION]: {
|
||||
next: (state, { payload }) => copy(payload),
|
||||
},
|
||||
[API_UPDATE_QUESTION]: {
|
||||
next: (state, { payload }) => copy(payload),
|
||||
},
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// references to FK tables specifically used on the ObjectDetail page.
|
||||
export const tableForeignKeyReferences = handleActions(
|
||||
{
|
||||
[LOAD_OBJECT_DETAIL_FK_REFERENCES]: {
|
||||
next: (state, { payload }) => payload,
|
||||
},
|
||||
[CLEAR_OBJECT_DETAIL_FK_REFERENCES]: () => null,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
export const lastRunCard = handleActions(
|
||||
{
|
||||
[RESET_QB]: { next: (state, { payload }) => null },
|
||||
[QUERY_COMPLETED]: { next: (state, { payload }) => payload.card },
|
||||
[QUERY_ERRORED]: { next: (state, { payload }) => null },
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// The results of a query execution. optionally an error if the query fails to complete successfully.
|
||||
export const queryResults = handleActions(
|
||||
{
|
||||
[RESET_QB]: { next: (state, { payload }) => null },
|
||||
[QUERY_COMPLETED]: {
|
||||
next: (state, { payload: { queryResults } }) => queryResults,
|
||||
},
|
||||
[QUERY_ERRORED]: {
|
||||
next: (state, { payload }) => (payload ? [payload] : state),
|
||||
},
|
||||
[CLEAR_QUERY_RESULT]: { next: (state, { payload }) => null },
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
export const metadataDiff = handleActions(
|
||||
{
|
||||
[RESET_QB]: { next: () => ({}) },
|
||||
[API_CREATE_QUESTION]: { next: () => ({}) },
|
||||
[API_UPDATE_QUESTION]: { next: () => ({}) },
|
||||
[SET_METADATA_DIFF]: {
|
||||
next: (state, { payload }) => {
|
||||
const { name, changes } = payload;
|
||||
return {
|
||||
...state,
|
||||
[name]: state[name] ? merge(state[name], changes) : changes,
|
||||
};
|
||||
},
|
||||
},
|
||||
[CANCEL_QUESTION_CHANGES]: { next: () => ({}) },
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// promise used for tracking a query execution in progress. when a query is started we capture this.
|
||||
export const cancelQueryDeferred = handleActions(
|
||||
{
|
||||
[RUN_QUERY]: {
|
||||
next: (state, { payload: { cancelQueryDeferred } }) =>
|
||||
cancelQueryDeferred,
|
||||
},
|
||||
[CANCEL_QUERY]: { next: (state, { payload }) => null },
|
||||
[QUERY_COMPLETED]: { next: (state, { payload }) => null },
|
||||
[QUERY_ERRORED]: { next: (state, { payload }) => null },
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
export const queryStartTime = handleActions(
|
||||
{
|
||||
[RUN_QUERY]: { next: (state, { payload }) => performance.now() },
|
||||
[CANCEL_QUERY]: { next: (state, { payload }) => null },
|
||||
[QUERY_COMPLETED]: { next: (state, { payload }) => null },
|
||||
[QUERY_ERRORED]: { next: (state, { payload }) => null },
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
export const parameterValues = handleActions(
|
||||
{
|
||||
[INITIALIZE_QB]: {
|
||||
next: (state, { payload: { parameterValues } }) => parameterValues,
|
||||
},
|
||||
[SET_PARAMETER_VALUE]: {
|
||||
next: (state, { payload: { id, value } }) => assoc(state, id, value),
|
||||
},
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
export const currentState = handleActions(
|
||||
{
|
||||
[SET_CURRENT_STATE]: { next: (state, { payload }) => payload },
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
export const parentEntity = handleActions(
|
||||
{
|
||||
[NAVIGATE_TO_NEW_CARD]: {
|
||||
next: (_state, { payload: { id, model, name } }) => ({
|
||||
id,
|
||||
model,
|
||||
name,
|
||||
isEditing: false,
|
||||
}),
|
||||
},
|
||||
[EDIT_QUESTION]: {
|
||||
next: (_state, { payload: { id, model, name } }) => ({
|
||||
id,
|
||||
model,
|
||||
name,
|
||||
isEditing: true,
|
||||
}),
|
||||
},
|
||||
[CLOSE_QB]: { next: () => DEFAULT_PARENT_ENTITY_STATE },
|
||||
},
|
||||
DEFAULT_PARENT_ENTITY_STATE,
|
||||
);
|
||||
|
||||
export const visibleTimelineEventIds = handleActions(
|
||||
{
|
||||
[INITIALIZE_QB]: { next: () => [] },
|
||||
[SHOW_TIMELINE_EVENTS]: {
|
||||
next: (state, { payload: events }) =>
|
||||
_.uniq([...state, ...events.map((event) => event.id)]),
|
||||
},
|
||||
[HIDE_TIMELINE_EVENTS]: {
|
||||
next: (state, { payload: events }) => {
|
||||
const eventIdsToHide = events.map((event) => event.id);
|
||||
return state.filter((eventId) => !eventIdsToHide.includes(eventId));
|
||||
},
|
||||
},
|
||||
[TimelineEvents.actionTypes.CREATE]: {
|
||||
next: (state, { payload }) => [...state, payload.timelineEvent.id],
|
||||
},
|
||||
[RESET_QB]: { next: () => [] },
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
export const selectedTimelineEventIds = handleActions(
|
||||
{
|
||||
[INITIALIZE_QB]: { next: () => [] },
|
||||
[SELECT_TIMELINE_EVENTS]: {
|
||||
next: (state, { payload: events = [] }) => events.map((e) => e.id),
|
||||
},
|
||||
[DESELECT_TIMELINE_EVENTS]: {
|
||||
next: () => [],
|
||||
},
|
||||
[onCloseTimelines]: { next: () => [] },
|
||||
[RESET_QB]: { next: () => [] },
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
export * from "./reducers-typed";
|
||||
622
frontend/src/metabase/query_builder/reducers.ts
Normal file
622
frontend/src/metabase/query_builder/reducers.ts
Normal file
@@ -0,0 +1,622 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import { assoc, merge } from "icepick";
|
||||
import _ from "underscore";
|
||||
|
||||
import {
|
||||
EDIT_QUESTION,
|
||||
NAVIGATE_TO_NEW_CARD,
|
||||
} from "metabase/dashboard/actions";
|
||||
import { TimelineEvents } from "metabase/entities/timeline-events";
|
||||
import type { Deferred } from "metabase/lib/promise";
|
||||
import { copy } from "metabase/lib/utils";
|
||||
import type {
|
||||
Card,
|
||||
CollectionItemModel,
|
||||
Dataset,
|
||||
Field,
|
||||
NativeQuerySnippet,
|
||||
ParameterValuesMap,
|
||||
TimelineEvent,
|
||||
} from "metabase-types/api";
|
||||
import type {
|
||||
ForeignKeyReference,
|
||||
InitialChartSettingState,
|
||||
QueryBuilderLoadingControls,
|
||||
QueryBuilderParentEntityState,
|
||||
QueryBuilderQueryStatus,
|
||||
QueryBuilderUIControls,
|
||||
Range,
|
||||
} from "metabase-types/store";
|
||||
|
||||
import {
|
||||
API_CREATE_QUESTION,
|
||||
API_UPDATE_QUESTION,
|
||||
CANCEL_QUERY,
|
||||
CANCEL_QUESTION_CHANGES,
|
||||
CLEAR_OBJECT_DETAIL_FK_REFERENCES,
|
||||
CLEAR_QUERY_RESULT,
|
||||
CLOSE_AI_QUESTION_ANALYSIS_SIDEBAR,
|
||||
CLOSE_CHART_SETTINGS,
|
||||
CLOSE_CHART_TYPE,
|
||||
CLOSE_QB,
|
||||
CLOSE_QB_NEWB_MODAL,
|
||||
CLOSE_QUESTION_INFO,
|
||||
CLOSE_QUESTION_SETTINGS,
|
||||
CLOSE_SIDEBARS,
|
||||
CLOSE_TIMELINES,
|
||||
DESELECT_TIMELINE_EVENTS,
|
||||
HIDE_TIMELINE_EVENTS,
|
||||
INITIALIZE_QB,
|
||||
LOAD_OBJECT_DETAIL_FK_REFERENCES,
|
||||
ON_CLOSE_SUMMARY,
|
||||
ON_EDIT_SUMMARY,
|
||||
OPEN_AI_QUESTION_ANALYSIS_SIDEBAR,
|
||||
OPEN_CHART_SETTINGS,
|
||||
OPEN_CHART_TYPE,
|
||||
OPEN_DATA_REFERENCE_AT_QUESTION,
|
||||
OPEN_QUESTION_INFO,
|
||||
OPEN_QUESTION_SETTINGS,
|
||||
OPEN_TIMELINES,
|
||||
QUERY_COMPLETED,
|
||||
QUERY_ERRORED,
|
||||
RELOAD_CARD,
|
||||
RESET_QB,
|
||||
RESET_ROW_ZOOM,
|
||||
RESET_UI_CONTROLS,
|
||||
RUN_QUERY,
|
||||
SELECT_TIMELINE_EVENTS,
|
||||
SET_CARD_AND_RUN,
|
||||
SET_CURRENT_STATE,
|
||||
SET_DATA_REFERENCE_STACK,
|
||||
SET_DOCUMENT_TITLE,
|
||||
SET_DOCUMENT_TITLE_TIMEOUT_ID,
|
||||
SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
|
||||
SET_METADATA_DIFF,
|
||||
SET_MODAL_SNIPPET,
|
||||
SET_NATIVE_EDITOR_SELECTED_RANGE,
|
||||
SET_PARAMETER_VALUE,
|
||||
SET_SHOW_LOADING_COMPLETE_FAVICON,
|
||||
SET_SNIPPET_COLLECTION_ID,
|
||||
SET_UI_CONTROLS,
|
||||
SHOW_CHART_SETTINGS,
|
||||
SHOW_TIMELINE_EVENTS,
|
||||
TOGGLE_DATA_REFERENCE,
|
||||
TOGGLE_SNIPPET_SIDEBAR,
|
||||
TOGGLE_TEMPLATE_TAGS_EDITOR,
|
||||
UPDATE_QUESTION,
|
||||
ZOOM_IN_ROW,
|
||||
} from "./actions";
|
||||
import {
|
||||
CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
DEFAULT_LOADING_CONTROLS,
|
||||
DEFAULT_PARENT_ENTITY_STATE,
|
||||
DEFAULT_QUERY_STATUS,
|
||||
DEFAULT_UI_CONTROLS,
|
||||
UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
} from "./defaults";
|
||||
|
||||
function setUIControls(
|
||||
state: QueryBuilderUIControls,
|
||||
changes: Partial<QueryBuilderUIControls>,
|
||||
): QueryBuilderUIControls {
|
||||
const { queryBuilderMode: currentQBMode, ...currentState } = state;
|
||||
const { queryBuilderMode: nextQBMode, ...nextStateChanges } = changes;
|
||||
|
||||
const isChangingQBMode = nextQBMode && currentQBMode !== nextQBMode;
|
||||
const isOpeningEditingQBMode = isChangingQBMode && nextQBMode !== "view";
|
||||
|
||||
const queryBuilderMode = nextQBMode || currentQBMode;
|
||||
// previousQueryBuilderMode is typed as boolean but stores QueryBuilderMode | false in practice
|
||||
const previousQueryBuilderMode = isChangingQBMode
|
||||
? currentQBMode
|
||||
: state.previousQueryBuilderMode;
|
||||
|
||||
// Close all the sidebars when entering notebook/dataset QB modes
|
||||
const extraState = isOpeningEditingQBMode ? UI_CONTROLS_SIDEBAR_DEFAULTS : {};
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
...extraState,
|
||||
...nextStateChanges,
|
||||
queryBuilderMode,
|
||||
previousQueryBuilderMode,
|
||||
};
|
||||
}
|
||||
|
||||
export const uiControls = createReducer<QueryBuilderUIControls>(
|
||||
DEFAULT_UI_CONTROLS,
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: Partial<QueryBuilderUIControls> }
|
||||
>(SET_UI_CONTROLS, (state, action) =>
|
||||
setUIControls(state, action.payload),
|
||||
)
|
||||
.addCase(RESET_UI_CONTROLS, (state) => ({
|
||||
...DEFAULT_UI_CONTROLS,
|
||||
isRunning: state.isRunning,
|
||||
}))
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { uiControls: QueryBuilderUIControls } }
|
||||
>(INITIALIZE_QB, (state, action) => ({
|
||||
...state,
|
||||
...DEFAULT_UI_CONTROLS,
|
||||
...action.payload.uiControls,
|
||||
}))
|
||||
.addCase(UPDATE_QUESTION, (state) => ({
|
||||
...state,
|
||||
highlightedNativeQueryLineNumbers:
|
||||
DEFAULT_UI_CONTROLS.highlightedNativeQueryLineNumbers,
|
||||
}))
|
||||
.addCase(TOGGLE_DATA_REFERENCE, (state) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingDataReference: !state.isShowingDataReference,
|
||||
}))
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: QueryBuilderUIControls["dataReferenceStack"] }
|
||||
>(SET_DATA_REFERENCE_STACK, (state, action) => ({
|
||||
...state,
|
||||
dataReferenceStack: action.payload,
|
||||
}))
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: QueryBuilderUIControls["dataReferenceStack"] }
|
||||
>(OPEN_DATA_REFERENCE_AT_QUESTION, (state, action) =>
|
||||
action.payload
|
||||
? {
|
||||
...state,
|
||||
dataReferenceStack: action.payload,
|
||||
isShowingDataReference: true,
|
||||
}
|
||||
: state,
|
||||
)
|
||||
.addCase(TOGGLE_TEMPLATE_TAGS_EDITOR, (state) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingTemplateTagsEditor: !state.isShowingTemplateTagsEditor,
|
||||
}))
|
||||
.addCase(TOGGLE_SNIPPET_SIDEBAR, (state) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingSnippetSidebar: !state.isShowingSnippetSidebar,
|
||||
snippetCollectionId: null,
|
||||
}))
|
||||
.addCase<string, { type: string; isShowingTemplateTagsEditor: boolean }>(
|
||||
SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
|
||||
(state, action) => ({
|
||||
...state,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingTemplateTagsEditor: action.isShowingTemplateTagsEditor,
|
||||
}),
|
||||
)
|
||||
.addCase<string, { type: string; payload: Range[] }>(
|
||||
SET_NATIVE_EDITOR_SELECTED_RANGE,
|
||||
(state, action) => ({
|
||||
...state,
|
||||
nativeEditorSelectedRange: action.payload,
|
||||
}),
|
||||
)
|
||||
.addCase<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
payload:
|
||||
| NativeQuerySnippet
|
||||
| Partial<Omit<NativeQuerySnippet, "id">>
|
||||
| null;
|
||||
}
|
||||
>(SET_MODAL_SNIPPET, (state, action) => ({
|
||||
...state,
|
||||
modalSnippet: action.payload,
|
||||
}))
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: QueryBuilderUIControls["snippetCollectionId"] }
|
||||
>(SET_SNIPPET_COLLECTION_ID, (state, action) => ({
|
||||
...state,
|
||||
snippetCollectionId: action.payload,
|
||||
}))
|
||||
.addCase(CLOSE_QB_NEWB_MODAL, (state) => ({
|
||||
...state,
|
||||
isShowingNewbModal: false,
|
||||
}))
|
||||
.addCase(RUN_QUERY, (state) => ({
|
||||
...state,
|
||||
isRunning: true,
|
||||
highlightedNativeQueryLineNumbers:
|
||||
DEFAULT_UI_CONTROLS.highlightedNativeQueryLineNumbers,
|
||||
}))
|
||||
.addCase(CANCEL_QUERY, (state) => ({ ...state, isRunning: false }))
|
||||
.addCase(QUERY_COMPLETED, (state) => ({ ...state, isRunning: false }))
|
||||
.addCase(QUERY_ERRORED, (state) => ({ ...state, isRunning: false }))
|
||||
.addCase<string, { type: string; payload: InitialChartSettingState }>(
|
||||
SHOW_CHART_SETTINGS,
|
||||
(state, action) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingChartSettingsSidebar: true,
|
||||
initialChartSetting: action.payload,
|
||||
}),
|
||||
)
|
||||
.addCase(CANCEL_QUESTION_CHANGES, (state) => ({
|
||||
...state,
|
||||
isModifiedFromNotebook: false,
|
||||
}))
|
||||
.addCase(ON_EDIT_SUMMARY, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingSummarySidebar: true,
|
||||
}))
|
||||
.addCase(ON_CLOSE_SUMMARY, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}))
|
||||
.addCase(OPEN_AI_QUESTION_ANALYSIS_SIDEBAR, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingAIQuestionAnalysisSidebar: true,
|
||||
}))
|
||||
.addCase(CLOSE_AI_QUESTION_ANALYSIS_SIDEBAR, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}))
|
||||
.addCase<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
payload:
|
||||
| {
|
||||
initialChartSettings?: InitialChartSettingState;
|
||||
showSidebarTitle?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
>(OPEN_CHART_SETTINGS, (state, action) => {
|
||||
const { initialChartSettings, showSidebarTitle = false } =
|
||||
action.payload ?? {};
|
||||
return {
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingChartSettingsSidebar: true,
|
||||
initialChartSetting: initialChartSettings ?? {},
|
||||
showSidebarTitle,
|
||||
};
|
||||
})
|
||||
.addCase(CLOSE_CHART_SETTINGS, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}))
|
||||
.addCase(OPEN_CHART_TYPE, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
isShowingChartTypeSidebar: true,
|
||||
}))
|
||||
.addCase(CLOSE_CHART_TYPE, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}))
|
||||
.addCase(OPEN_QUESTION_INFO, (state) =>
|
||||
setUIControls(state, {
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingQuestionInfoSidebar: true,
|
||||
queryBuilderMode: "view",
|
||||
}),
|
||||
)
|
||||
.addCase(CLOSE_QUESTION_INFO, (state) => ({
|
||||
...state,
|
||||
isShowingQuestionInfoSidebar: false,
|
||||
}))
|
||||
.addCase(OPEN_QUESTION_SETTINGS, (state) =>
|
||||
setUIControls(state, {
|
||||
...(UI_CONTROLS_SIDEBAR_DEFAULTS as Partial<QueryBuilderUIControls>),
|
||||
isShowingQuestionSettingsSidebar: true,
|
||||
queryBuilderMode: "view",
|
||||
} as Partial<QueryBuilderUIControls>),
|
||||
)
|
||||
.addCase(CLOSE_QUESTION_SETTINGS, (state) => ({
|
||||
...state,
|
||||
isShowingQuestionSettingsSidebar: false,
|
||||
}))
|
||||
.addCase(OPEN_TIMELINES, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
...CLOSED_NATIVE_EDITOR_SIDEBARS,
|
||||
isShowingTimelineSidebar: true,
|
||||
}))
|
||||
.addCase(CLOSE_TIMELINES, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}))
|
||||
.addCase(CLOSE_SIDEBARS, (state) => ({
|
||||
...state,
|
||||
...UI_CONTROLS_SIDEBAR_DEFAULTS,
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
export const loadingControls = createReducer<QueryBuilderLoadingControls>(
|
||||
DEFAULT_LOADING_CONTROLS,
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase<string, { type: string; payload: string }>(
|
||||
SET_DOCUMENT_TITLE,
|
||||
(state, action) => ({
|
||||
...state,
|
||||
documentTitle: action.payload,
|
||||
}),
|
||||
)
|
||||
.addCase<string, { type: string; payload: boolean }>(
|
||||
SET_SHOW_LOADING_COMPLETE_FAVICON,
|
||||
(state, action) => ({
|
||||
...state,
|
||||
showLoadCompleteFavicon: action.payload,
|
||||
}),
|
||||
)
|
||||
.addCase<string, { type: string; payload: string }>(
|
||||
SET_DOCUMENT_TITLE_TIMEOUT_ID,
|
||||
(state, action) => ({
|
||||
...state,
|
||||
timeoutId: action.payload,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const queryStatus = createReducer<QueryBuilderQueryStatus>(
|
||||
DEFAULT_QUERY_STATUS,
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase(RUN_QUERY, () => "running" as const)
|
||||
.addCase(QUERY_COMPLETED, () => "complete" as const)
|
||||
.addCase(CANCEL_QUERY, () => "idle" as const);
|
||||
},
|
||||
);
|
||||
|
||||
export const zoomedRowObjectId = createReducer<number | string | null>(
|
||||
null,
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { objectId?: number | string } | undefined }
|
||||
>(INITIALIZE_QB, (_state, action) => action.payload?.objectId ?? null)
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { objectId: number | string } }
|
||||
>(ZOOM_IN_ROW, (_state, action) => action.payload.objectId)
|
||||
.addCase(RESET_ROW_ZOOM, () => null)
|
||||
.addCase(RESET_QB, () => null);
|
||||
},
|
||||
);
|
||||
|
||||
// a copy of the card being worked on at its last known saved state. if the card is NEW then this should be null.
|
||||
// NOTE: we use JSON serialization/deserialization to ensure a deep clone of the object which is required
|
||||
// because we can't have any links between the active card being modified and the "originalCard" for testing dirtiness
|
||||
// ALSO: we consistently check for payload.id because an unsaved card has no "originalCard"
|
||||
export const originalCard = createReducer<Card | null>(null, (builder) => {
|
||||
builder
|
||||
.addCase<string, { type: string; payload: { originalCard?: Card } }>(
|
||||
INITIALIZE_QB,
|
||||
(_state, action) =>
|
||||
action.payload.originalCard ? copy(action.payload.originalCard) : null,
|
||||
)
|
||||
.addCase<string, { type: string; payload: Card }>(
|
||||
RELOAD_CARD,
|
||||
(_state, action) => (action.payload.id ? copy(action.payload) : null),
|
||||
)
|
||||
.addCase<string, { type: string; payload: { originalCard?: Card } }>(
|
||||
SET_CARD_AND_RUN,
|
||||
(_state, action) =>
|
||||
action.payload.originalCard ? copy(action.payload.originalCard) : null,
|
||||
)
|
||||
.addCase<string, { type: string; payload: Card }>(
|
||||
API_CREATE_QUESTION,
|
||||
(_state, action) => copy(action.payload),
|
||||
)
|
||||
.addCase<string, { type: string; payload: Card }>(
|
||||
API_UPDATE_QUESTION,
|
||||
(_state, action) => copy(action.payload),
|
||||
);
|
||||
});
|
||||
|
||||
// references to FK tables specifically used on the ObjectDetail page.
|
||||
export const tableForeignKeyReferences = createReducer<Record<
|
||||
number,
|
||||
ForeignKeyReference
|
||||
> | null>(null, (builder) => {
|
||||
builder
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: Record<number, ForeignKeyReference> }
|
||||
>(LOAD_OBJECT_DETAIL_FK_REFERENCES, (_state, action) => action.payload)
|
||||
.addCase(CLEAR_OBJECT_DETAIL_FK_REFERENCES, () => null);
|
||||
});
|
||||
|
||||
export const lastRunCard = createReducer<Card | null>(null, (builder) => {
|
||||
builder
|
||||
.addCase(RESET_QB, () => null)
|
||||
.addCase<string, { type: string; payload: { card: Card } }>(
|
||||
QUERY_COMPLETED,
|
||||
(_state, action) => action.payload.card,
|
||||
)
|
||||
.addCase(QUERY_ERRORED, () => null);
|
||||
});
|
||||
|
||||
// The results of a query execution. optionally an error if the query fails to complete successfully.
|
||||
export const queryResults = createReducer<Dataset[] | null>(null, (builder) => {
|
||||
// @ts-expect-error — Draft<Dataset[]> triggers TS2589 due to Dataset's recursive types
|
||||
builder
|
||||
.addCase(RESET_QB, () => null)
|
||||
.addCase<string, { type: string; payload: { queryResults: Dataset[] } }>(
|
||||
QUERY_COMPLETED,
|
||||
(_state, action) => action.payload.queryResults,
|
||||
)
|
||||
.addCase<string, { type: string; payload: Dataset | null }>(
|
||||
QUERY_ERRORED,
|
||||
(state, action) => (action.payload ? [action.payload] : state),
|
||||
)
|
||||
.addCase(CLEAR_QUERY_RESULT, () => null);
|
||||
});
|
||||
|
||||
export const metadataDiff = createReducer<Record<string, Partial<Field>>>(
|
||||
{},
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase(RESET_QB, () => ({}))
|
||||
.addCase(API_CREATE_QUESTION, () => ({}))
|
||||
.addCase(API_UPDATE_QUESTION, () => ({}))
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { name: string; changes: Partial<Field> } }
|
||||
>(SET_METADATA_DIFF, (state, action) => {
|
||||
const { name, changes } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[name]: state[name] ? merge(state[name], changes) : changes,
|
||||
};
|
||||
})
|
||||
.addCase(CANCEL_QUESTION_CHANGES, () => ({}));
|
||||
},
|
||||
);
|
||||
|
||||
// promise used for tracking a query execution in progress. when a query is started we capture this.
|
||||
export const cancelQueryDeferred = createReducer<Deferred<void> | null>(
|
||||
null,
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { cancelQueryDeferred: Deferred<void> } }
|
||||
>(RUN_QUERY, (_state, action) => action.payload.cancelQueryDeferred)
|
||||
.addCase(CANCEL_QUERY, () => null)
|
||||
.addCase(QUERY_COMPLETED, () => null)
|
||||
.addCase(QUERY_ERRORED, () => null);
|
||||
},
|
||||
);
|
||||
|
||||
export const queryStartTime = createReducer<number | null>(null, (builder) => {
|
||||
builder
|
||||
.addCase(RUN_QUERY, () => performance.now())
|
||||
.addCase(CANCEL_QUERY, () => null)
|
||||
.addCase(QUERY_COMPLETED, () => null)
|
||||
.addCase(QUERY_ERRORED, () => null);
|
||||
});
|
||||
|
||||
export const parameterValues = createReducer<ParameterValuesMap>(
|
||||
{},
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { parameterValues: ParameterValuesMap } }
|
||||
>(INITIALIZE_QB, (_state, action) => action.payload.parameterValues)
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { id: string; value: unknown } }
|
||||
>(SET_PARAMETER_VALUE, (state, action) => assoc(state, action.payload.id, action.payload.value));
|
||||
},
|
||||
);
|
||||
|
||||
export const currentState = createReducer<{
|
||||
card: Card;
|
||||
cardId?: number;
|
||||
serializedCard: string;
|
||||
} | null>(null, (builder) => {
|
||||
builder.addCase<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
payload: { card: Card; cardId?: number; serializedCard: string };
|
||||
}
|
||||
>(SET_CURRENT_STATE, (_state, action) => action.payload);
|
||||
});
|
||||
|
||||
export const parentEntity = createReducer<QueryBuilderParentEntityState>(
|
||||
DEFAULT_PARENT_ENTITY_STATE,
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
payload: {
|
||||
id: number | string | null;
|
||||
model: CollectionItemModel | null;
|
||||
name: string | null;
|
||||
};
|
||||
}
|
||||
>(NAVIGATE_TO_NEW_CARD, (_state, action) => ({
|
||||
id: action.payload.id,
|
||||
model: action.payload.model,
|
||||
name: action.payload.name,
|
||||
isEditing: false,
|
||||
}))
|
||||
.addCase<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
payload: {
|
||||
id: number | string | null;
|
||||
model: CollectionItemModel | null;
|
||||
name: string | null;
|
||||
};
|
||||
}
|
||||
>(EDIT_QUESTION, (_state, action) => ({
|
||||
id: action.payload.id,
|
||||
model: action.payload.model,
|
||||
name: action.payload.name,
|
||||
isEditing: true,
|
||||
}))
|
||||
.addCase(CLOSE_QB, () => DEFAULT_PARENT_ENTITY_STATE);
|
||||
},
|
||||
);
|
||||
|
||||
export const visibleTimelineEventIds = createReducer<number[]>(
|
||||
[],
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase(INITIALIZE_QB, () => [])
|
||||
.addCase<string, { type: string; payload: TimelineEvent[] }>(
|
||||
SHOW_TIMELINE_EVENTS,
|
||||
(state, action) =>
|
||||
_.uniq([...state, ...action.payload.map((event) => event.id)]),
|
||||
)
|
||||
.addCase<string, { type: string; payload: TimelineEvent[] }>(
|
||||
HIDE_TIMELINE_EVENTS,
|
||||
(state, action) => {
|
||||
const eventIdsToHide = action.payload.map((event) => event.id);
|
||||
return state.filter((eventId) => !eventIdsToHide.includes(eventId));
|
||||
},
|
||||
)
|
||||
.addCase<
|
||||
string,
|
||||
{ type: string; payload: { timelineEvent: { id: number } } }
|
||||
>(TimelineEvents.actionTypes.CREATE, (state, action) => [
|
||||
...state,
|
||||
action.payload.timelineEvent.id,
|
||||
])
|
||||
.addCase(RESET_QB, () => []);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectedTimelineEventIds = createReducer<number[]>(
|
||||
[],
|
||||
(builder) => {
|
||||
builder
|
||||
.addCase(INITIALIZE_QB, () => [])
|
||||
.addCase<string, { type: string; payload: TimelineEvent[] | undefined }>(
|
||||
SELECT_TIMELINE_EVENTS,
|
||||
(_state, action) => (action.payload ?? []).map((e) => e.id),
|
||||
)
|
||||
.addCase(DESELECT_TIMELINE_EVENTS, () => [])
|
||||
.addCase(CLOSE_TIMELINES, () => [])
|
||||
.addCase(RESET_QB, () => []);
|
||||
},
|
||||
);
|
||||
|
||||
export * from "./reducers-typed";
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
extractRemappings,
|
||||
getVisualizationTransformed,
|
||||
} from "metabase/visualizations";
|
||||
import type { ObjectId } from "metabase/visualizations/components/ObjectDetail/types";
|
||||
import type { TimeSeriesInterval } from "metabase/visualizations/echarts/cartesian/model/types";
|
||||
import {
|
||||
computeTimeseriesDataInterval,
|
||||
minTimeseriesUnit,
|
||||
@@ -32,13 +34,27 @@ import {
|
||||
import { getComputedSettingsForSeries } from "metabase/visualizations/lib/settings/visualization";
|
||||
import * as Lib from "metabase-lib";
|
||||
import Question from "metabase-lib/v1/Question";
|
||||
import type Database from "metabase-lib/v1/metadata/Database";
|
||||
import type Table from "metabase-lib/v1/metadata/Table";
|
||||
import { getCardUiParameters } from "metabase-lib/v1/parameters/utils/cards";
|
||||
import {
|
||||
normalizeParameterValue,
|
||||
normalizeParameters,
|
||||
} from "metabase-lib/v1/parameters/utils/parameter-values";
|
||||
import { getIsPKFromTablePredicate } from "metabase-lib/v1/types/utils/isa";
|
||||
import type { TimelineEvent } from "metabase-types/analytics";
|
||||
import type {
|
||||
Bookmark,
|
||||
Dataset,
|
||||
DatasetColumn,
|
||||
DatasetQuery,
|
||||
Field,
|
||||
RowValue,
|
||||
Series,
|
||||
Timeline,
|
||||
} from "metabase-types/api";
|
||||
import { isAbsoluteDateTimeUnit } from "metabase-types/guards/date-time";
|
||||
import type { State } from "metabase-types/store";
|
||||
|
||||
import { getQuestionWithDefaultVisualizationSettings } from "./actions/core/utils";
|
||||
import { createRawSeries, getWritableColumnProperties } from "./utils";
|
||||
@@ -49,22 +65,22 @@ import {
|
||||
} from "./utils/question";
|
||||
|
||||
// This selector can be called from public questions / dashboards, which do not have state.qb
|
||||
export const getUiControls = (state) => state.qb?.uiControls;
|
||||
export const getQueryStatus = (state) => state.qb.queryStatus;
|
||||
export const getLoadingControls = (state) => state.qb.loadingControls;
|
||||
export const getUiControls = (state: State) => state.qb?.uiControls;
|
||||
export const getQueryStatus = (state: State) => state.qb.queryStatus;
|
||||
export const getLoadingControls = (state: State) => state.qb.loadingControls;
|
||||
|
||||
export const getIsShowingTemplateTagsEditor = (state) =>
|
||||
export const getIsShowingTemplateTagsEditor = (state: State) =>
|
||||
getUiControls(state).isShowingTemplateTagsEditor;
|
||||
export const getIsShowingSnippetSidebar = (state) =>
|
||||
export const getIsShowingSnippetSidebar = (state: State) =>
|
||||
getUiControls(state).isShowingSnippetSidebar;
|
||||
export const getIsShowingDataReference = (state) =>
|
||||
export const getIsShowingDataReference = (state: State) =>
|
||||
getUiControls(state).isShowingDataReference;
|
||||
export const getHighlightedNativeQueryLineNumbers = (state) =>
|
||||
export const getHighlightedNativeQueryLineNumbers = (state: State) =>
|
||||
getUiControls(state).highlightedNativeQueryLineNumbers;
|
||||
|
||||
// This selector can be called from public questions / dashboards, which do not
|
||||
// have state.qb
|
||||
export const getIsShowingRawTable = (state) =>
|
||||
export const getIsShowingRawTable = (state: State) =>
|
||||
!!state.qb?.uiControls.isShowingRawTable;
|
||||
|
||||
const SIDEBARS = [
|
||||
@@ -79,37 +95,38 @@ const SIDEBARS = [
|
||||
"isShowingDataReference",
|
||||
"isShowingTemplateTagsEditor",
|
||||
"isShowingSnippetSidebar",
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const getIsAnySidebarOpen = createSelector(
|
||||
[getUiControls],
|
||||
(uiControls) => SIDEBARS.some((sidebar) => uiControls[sidebar]),
|
||||
);
|
||||
|
||||
export const getIsRunning = (state) => getUiControls(state).isRunning;
|
||||
export const getIsLoadingComplete = (state) =>
|
||||
export const getIsRunning = (state: State) => getUiControls(state).isRunning;
|
||||
export const getIsLoadingComplete = (state: State) =>
|
||||
getQueryStatus(state) === "complete";
|
||||
|
||||
export const getCard = (state) => state.qb.card;
|
||||
export const getOriginalCard = (state) => state.qb.originalCard;
|
||||
export const getLastRunCard = (state) => state.qb.lastRunCard;
|
||||
export const getCard = (state: State) => state.qb.card;
|
||||
export const getOriginalCard = (state: State) => state.qb.originalCard;
|
||||
export const getLastRunCard = (state: State) => state.qb.lastRunCard;
|
||||
|
||||
export const getParameterValues = (state) => state.qb.parameterValues;
|
||||
export const getParameterValuesSearchCache = (state) =>
|
||||
state.qb.parameterValuesSearchCache;
|
||||
export const getParameterValues = (state: State) => state.qb.parameterValues;
|
||||
|
||||
export const getMetadataDiff = (state) => state.qb.metadataDiff;
|
||||
export const getMetadataDiff = (state: State) => state.qb.metadataDiff;
|
||||
|
||||
export const getEntities = (state) => state.entities;
|
||||
export const getVisibleTimelineEventIds = (state) =>
|
||||
export const getEntities = (state: State) => state.entities;
|
||||
export const getVisibleTimelineEventIds = (state: State) =>
|
||||
state.qb.visibleTimelineEventIds;
|
||||
export const getSelectedTimelineEventIds = (state) =>
|
||||
export const getSelectedTimelineEventIds = (state: State) =>
|
||||
state.qb.selectedTimelineEventIds;
|
||||
|
||||
const getRawQueryResults = (state) => state.qb.queryResults;
|
||||
const getRawQueryResults = (state: State) => state.qb.queryResults;
|
||||
|
||||
export const getIsBookmarked = (state, props) =>
|
||||
props.bookmarks.some(
|
||||
export const getIsBookmarked = (
|
||||
state: State,
|
||||
{ bookmarks }: { bookmarks: Bookmark[] },
|
||||
) =>
|
||||
bookmarks.some(
|
||||
(bookmark) =>
|
||||
bookmark.type === "card" && bookmark.item_id === state.qb.card?.id,
|
||||
);
|
||||
@@ -119,18 +136,18 @@ export const getQueryBuilderMode = createSelector(
|
||||
(uiControls) => uiControls.queryBuilderMode,
|
||||
);
|
||||
|
||||
export const getQueryStartTime = (state) => state.qb.queryStartTime;
|
||||
export const getQueryStartTime = (state: State) => state.qb.queryStartTime;
|
||||
|
||||
export const getDatabaseId = createSelector(
|
||||
[getCard],
|
||||
(card) => card && card.dataset_query && card.dataset_query.database,
|
||||
);
|
||||
|
||||
export const getTableForeignKeyReferences = (state) =>
|
||||
export const getTableForeignKeyReferences = (state: State) =>
|
||||
state.qb.tableForeignKeyReferences;
|
||||
|
||||
const getDatabasesListDefaultValue = [];
|
||||
export const getDatabasesList = (state) =>
|
||||
const getDatabasesListDefaultValue: Database[] = [];
|
||||
export const getDatabasesList = (state: State) =>
|
||||
Databases.selectors.getList(state, {
|
||||
entityQuery: { include: "tables", saved: true },
|
||||
}) || getDatabasesListDefaultValue;
|
||||
@@ -146,7 +163,7 @@ export const getSampleDatabaseId = createSelector(
|
||||
export const getParameters = createSelector(
|
||||
[getCard, getMetadata, getParameterValues],
|
||||
(card, metadata, parameterValues) =>
|
||||
getCardUiParameters(card, metadata, parameterValues),
|
||||
card ? getCardUiParameters(card, metadata, parameterValues) : [],
|
||||
);
|
||||
|
||||
const getLastRunDatasetQuery = createSelector(
|
||||
@@ -166,7 +183,8 @@ export const getDatasetEditorTab = createSelector(
|
||||
|
||||
export const getOriginalQuestion = createSelector(
|
||||
[getMetadata, getOriginalCard],
|
||||
(metadata, card) => metadata && card && new Question(card, metadata),
|
||||
(metadata, card) =>
|
||||
(metadata && card && new Question(card, metadata)) ?? undefined,
|
||||
);
|
||||
|
||||
export const getOriginalQuestionWithParameterValues = createSelector(
|
||||
@@ -218,9 +236,8 @@ export const getQuestion = createSelector(
|
||||
/**
|
||||
* Returns whether the current question is a native query
|
||||
*/
|
||||
export const getIsNative = createSelector(
|
||||
[getQuestion],
|
||||
(question) => question && Lib.queryDisplayInfo(question.query()).isNative,
|
||||
export const getIsNative = createSelector([getQuestion], (question) =>
|
||||
question ? Lib.queryDisplayInfo(question.query()).isNative : false,
|
||||
);
|
||||
|
||||
const getCardResultMetadata = createSelector(
|
||||
@@ -252,7 +269,7 @@ const getModelMetadataDiff = createSelector(
|
||||
|
||||
export const getQueryResults = createSelector(
|
||||
[getRawQueryResults, getModelMetadataDiff],
|
||||
(queryResults, metadataDiff) => {
|
||||
(queryResults, metadataDiff): Dataset[] | null => {
|
||||
if (!Array.isArray(queryResults) || !queryResults.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -263,7 +280,7 @@ export const getQueryResults = createSelector(
|
||||
}
|
||||
const { cols, results_metadata } = result.data;
|
||||
|
||||
function applyMetadataDiff(column) {
|
||||
function applyMetadataDiff(column: Field | DatasetColumn) {
|
||||
const columnDiff = metadataDiff[column.name];
|
||||
return columnDiff ? merge(column, columnDiff) : column;
|
||||
}
|
||||
@@ -347,7 +364,7 @@ export const getPKColumnIndex = createSelector(
|
||||
[getFirstQueryResult, getTableId],
|
||||
(result, tableId) => {
|
||||
if (!result || !result.data) {
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
const { cols } = result.data;
|
||||
|
||||
@@ -364,17 +381,17 @@ export const getPKColumnIndex = createSelector(
|
||||
export const getPKRowIndexMap = createSelector(
|
||||
[getFirstQueryResult, getPKColumnIndex],
|
||||
(result, PKColumnIndex) => {
|
||||
const map = new Map<RowValue, number>();
|
||||
if (!result || !result.data || !Number.isSafeInteger(PKColumnIndex)) {
|
||||
return {};
|
||||
return map;
|
||||
}
|
||||
const { rows } = result.data;
|
||||
if (PKColumnIndex < 0) {
|
||||
return rows.map((_, index) => index);
|
||||
}
|
||||
const map = {};
|
||||
rows.forEach((row, index) => {
|
||||
const PKValue = row[PKColumnIndex];
|
||||
map[PKValue] = index;
|
||||
map.set(PKValue, index);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
@@ -394,7 +411,7 @@ export const getRowIndexToPKMap = createSelector(
|
||||
if (PKColumnIndex < 0) {
|
||||
return rows.map((_, index) => index);
|
||||
}
|
||||
const map = {};
|
||||
const map: Record<number, ObjectId> = {};
|
||||
rows.forEach((row, index) => {
|
||||
const PKValue = row[PKColumnIndex];
|
||||
map[index] = PKValue;
|
||||
@@ -403,11 +420,20 @@ export const getRowIndexToPKMap = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
function areLegacyQueriesEqual(queryA, queryB, tableMetadata) {
|
||||
function areLegacyQueriesEqual(
|
||||
queryA: DatasetQuery | undefined,
|
||||
queryB: DatasetQuery | undefined,
|
||||
tableMetadata: Table,
|
||||
) {
|
||||
if (queryA == null || queryB == null) {
|
||||
return false;
|
||||
}
|
||||
return Lib.areLegacyQueriesEqual(
|
||||
queryA,
|
||||
queryB,
|
||||
tableMetadata?.fields.map(({ id }) => id),
|
||||
tableMetadata?.fields
|
||||
?.map(({ id }) => id)
|
||||
?.filter((id): id is number => typeof id === "number") ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -420,6 +446,11 @@ function areComposedEntitiesEquivalent({
|
||||
lastRunQuestion,
|
||||
currentQuestion,
|
||||
tableMetadata,
|
||||
}: {
|
||||
originalQuestion?: Question | null;
|
||||
lastRunQuestion?: Question | null;
|
||||
currentQuestion?: Question | null;
|
||||
tableMetadata: Table;
|
||||
}) {
|
||||
const isQuestion = originalQuestion?.type() === "question";
|
||||
if (!originalQuestion || !lastRunQuestion || !currentQuestion || isQuestion) {
|
||||
@@ -463,6 +494,11 @@ export function areQueriesEquivalent({
|
||||
lastRunQuestion,
|
||||
currentQuestion,
|
||||
tableMetadata,
|
||||
}: {
|
||||
originalQuestion?: Question | null;
|
||||
lastRunQuestion?: Question | null;
|
||||
currentQuestion?: Question | null;
|
||||
tableMetadata: Table;
|
||||
}) {
|
||||
return (
|
||||
areLegacyQueriesEqual(
|
||||
@@ -500,20 +536,21 @@ export const getIsResultDirty = createSelector(
|
||||
const isEditable =
|
||||
!!currentQuestion &&
|
||||
Lib.queryDisplayInfo(currentQuestion.query()).isEditable;
|
||||
return (
|
||||
return Boolean(
|
||||
haveParametersChanged ||
|
||||
(isEditable &&
|
||||
!areQueriesEquivalent({
|
||||
originalQuestion,
|
||||
lastRunQuestion,
|
||||
currentQuestion,
|
||||
tableMetadata,
|
||||
}))
|
||||
(isEditable &&
|
||||
tableMetadata &&
|
||||
!areQueriesEquivalent({
|
||||
originalQuestion,
|
||||
lastRunQuestion,
|
||||
currentQuestion,
|
||||
tableMetadata,
|
||||
})),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const getZoomedObjectId = (state) => state.qb.zoomedRowObjectId;
|
||||
export const getZoomedObjectId = (state: State) => state.qb.zoomedRowObjectId;
|
||||
|
||||
export const getZoomedObjectRowIndex = createSelector(
|
||||
[getPKRowIndexMap, getZoomedObjectId],
|
||||
@@ -521,14 +558,22 @@ export const getZoomedObjectRowIndex = createSelector(
|
||||
if (!PKRowIndexMap) {
|
||||
return;
|
||||
}
|
||||
return PKRowIndexMap[objectId] ?? PKRowIndexMap[parseInt(objectId)];
|
||||
|
||||
if (PKRowIndexMap instanceof Map) {
|
||||
return PKRowIndexMap.get(objectId);
|
||||
}
|
||||
|
||||
const index =
|
||||
typeof objectId === "string" ? parseInt(objectId) : (objectId ?? -1);
|
||||
|
||||
return PKRowIndexMap[index];
|
||||
},
|
||||
);
|
||||
|
||||
export const getPreviousRowPKValue = createSelector(
|
||||
[getFirstQueryResult, getPKColumnIndex, getZoomedObjectRowIndex],
|
||||
(result, PKColumnIndex, rowIndex) => {
|
||||
if (!result) {
|
||||
if (!result || rowIndex == null) {
|
||||
return;
|
||||
}
|
||||
if (PKColumnIndex === -1) {
|
||||
@@ -542,7 +587,7 @@ export const getPreviousRowPKValue = createSelector(
|
||||
export const getNextRowPKValue = createSelector(
|
||||
[getFirstQueryResult, getPKColumnIndex, getZoomedObjectRowIndex],
|
||||
(result, PKColumnIndex, rowIndex) => {
|
||||
if (!result) {
|
||||
if (!result || rowIndex == null) {
|
||||
return;
|
||||
}
|
||||
if (PKColumnIndex === -1) {
|
||||
@@ -572,7 +617,11 @@ export const getCanZoomNextRow = createSelector(
|
||||
export const getZoomRow = createSelector(
|
||||
[getQueryResults, getZoomedObjectRowIndex],
|
||||
(queryResults, rowIndex) => {
|
||||
if (!Array.isArray(queryResults) || !queryResults.length) {
|
||||
if (
|
||||
!Array.isArray(queryResults) ||
|
||||
!queryResults.length ||
|
||||
rowIndex == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return queryResults[0].data.rows[rowIndex];
|
||||
@@ -662,9 +711,9 @@ export const getShouldShowUnsavedChangesWarning = createSelector(
|
||||
*/
|
||||
export const getRawSeries = createSelector(
|
||||
[getCard, getFirstQueryResult, getLastRunDatasetQuery, getIsShowingRawTable],
|
||||
(card, queryResult, lastRunDatasetQuery, isShowingRawTable) => {
|
||||
(card, queryResult, lastRunDatasetQuery, isShowingRawTable): Series => {
|
||||
const rawSeries = createRawSeries({
|
||||
card,
|
||||
card: card!,
|
||||
queryResult,
|
||||
datasetQuery: lastRunDatasetQuery,
|
||||
});
|
||||
@@ -702,12 +751,12 @@ const _getVisualizationTransformed = createSelector(
|
||||
*/
|
||||
export const getTransformedSeries = createSelector(
|
||||
[_getVisualizationTransformed],
|
||||
(transformed) => transformed && transformed.series,
|
||||
(transformed) => transformed?.series,
|
||||
);
|
||||
|
||||
export const getTransformedVisualization = createSelector(
|
||||
[_getVisualizationTransformed],
|
||||
(transformed) => transformed && transformed.visualization,
|
||||
(transformed) => transformed?.visualization,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -728,12 +777,12 @@ export const getIsNativeEditorOpen = createSelector(
|
||||
|
||||
export const getNativeEditorSelectedRange = createSelector(
|
||||
[getUiControls],
|
||||
(uiControls) => uiControls && uiControls.nativeEditorSelectedRange?.[0],
|
||||
(uiControls) => uiControls?.nativeEditorSelectedRange?.[0],
|
||||
);
|
||||
|
||||
const getNativeEditorSelectedRanges = createSelector(
|
||||
[getUiControls],
|
||||
(uiControls) => uiControls && uiControls.nativeEditorSelectedRange,
|
||||
(uiControls) => uiControls?.nativeEditorSelectedRange,
|
||||
);
|
||||
|
||||
export const getIsTimeseries = createSelector(
|
||||
@@ -744,7 +793,9 @@ export const getIsTimeseries = createSelector(
|
||||
export const getTimeseriesXValues = createSelector(
|
||||
[getIsTimeseries, getTransformedSeries, getVisualizationSettings],
|
||||
(isTimeseries, series, settings) =>
|
||||
isTimeseries && series && settings && getXValues({ series, settings }),
|
||||
isTimeseries && series && settings
|
||||
? getXValues({ series, settings })
|
||||
: null,
|
||||
);
|
||||
|
||||
const getTimeseriesDataInterval = createSelector(
|
||||
@@ -754,7 +805,7 @@ const getTimeseriesDataInterval = createSelector(
|
||||
getIsTimeseries,
|
||||
getTimeseriesXValues,
|
||||
],
|
||||
(series, settings, isTimeseries, xValues) => {
|
||||
(series, settings, isTimeseries, xValues): TimeSeriesInterval | null => {
|
||||
if (!isTimeseries || !xValues) {
|
||||
return null;
|
||||
}
|
||||
@@ -768,29 +819,32 @@ const getTimeseriesDataInterval = createSelector(
|
||||
isAbsoluteDateTimeUnit(column?.unit) ? column.unit : null,
|
||||
)
|
||||
.filter(isNotNull);
|
||||
return computeTimeseriesDataInterval(
|
||||
xValues,
|
||||
minTimeseriesUnit(columnUnits),
|
||||
return (
|
||||
computeTimeseriesDataInterval(xValues, minTimeseriesUnit(columnUnits)) ??
|
||||
null
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
type Domain = [number, number];
|
||||
|
||||
export const getTimeseriesXDomain = createSelector(
|
||||
[getIsTimeseries, getTimeseriesXValues],
|
||||
(isTimeseries, xValues) => {
|
||||
return (
|
||||
isTimeseries &&
|
||||
Array.isArray(xValues) &&
|
||||
xValues.length > 0 &&
|
||||
d3.extent(xValues)
|
||||
);
|
||||
if (isTimeseries && Array.isArray(xValues) && xValues.length > 0) {
|
||||
return d3.extent(xValues) as Domain;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
export const getFetchedTimelines = createSelector([getEntities], (entities) => {
|
||||
const entityQuery = { include: "events" };
|
||||
return Timelines.selectors.getList({ entities }, { entityQuery }) ?? [];
|
||||
});
|
||||
export const getFetchedTimelines = createSelector(
|
||||
[getEntities],
|
||||
(entities): Timeline[] => {
|
||||
const entityQuery = { include: "events" };
|
||||
return Timelines.selectors.getList({ entities }, { entityQuery }) ?? [];
|
||||
},
|
||||
);
|
||||
|
||||
export const getTransformedTimelines = createSelector(
|
||||
[getFetchedTimelines],
|
||||
@@ -808,11 +862,14 @@ export const getTransformedTimelines = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
function isEventWithinDomain(event, xDomain) {
|
||||
function isEventWithinDomain(event: TimelineEvent, xDomain: Domain) {
|
||||
return event.timestamp.isBetween(xDomain[0], xDomain[1], undefined, "[]");
|
||||
}
|
||||
|
||||
function getXDomainForTimelines(xDomain, dataInterval) {
|
||||
function getXDomainForTimelines(
|
||||
xDomain: Domain | null,
|
||||
dataInterval: TimeSeriesInterval | null,
|
||||
): Domain | null {
|
||||
// When looking at, let's say, count of orders over years, last year value is Jan 1, 2024
|
||||
// If we filter timeline events up until Jan 1, 2024, we won't see any events from 2024,
|
||||
// so we need to extend xDomain by dataInterval.count * dataInterval.unit to include them
|
||||
@@ -837,15 +894,17 @@ export const getFilteredTimelines = createSelector(
|
||||
const timelineXDomain = getXDomainForTimelines(xDomain, dataInterval);
|
||||
return timelines
|
||||
.map((timeline) =>
|
||||
updateIn(timeline, ["events"], (events) =>
|
||||
updateIn(timeline, ["events"], (events: TimelineEvent[]) =>
|
||||
xDomain
|
||||
? events.filter((event) =>
|
||||
isEventWithinDomain(event, timelineXDomain),
|
||||
? events.filter(
|
||||
(event) =>
|
||||
timelineXDomain &&
|
||||
isEventWithinDomain(event, timelineXDomain),
|
||||
)
|
||||
: events,
|
||||
),
|
||||
)
|
||||
.filter((timeline) => timeline.events.length > 0);
|
||||
.filter((timeline) => (timeline.events ?? []).length > 0);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -861,7 +920,10 @@ export const getVisibleTimelineEvents = createSelector(
|
||||
.value(),
|
||||
);
|
||||
|
||||
export function getOffsetForQueryAndPosition(queryText, { row, column }) {
|
||||
export function getOffsetForQueryAndPosition(
|
||||
queryText: string,
|
||||
{ row, column }: { row: number; column: number },
|
||||
) {
|
||||
const queryLines = queryText.split("\n");
|
||||
return (
|
||||
// the total length of the previous rows
|
||||
@@ -926,12 +988,12 @@ export const getAllNativeEditorSelectedText = createSelector(
|
||||
|
||||
export const getModalSnippet = createSelector(
|
||||
[getUiControls],
|
||||
(uiControls) => uiControls && uiControls.modalSnippet,
|
||||
(uiControls) => uiControls?.modalSnippet,
|
||||
);
|
||||
|
||||
export const getSnippetCollectionId = createSelector(
|
||||
[getUiControls],
|
||||
(uiControls) => uiControls && uiControls.snippetCollectionId,
|
||||
(uiControls) => uiControls?.snippetCollectionId,
|
||||
);
|
||||
|
||||
export const getIsVisualized = createSelector(
|
||||
@@ -966,7 +1028,7 @@ export const getIsLiveResizable = createSelector(
|
||||
|
||||
export const getQuestionDetailsTimelineDrawerState = createSelector(
|
||||
[getUiControls],
|
||||
(uiControls) => uiControls && uiControls.questionDetailsTimelineDrawerState,
|
||||
(uiControls) => uiControls?.questionDetailsTimelineDrawerState,
|
||||
);
|
||||
|
||||
export const isBasedOnExistingQuestion = createSelector(
|
||||
@@ -1015,21 +1077,18 @@ export const getIsAdditionalInfoVisible = createSelector(
|
||||
export const getDataReferenceStack = createSelector(
|
||||
[getUiControls, getDatabaseId],
|
||||
(uiControls, dbId) =>
|
||||
uiControls.dataReferenceStack
|
||||
? uiControls.dataReferenceStack
|
||||
: dbId
|
||||
? [{ type: "database", item: { id: dbId } }]
|
||||
: [],
|
||||
uiControls.dataReferenceStack ??
|
||||
(dbId ? [{ type: "database", id: dbId }] : []),
|
||||
);
|
||||
|
||||
export const getIsEditingInDashboard = (state) => {
|
||||
export const getIsEditingInDashboard = (state: State) => {
|
||||
return (
|
||||
state.qb.parentEntity.model === "dashboard" &&
|
||||
state.qb.parentEntity.isEditing
|
||||
);
|
||||
};
|
||||
|
||||
export const getParentEntity = (state) => {
|
||||
export const getParentEntity = (state: State) => {
|
||||
return state.qb.parentEntity;
|
||||
};
|
||||
|
||||
@@ -1044,7 +1103,7 @@ export const getEmbeddingParameters = createSelector([getCard], (card) => {
|
||||
// Embeddings might be published without passing embedding_params to the server,
|
||||
// in which case it's an empty object. We should treat such situations with
|
||||
// caution, assuming that an absent parameter is "disabled".
|
||||
export function getEmbeddedParameterVisibility(state, slug) {
|
||||
export function getEmbeddedParameterVisibility(state: State, slug: string) {
|
||||
const card = getCard(state);
|
||||
if (!card?.enable_embedding) {
|
||||
return null;
|
||||
@@ -1054,9 +1113,10 @@ export function getEmbeddedParameterVisibility(state, slug) {
|
||||
return embeddingParams[slug] ?? "disabled";
|
||||
}
|
||||
|
||||
export const getSubmittableQuestion = (state, question) => {
|
||||
export const getSubmittableQuestion = (state: State, question: Question) => {
|
||||
const card = getCard(state) ?? question.card();
|
||||
const rawSeries = createRawSeries({
|
||||
card: getCard(state),
|
||||
card,
|
||||
queryResult: getFirstQueryResult(state),
|
||||
datasetQuery: getLastRunDatasetQuery(state),
|
||||
});
|
||||
@@ -1089,7 +1149,7 @@ export const getSubmittableQuestion = (state, question) => {
|
||||
return submittableQuestion;
|
||||
};
|
||||
|
||||
export const getNotebookNativePreviewSidebarWidth = (state) =>
|
||||
export const getNotebookNativePreviewSidebarWidth = (state: State) =>
|
||||
getSetting(state, "notebook-native-preview-sidebar-width");
|
||||
|
||||
export const getIsListViewConfigurationShown = createSelector(
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import registerVisualizations from "metabase/visualizations/register";
|
||||
import * as Lib from "metabase-lib";
|
||||
import Question from "metabase-lib/v1/Question";
|
||||
import type { Card, StructuredDatasetQuery } from "metabase-types/api";
|
||||
import {
|
||||
createMockCard,
|
||||
createMockColumn,
|
||||
@@ -31,6 +32,10 @@ import {
|
||||
SAMPLE_DB_ID,
|
||||
createSampleDatabase,
|
||||
} from "metabase-types/api/mocks/presets";
|
||||
import type {
|
||||
QueryBuilderState,
|
||||
QueryBuilderUIControls,
|
||||
} from "metabase-types/store";
|
||||
import {
|
||||
createMockQueryBuilderState,
|
||||
createMockQueryBuilderUIControlsState,
|
||||
@@ -39,7 +44,14 @@ import {
|
||||
|
||||
registerVisualizations();
|
||||
|
||||
function getBaseState({ uiControls = {}, ...state } = {}) {
|
||||
type BaseQueryBuilderState = Omit<Partial<QueryBuilderState>, "uiControls"> & {
|
||||
uiControls?: Partial<QueryBuilderUIControls>;
|
||||
};
|
||||
|
||||
function getBaseState({
|
||||
uiControls = {},
|
||||
...state
|
||||
}: BaseQueryBuilderState = {}) {
|
||||
return createMockState({
|
||||
entities: createMockEntitiesState({
|
||||
databases: [createSampleDatabase()],
|
||||
@@ -55,7 +67,11 @@ function getBaseState({ uiControls = {}, ...state } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function getBaseCard(opts) {
|
||||
type BaseCardOpts = Omit<Partial<Card>, "dataset_query"> & {
|
||||
dataset_query?: Partial<StructuredDatasetQuery>;
|
||||
};
|
||||
|
||||
function getBaseCard(opts: BaseCardOpts) {
|
||||
return {
|
||||
...opts,
|
||||
dataset_query: {
|
||||
@@ -72,7 +88,7 @@ describe("getQuestion", () => {
|
||||
});
|
||||
|
||||
it("should return question instance correctly", () => {
|
||||
const card = {
|
||||
const card = createMockCard({
|
||||
id: 5,
|
||||
dataset_query: {
|
||||
database: 1,
|
||||
@@ -81,16 +97,16 @@ describe("getQuestion", () => {
|
||||
"source-table": 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const question = getQuestion(getBaseState({ card }));
|
||||
|
||||
expect(question).toBeInstanceOf(Question);
|
||||
expect(question._doNotCallSerializableCard()).toEqual(card);
|
||||
expect(question?._doNotCallSerializableCard()).toEqual(card);
|
||||
});
|
||||
|
||||
it("should return composed dataset when dataset is open", () => {
|
||||
const card = {
|
||||
const card = createMockCard({
|
||||
id: 1,
|
||||
type: "model",
|
||||
dataset_query: {
|
||||
@@ -100,11 +116,13 @@ describe("getQuestion", () => {
|
||||
"source-table": ORDERS_ID,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const question = getQuestion(getBaseState({ card }));
|
||||
|
||||
expect(Lib.sourceTableOrCardId(question.query())).toBe("card__1");
|
||||
expect(question && Lib.sourceTableOrCardId(question.query())).toBe(
|
||||
"card__1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return real dataset when dataset is open in 'dataset' QB mode", () => {
|
||||
@@ -351,7 +369,7 @@ describe("getIsResultDirty", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function getState(state) {
|
||||
function getState(state: Partial<QueryBuilderState>) {
|
||||
return getBaseState(state);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { createSelector } from "@reduxjs/toolkit";
|
||||
import { getMetadata } from "metabase/selectors/metadata";
|
||||
import { getMode as getQuestionMode } from "metabase/visualizations/click-actions/lib/modes";
|
||||
import Question from "metabase-lib/v1/Question";
|
||||
import type { State } from "metabase-types/store";
|
||||
|
||||
const getLastRunCard = (state) => state.qb.lastRunCard;
|
||||
const getParameterValues = (state) => state.qb.parameterValues;
|
||||
const getZoomedObjectId = (state) => state.qb.zoomedRowObjectId;
|
||||
const getLastRunCard = (state: State) => state.qb.lastRunCard;
|
||||
const getParameterValues = (state: State) => state.qb.parameterValues;
|
||||
const getZoomedObjectId = (state: State) => state.qb.zoomedRowObjectId;
|
||||
|
||||
const getLastRunQuestion = createSelector(
|
||||
[getMetadata, getLastRunCard, getParameterValues],
|
||||
@@ -39,14 +39,14 @@ export function syncVizSettingsWithSeries(
|
||||
settings: VisualizationSettings,
|
||||
newQuery: Lib.Query,
|
||||
newSeries: Series,
|
||||
oldSeries: Series,
|
||||
oldSeries?: Series,
|
||||
): VisualizationSettings {
|
||||
if (!isNativeQuery(newQuery)) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
const [newSingleSeries] = newSeries;
|
||||
const [oldSingleSeries] = oldSeries;
|
||||
const [oldSingleSeries] = oldSeries ?? [];
|
||||
if (!isValidSeries(newSingleSeries) || !isValidSeries(oldSingleSeries)) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isMac } from "metabase/lib/browser";
|
||||
import { useDispatch, useSelector } from "metabase/lib/redux";
|
||||
import { NativeQueryEditor } from "metabase/query_builder/components/NativeQueryEditor";
|
||||
import { DataReference } from "metabase/query_builder/components/dataref/DataReference";
|
||||
import type { DataReferenceItem } from "metabase/query_builder/components/dataref/types";
|
||||
import { createRawSeries } from "metabase/query_builder/utils";
|
||||
import { getMetadata } from "metabase/selectors/metadata";
|
||||
import { Box, Button, Flex, Loader, Modal, Stack, Text } from "metabase/ui";
|
||||
@@ -26,11 +27,6 @@ import type { Card, DatabaseId, Dataset, RawSeries } from "metabase-types/api";
|
||||
|
||||
import S from "./NativeQueryModal.module.css";
|
||||
|
||||
type DataReferenceStackItem = {
|
||||
type: string;
|
||||
item: unknown;
|
||||
};
|
||||
|
||||
interface NativeQueryModalProps {
|
||||
card: Card;
|
||||
isOpen: boolean;
|
||||
@@ -124,7 +120,7 @@ export const NativeQueryModal = ({
|
||||
useState(false);
|
||||
const [isShowingDataReference, setIsShowingDataReference] = useState(false);
|
||||
const [dataReferenceStack, setDataReferenceStack] = useState<
|
||||
DataReferenceStackItem[]
|
||||
DataReferenceItem[]
|
||||
>([]);
|
||||
|
||||
const [
|
||||
@@ -357,7 +353,7 @@ export const NativeQueryModal = ({
|
||||
const databaseId = modifiedQuestion.databaseId();
|
||||
if (databaseId) {
|
||||
setDataReferenceStack([
|
||||
{ type: "database", item: { id: databaseId } },
|
||||
{ type: "database", id: databaseId },
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -442,7 +438,7 @@ export const NativeQueryModal = ({
|
||||
setDataReferenceStack(dataReferenceStack.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
pushDataReferenceStack={(item: DataReferenceStackItem) => {
|
||||
pushDataReferenceStack={(item: DataReferenceItem) => {
|
||||
setDataReferenceStack([...dataReferenceStack, item]);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
Card,
|
||||
DatasetData,
|
||||
QuestionDashboardCard,
|
||||
RowValue,
|
||||
RowValues,
|
||||
} from "metabase-types/api";
|
||||
|
||||
export type ObjectId = number | string;
|
||||
@@ -31,7 +31,7 @@ export interface ObjectDetailProps {
|
||||
dashcard?: QuestionDashboardCard;
|
||||
isObjectDetail?: boolean; // whether this should be shown in a modal
|
||||
table?: Table | null;
|
||||
zoomedRow?: RowValue[] | undefined;
|
||||
zoomedRow?: RowValues | undefined;
|
||||
zoomedRowID?: ObjectId;
|
||||
tableForeignKeys?: ForeignKey[];
|
||||
tableForeignKeyReferences?: {
|
||||
|
||||
@@ -121,7 +121,7 @@ export const getIdValue = ({
|
||||
};
|
||||
|
||||
export function getSingleResultsRow(data: DatasetData) {
|
||||
return data.rows.length === 1 ? data.rows[0] : null;
|
||||
return data.rows.length === 1 ? data.rows[0] : undefined;
|
||||
}
|
||||
|
||||
export const getSinglePKIndex = (cols: DatasetColumn[]) => {
|
||||
|
||||
@@ -82,6 +82,7 @@ import type {
|
||||
import type { Dispatch, State } from "metabase-types/store";
|
||||
|
||||
import { EmptyVizState } from "../EmptyVizState";
|
||||
import type { ObjectId } from "../ObjectDetail/types";
|
||||
|
||||
import ChartSettingsErrorButton from "./ChartSettingsErrorButton";
|
||||
import { ErrorView } from "./ErrorView";
|
||||
@@ -116,7 +117,7 @@ type ForwardedRefProps = {
|
||||
type OnChangeCardAndRunOpts = {
|
||||
nextCard: Card;
|
||||
previousCard: Card;
|
||||
objectId?: number;
|
||||
objectId: ObjectId;
|
||||
};
|
||||
|
||||
type VisualizationOwnProps = {
|
||||
@@ -212,7 +213,7 @@ const mapStateToProps = (state: State): StateProps => ({
|
||||
fontFamily: getFont(state),
|
||||
isRawTable: getIsShowingRawTable(state),
|
||||
isEmbeddingSdk: isEmbeddingSdk(),
|
||||
scrollToLastColumn: getUiControls(state)?.scrollToLastColumn,
|
||||
scrollToLastColumn: getUiControls(state)?.scrollToLastColumn ?? false,
|
||||
isDownloadingToImage: getIsDownloadingToImage(state),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user