diff --git a/enterprise/frontend/src/metabase-enterprise/sandboxes/containers/QuestionParameterTargetWidget.jsx b/enterprise/frontend/src/metabase-enterprise/sandboxes/containers/QuestionParameterTargetWidget.jsx index 248b73adba643b56c105470b01e82309e237c63e..43f80867fafd5b9109c146e71e451e7b107d45e7 100644 --- a/enterprise/frontend/src/metabase-enterprise/sandboxes/containers/QuestionParameterTargetWidget.jsx +++ b/enterprise/frontend/src/metabase-enterprise/sandboxes/containers/QuestionParameterTargetWidget.jsx @@ -4,7 +4,7 @@ import React from "react"; import ParameterTargetWidget from "metabase/parameters/components/ParameterTargetWidget"; import { QuestionLoaderHOC } from "metabase/containers/QuestionLoader"; -import * as Dashboard from "metabase/meta/Dashboard"; +import { getParameterMappingOptions } from "metabase/parameters/utils/mapping-options"; import type { ParameterTarget } from "metabase-types/types/Parameter"; @@ -23,11 +23,7 @@ export default class QuestionParameterTargetWidget extends React.Component { render() { const { question, ...props } = this.props; const mappingOptions = question - ? Dashboard.getParameterMappingOptions( - question.metadata(), - null, - question.card(), - ) + ? getParameterMappingOptions(question.metadata(), null, question.card()) : []; return <ParameterTargetWidget {...props} mappingOptions={mappingOptions} />; } diff --git a/frontend/src/metabase/dashboard/selectors.js b/frontend/src/metabase/dashboard/selectors.js index 7b114a1a6950df1b65ade9f80370244fe271be35..37faea69b1abb39876691f8f021f542ab611870c 100644 --- a/frontend/src/metabase/dashboard/selectors.js +++ b/frontend/src/metabase/dashboard/selectors.js @@ -5,10 +5,10 @@ import { createSelector } from "reselect"; import { getMetadata } from "metabase/selectors/metadata"; import { - getParameterMappingOptions as _getParameterMappingOptions, getMappingsByParameter as _getMappingsByParameter, getDashboardParametersWithFieldMetadata, } from "metabase/meta/Dashboard"; +import { getParameterMappingOptions as _getParameterMappingOptions } from "metabase/parameters/utils/mapping-options"; import { SIDEBAR_NAME } from "metabase/dashboard/constants"; diff --git a/frontend/src/metabase/meta/Dashboard.js b/frontend/src/metabase/meta/Dashboard.js index 03eb252e49d6245e91961032d1dc0ec6cf7b36bf..e895edcc2d32c26755d4db4c648a4385393a4575 100644 --- a/frontend/src/metabase/meta/Dashboard.js +++ b/frontend/src/metabase/meta/Dashboard.js @@ -3,23 +3,11 @@ import { setIn } from "icepick"; import Question from "metabase-lib/lib/Question"; -import { ExpressionDimension } from "metabase-lib/lib/Dimension"; - -import type Metadata from "metabase-lib/lib/metadata/Metadata"; -import type { Card } from "metabase-types/types/Card"; import type { ParameterOption, Parameter, - ParameterMappingUIOption, } from "metabase-types/types/Parameter"; - import { getParameterTargetField } from "metabase/meta/Parameter"; -import { - dimensionFilterForParameter, - getTagOperatorFilterForParameter, - variableFilterForParameter, -} from "metabase/parameters/utils/filters"; - import { slugify } from "metabase/lib/formatting"; export type ParameterSection = { @@ -29,74 +17,6 @@ export type ParameterSection = { options: ParameterOption[], }; -export function getParameterMappingOptions( - metadata: Metadata, - parameter: ?Parameter = null, - card: Card, -): ParameterMappingUIOption[] { - const options = []; - if (card.display === "text") { - // text cards don't have parameters - return []; - } - - const question = new Question(card, metadata); - const query = question.query(); - - if (question.isStructured()) { - options.push( - ...query - .dimensionOptions( - parameter ? dimensionFilterForParameter(parameter) : undefined, - ) - .sections() - .flatMap(section => - section.items.map(({ dimension }) => ({ - sectionName: section.name, - name: dimension.displayName(), - icon: dimension.icon(), - target: ["dimension", dimension.mbql()], - // these methods don't exist on instances of ExpressionDimension - isForeign: !!(dimension instanceof ExpressionDimension - ? false - : dimension.fk() || dimension.joinAlias()), - })), - ), - ); - } else { - options.push( - ...query - .variables( - parameter ? variableFilterForParameter(parameter) : undefined, - ) - .map(variable => ({ - name: variable.displayName(), - icon: variable.icon(), - isForeign: false, - target: ["variable", variable.mbql()], - })), - ); - options.push( - ...query - .dimensionOptions( - parameter ? dimensionFilterForParameter(parameter) : undefined, - parameter ? getTagOperatorFilterForParameter(parameter) : undefined, - ) - .sections() - .flatMap(section => - section.items.map(({ dimension }) => ({ - name: dimension.displayName(), - icon: dimension.icon(), - isForeign: false, - target: ["dimension", dimension.mbql()], - })), - ), - ); - } - - return options; -} - export function createParameter( option: ParameterOption, parameters: Parameter[] = [], diff --git a/frontend/src/metabase/parameters/utils/mapping-options.js b/frontend/src/metabase/parameters/utils/mapping-options.js new file mode 100644 index 0000000000000000000000000000000000000000..72d19dfb7025758a24bb2dca33496cef1d5a4999 --- /dev/null +++ b/frontend/src/metabase/parameters/utils/mapping-options.js @@ -0,0 +1,81 @@ +import Question from "metabase-lib/lib/Question"; + +import { ExpressionDimension } from "metabase-lib/lib/Dimension"; + +import { + dimensionFilterForParameter, + getTagOperatorFilterForParameter, + variableFilterForParameter, +} from "./filters"; + +function buildStructuredQuerySectionOptions(section) { + return section.items.map(({ dimension }) => ({ + sectionName: section.name, + name: dimension.displayName(), + icon: dimension.icon(), + target: ["dimension", dimension.mbql()], + // these methods don't exist on instances of ExpressionDimension + isForeign: !!(dimension instanceof ExpressionDimension + ? false + : dimension.fk() || dimension.joinAlias()), + })); +} + +function buildNativeQuerySectionOptions(section) { + return section.items.map(({ dimension }) => ({ + name: dimension.displayName(), + icon: dimension.icon(), + isForeign: false, + target: ["dimension", dimension.mbql()], + })); +} + +function buildVariableOption(variable) { + return { + name: variable.displayName(), + icon: variable.icon(), + isForeign: false, + target: ["variable", variable.mbql()], + }; +} + +export function getParameterMappingOptions(metadata, parameter = null, card) { + const options = []; + if (card.display === "text") { + // text cards don't have parameters + return []; + } + + const question = new Question(card, metadata); + const query = question.query(); + + if (question.isStructured()) { + options.push( + ...query + .dimensionOptions( + parameter ? dimensionFilterForParameter(parameter) : undefined, + ) + .sections() + .flatMap(section => buildStructuredQuerySectionOptions(section)), + ); + } else { + options.push( + ...query + .variables( + parameter ? variableFilterForParameter(parameter) : undefined, + ) + .map(buildVariableOption), + ); + options.push( + ...query + .dimensionOptions( + parameter ? dimensionFilterForParameter(parameter) : undefined, + parameter ? getTagOperatorFilterForParameter(parameter) : undefined, + ) + .sections() + .flatMap(section => buildNativeQuerySectionOptions(section)), + ); + } + + return options; +} diff --git a/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js b/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..00910e13197a6a46aaa14d2e0b9f3d4c999871e4 --- /dev/null +++ b/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js @@ -0,0 +1,170 @@ +import { + metadata, + SAMPLE_DATASET, + REVIEWS, + ORDERS, + PRODUCTS, +} from "__support__/sample_dataset_fixture"; + +import { getParameterMappingOptions } from "./mapping-options"; + +function structured(query) { + return SAMPLE_DATASET.question(query).card(); +} + +function native(native) { + return SAMPLE_DATASET.nativeQuestion(native).card(); +} + +describe("parameters/utils/mapping-options", () => { + describe("getParameterMappingOptions", () => { + describe("Structured Query", () => { + it("should return field-id and fk-> dimensions", () => { + const options = getParameterMappingOptions( + metadata, + { type: "date/single" }, + structured({ + "source-table": REVIEWS.id, + }), + ); + expect(options).toEqual([ + { + sectionName: "Review", + icon: "calendar", + name: "Created At", + target: ["dimension", ["field", REVIEWS.CREATED_AT.id, null]], + isForeign: false, + }, + { + sectionName: "Product", + name: "Created At", + icon: "calendar", + target: [ + "dimension", + [ + "field", + PRODUCTS.CREATED_AT.id, + { "source-field": REVIEWS.PRODUCT_ID.id }, + ], + ], + isForeign: true, + }, + ]); + }); + it("should also return fields from explicitly joined tables", () => { + const options = getParameterMappingOptions( + metadata, + { type: "date/single" }, + structured({ + "source-table": REVIEWS.id, + joins: [ + { + alias: "Joined Table", + "source-table": ORDERS.id, + }, + ], + }), + ); + expect(options).toEqual([ + { + sectionName: "Review", + name: "Created At", + icon: "calendar", + target: ["dimension", ["field", 30, null]], + isForeign: false, + }, + { + sectionName: "Joined Table", + name: "Created At", + icon: "calendar", + target: [ + "dimension", + ["field", 1, { "join-alias": "Joined Table" }], + ], + isForeign: true, + }, + { + sectionName: "Product", + name: "Created At", + icon: "calendar", + target: ["dimension", ["field", 22, { "source-field": 32 }]], + isForeign: true, + }, + ]); + }); + it("should return fields in nested query", () => { + const options = getParameterMappingOptions( + metadata, + { type: "date/single" }, + structured({ + "source-query": { + "source-table": ORDERS.id, + }, + }), + ); + expect(options).toEqual([ + { + sectionName: null, + name: "Created At", + icon: "calendar", + target: [ + "dimension", + ["field", "CREATED_AT", { "base-type": "type/DateTime" }], + ], + isForeign: false, + }, + ]); + }); + }); + + describe("NativeQuery", () => { + it("should return variables for non-dimension template-tags", () => { + const options = getParameterMappingOptions( + metadata, + { type: "date/single" }, + native({ + query: "select * from ORDERS where CREATED_AT = {{created}}", + "template-tags": { + created: { + type: "date", + name: "created", + }, + }, + }), + ); + expect(options).toEqual([ + { + name: "created", + icon: "calendar", + target: ["variable", ["template-tag", "created"]], + isForeign: false, + }, + ]); + }); + }); + it("should return dimensions for dimension template-tags", () => { + const options = getParameterMappingOptions( + metadata, + { type: "date/single" }, + native({ + query: "select * from ORDERS where CREATED_AT = {{created}}", + "template-tags": { + created: { + type: "dimension", + name: "created", + dimension: ["field", ORDERS.CREATED_AT.id, null], + }, + }, + }), + ); + expect(options).toEqual([ + { + name: "Created At", + icon: "calendar", + target: ["dimension", ["template-tag", "created"]], + isForeign: false, + }, + ]); + }); + }); +}); diff --git a/frontend/test/metabase/meta/Dashboard.unit.spec.js b/frontend/test/metabase/meta/Dashboard.unit.spec.js index f7f34396526ddbaabe38e55e90bf117b74b29ba4..057ee4cbe5c9fc7b8bd7b3d895ff07ec555e5a1d 100644 --- a/frontend/test/metabase/meta/Dashboard.unit.spec.js +++ b/frontend/test/metabase/meta/Dashboard.unit.spec.js @@ -1,15 +1,7 @@ -import { - metadata, - SAMPLE_DATASET, - REVIEWS, - ORDERS, - PRODUCTS, -} from "__support__/sample_dataset_fixture"; import { createParameter, setParameterName, setParameterDefaultValue, - getParameterMappingOptions, hasMapping, isDashboardParameterWithoutMapping, getMappingsByParameter, @@ -18,14 +10,6 @@ import DASHBOARD_WITH_BOOLEAN_PARAMETER from "./dashboard-with-boolean-parameter import Field from "metabase-lib/lib/metadata/Field"; -function structured(query) { - return SAMPLE_DATASET.question(query).card(); -} - -function native(native) { - return SAMPLE_DATASET.nativeQuestion(native).card(); -} - describe("meta/Dashboard", () => { describe("createParameter", () => { it("should create a new parameter using the given parameter option", () => { @@ -122,157 +106,6 @@ describe("meta/Dashboard", () => { }); }); - describe("getParameterMappingOptions", () => { - describe("Structured Query", () => { - it("should return field-id and fk-> dimensions", () => { - const options = getParameterMappingOptions( - metadata, - { type: "date/single" }, - structured({ - "source-table": REVIEWS.id, - }), - ); - expect(options).toEqual([ - { - sectionName: "Review", - icon: "calendar", - name: "Created At", - target: ["dimension", ["field", REVIEWS.CREATED_AT.id, null]], - isForeign: false, - }, - { - sectionName: "Product", - name: "Created At", - icon: "calendar", - target: [ - "dimension", - [ - "field", - PRODUCTS.CREATED_AT.id, - { "source-field": REVIEWS.PRODUCT_ID.id }, - ], - ], - isForeign: true, - }, - ]); - }); - it("should also return fields from explicitly joined tables", () => { - const options = getParameterMappingOptions( - metadata, - { type: "date/single" }, - structured({ - "source-table": REVIEWS.id, - joins: [ - { - alias: "Joined Table", - "source-table": ORDERS.id, - }, - ], - }), - ); - expect(options).toEqual([ - { - sectionName: "Review", - name: "Created At", - icon: "calendar", - target: ["dimension", ["field", 30, null]], - isForeign: false, - }, - { - sectionName: "Joined Table", - name: "Created At", - icon: "calendar", - target: [ - "dimension", - ["field", 1, { "join-alias": "Joined Table" }], - ], - isForeign: true, - }, - { - sectionName: "Product", - name: "Created At", - icon: "calendar", - target: ["dimension", ["field", 22, { "source-field": 32 }]], - isForeign: true, - }, - ]); - }); - it("should return fields in nested query", () => { - const options = getParameterMappingOptions( - metadata, - { type: "date/single" }, - structured({ - "source-query": { - "source-table": ORDERS.id, - }, - }), - ); - expect(options).toEqual([ - { - sectionName: null, - name: "Created At", - icon: "calendar", - target: [ - "dimension", - ["field", "CREATED_AT", { "base-type": "type/DateTime" }], - ], - isForeign: false, - }, - ]); - }); - }); - - describe("NativeQuery", () => { - it("should return variables for non-dimension template-tags", () => { - const options = getParameterMappingOptions( - metadata, - { type: "date/single" }, - native({ - query: "select * from ORDERS where CREATED_AT = {{created}}", - "template-tags": { - created: { - type: "date", - name: "created", - }, - }, - }), - ); - expect(options).toEqual([ - { - name: "created", - icon: "calendar", - target: ["variable", ["template-tag", "created"]], - isForeign: false, - }, - ]); - }); - }); - it("should return dimensions for dimension template-tags", () => { - const options = getParameterMappingOptions( - metadata, - { type: "date/single" }, - native({ - query: "select * from ORDERS where CREATED_AT = {{created}}", - "template-tags": { - created: { - type: "dimension", - name: "created", - dimension: ["field", ORDERS.CREATED_AT.id, null], - }, - }, - }), - ); - expect(options).toEqual([ - { - name: "Created At", - icon: "calendar", - target: ["dimension", ["template-tag", "created"]], - isForeign: false, - }, - ]); - }); - }); - describe("hasMapping", () => { const parameter = { id: "foo" };