Skip to content
Snippets Groups Projects
Unverified Commit af395108 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Revert "Add values source settings to template tags (#27713)" (#27764)

parent 9f94b7a6
No related branches found
No related tags found
No related merge requests found
Showing
with 174 additions and 243 deletions
......@@ -148,6 +148,11 @@ export const LOCATION_OPTIONS = [
},
];
export const CUSTOM_SOURCE_PARAMETER_TYPES: Record<string, string[]> = {
string: ["="],
location: ["="],
};
export const TYPE_SUPPORTS_LINKED_FILTERS = [
"string",
"category",
......
......@@ -6,9 +6,8 @@ import {
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import { getFields } from "metabase-lib/parameters/utils/parameter-fields";
import Field from "metabase-lib/metadata/Field";
import { getFields } from "./parameter-fields";
import { getParameterSubType, getParameterType } from "./parameter-type";
export const getQueryType = (parameter: Parameter): ValuesQueryType => {
return parameter.values_query_type ?? "list";
......@@ -22,21 +21,6 @@ export const getSourceConfig = (parameter: Parameter): ValuesSourceConfig => {
return parameter.values_source_config ?? {};
};
export const canUseCustomSource = (parameter: Parameter) => {
const type = getParameterType(parameter);
const subType = getParameterSubType(parameter);
switch (type) {
case "string":
case "location":
return subType === "=";
case "category":
return true;
default:
return false;
}
};
export const isValidSourceConfig = (
sourceType: ValuesSourceType,
{ card_id, value_field, values }: ValuesSourceConfig,
......
......@@ -38,9 +38,6 @@ export function getTemplateTagParameter(tag: TemplateTag): ParameterWithTarget {
name: tag["display-name"],
slug: tag.name,
default: tag.default,
values_query_type: tag.values_query_type,
values_source_type: tag.values_source_type,
values_source_config: tag.values_source_config,
};
}
......
......@@ -3,11 +3,6 @@
* @deprecated use existing types from, or add to metabase-types/api/*
*/
import {
ValuesQueryType,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import { DatetimeUnit } from "metabase-types/api/query";
import { TableId } from "./Table";
import { FieldId, BaseType } from "./Field";
......@@ -65,11 +60,6 @@ export type TemplateTag = {
// Snippet specific
"snippet-id"?: number;
"snippet-name"?: string;
// Values source
values_query_type?: ValuesQueryType;
values_source_type?: ValuesSourceType;
values_source_config?: ValuesSourceConfig;
};
export type TemplateTags = { [key: TemplateTagName]: TemplateTag };
......
......@@ -4,13 +4,8 @@ import Toggle from "metabase/core/components/Toggle";
import Fields from "metabase/entities/fields";
import Tables from "metabase/entities/tables";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import {
Field,
FieldId,
Parameter,
ParameterId,
Table,
} from "metabase-types/api";
import { Field, FieldId, ParameterId, Table } from "metabase-types/api";
import { UiParameter } from "metabase-lib/parameters/types";
import { usableAsLinkedFilter } from "../../utils/linked-filters";
import useFilterFields from "./use-filter-fields";
import {
......@@ -30,8 +25,8 @@ import {
} from "./ParameterLinkedFilters.styled";
export interface ParameterLinkedFiltersProps {
parameter: Parameter;
otherParameters: Parameter[];
parameter: UiParameter;
otherParameters: UiParameter[];
onChangeFilteringParameters: (filteringParameters: ParameterId[]) => void;
onShowAddParameterPopover: () => void;
}
......@@ -55,7 +50,7 @@ const ParameterLinkedFilters = ({
);
const handleFilterChange = useCallback(
(otherParameter: Parameter, isFiltered: boolean) => {
(otherParameter: UiParameter, isFiltered: boolean) => {
const newParameters = isFiltered
? filteringParameters.concat(otherParameter.id)
: filteringParameters.filter(id => id !== otherParameter.id);
......@@ -66,7 +61,7 @@ const ParameterLinkedFilters = ({
);
const handleExpandedChange = useCallback(
(otherParameter: Parameter, isExpanded: boolean) => {
(otherParameter: UiParameter, isExpanded: boolean) => {
setExpandedParameterId(isExpanded ? otherParameter.id : undefined);
},
[],
......@@ -116,12 +111,12 @@ const ParameterLinkedFilters = ({
};
interface LinkedParameterProps {
parameter: Parameter;
otherParameter: Parameter;
parameter: UiParameter;
otherParameter: UiParameter;
isFiltered: boolean;
isExpanded: boolean;
onFilterChange: (otherParameter: Parameter, isFiltered: boolean) => void;
onExpandedChange: (otherParameter: Parameter, isExpanded: boolean) => void;
onFilterChange: (otherParameter: UiParameter, isFiltered: boolean) => void;
onExpandedChange: (otherParameter: UiParameter, isExpanded: boolean) => void;
}
const LinkedParameter = ({
......@@ -162,8 +157,8 @@ const LinkedParameter = ({
};
interface LinkedFieldListProps {
parameter: Parameter;
otherParameter: Parameter;
parameter: UiParameter;
otherParameter: UiParameter;
}
const LinkedFieldList = ({
......
......@@ -2,8 +2,8 @@ import { useCallback, useState } from "react";
import { t } from "ttag";
import { DashboardApi } from "metabase/services";
import { useOnMount } from "metabase/hooks/use-on-mount";
import { FieldId, Parameter } from "metabase-types/api";
import { getFields } from "metabase-lib/parameters/utils/parameter-fields";
import { FieldId } from "metabase-types/api";
import { UiParameter } from "metabase-lib/parameters/types";
export interface UseFilterFieldsState {
data?: FieldId[][];
......@@ -12,14 +12,14 @@ export interface UseFilterFieldsState {
}
const useFilterFields = (
parameter: Parameter,
otherParameter: Parameter,
parameter: UiParameter,
otherParameter: UiParameter,
): UseFilterFieldsState => {
const [state, setState] = useState<UseFilterFieldsState>({ loading: false });
const handleLoad = useCallback(async () => {
const filtered = getFields(parameter).map(field => field.id);
const filtering = getFields(otherParameter).map(field => field.id);
const filtered = getParameterFieldIds(parameter);
const filtering = getParameterFieldIds(otherParameter);
if (!filtered.length || !filtered.length) {
const errorParameter = !filtered.length ? parameter : otherParameter;
......@@ -40,10 +40,18 @@ const useFilterFields = (
return state;
};
const getParameterError = ({ name }: Parameter) => {
const getParameterError = ({ name }: UiParameter) => {
return t`To view this, ${name} must be connected to at least one field.`;
};
const getParameterFieldIds = (parameter: UiParameter) => {
if ("fields" in parameter) {
return parameter.fields.map(field => field.id);
} else {
return [];
}
};
const getParameterMapping = (data: Record<FieldId, FieldId[]>) => {
return Object.entries(data).flatMap(([filteredId, filteringIds]) =>
filteringIds.map(filteringId => [filteringId, parseInt(filteredId, 10)]),
......
......@@ -3,14 +3,16 @@ import { t } from "ttag";
import InputBlurChange from "metabase/components/InputBlurChange";
import Radio from "metabase/core/components/Radio";
import {
Parameter,
ValuesQueryType,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import { canUseCustomSource } from "metabase-lib/parameters/utils/parameter-source";
import { UiParameter } from "metabase-lib/parameters/types";
import { getIsMultiSelect } from "../../utils/dashboards";
import { isSingleOrMultiSelectable } from "../../utils/parameter-type";
import {
canUseCustomSource,
isSingleOrMultiSelectable,
} from "../../utils/parameter-type";
import ValuesSourceSettings from "../ValuesSourceSettings";
import {
SettingLabel,
......@@ -26,7 +28,7 @@ const MULTI_SELECT_OPTIONS = [
];
export interface ParameterSettingsProps {
parameter: Parameter;
parameter: UiParameter;
onChangeName: (name: string) => void;
onChangeDefaultValue: (value: unknown) => void;
onChangeIsMultiSelect: (isMultiSelect: boolean) => void;
......
......@@ -3,20 +3,20 @@ import { t } from "ttag";
import Radio from "metabase/core/components/Radio";
import Sidebar from "metabase/dashboard/components/Sidebar";
import {
Parameter,
ParameterId,
ValuesQueryType,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import { UiParameter } from "metabase-lib/parameters/types";
import { canUseLinkedFilters } from "../../utils/linked-filters";
import ParameterSettings from "../ParameterSettings";
import ParameterLinkedFilters from "../ParameterLinkedFilters";
import { SidebarBody, SidebarHeader } from "./ParameterSidebar.styled";
export interface ParameterSidebarProps {
parameter: Parameter;
otherParameters: Parameter[];
parameter: UiParameter;
otherParameters: UiParameter[];
onChangeName: (parameterId: ParameterId, name: string) => void;
onChangeDefaultValue: (parameterId: ParameterId, value: unknown) => void;
onChangeIsMultiSelect: (
......@@ -151,7 +151,7 @@ const ParameterSidebar = ({
);
};
const getTabs = (parameter: Parameter) => {
const getTabs = (parameter: UiParameter) => {
const tabs = [{ value: "settings", name: t`Settings`, icon: "gear" }];
if (canUseLinkedFilters(parameter)) {
......
import React, { useCallback, useMemo, useState } from "react";
import {
Parameter,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import { ValuesSourceConfig, ValuesSourceType } from "metabase-types/api";
import { getNonVirtualFields } from "metabase-lib/parameters/utils/parameter-fields";
import {
getSourceConfig,
getSourceConfigForType,
getSourceType,
} from "metabase-lib/parameters/utils/parameter-source";
import { UiParameter } from "metabase-lib/parameters/types";
import ValuesSourceTypeModal from "./ValuesSourceTypeModal";
import ValuesSourceCardModal from "./ValuesSourceCardModal";
type ModalStep = "main" | "card";
interface ModalProps {
parameter: Parameter;
parameter: UiParameter;
onSubmit: (
sourceType: ValuesSourceType,
sourceConfig: ValuesSourceConfig,
......
......@@ -3,12 +3,12 @@ import { t } from "ttag";
import Radio from "metabase/core/components/Radio/Radio";
import Modal from "metabase/components/Modal";
import {
Parameter,
ValuesQueryType,
ValuesSourceConfig,
ValuesSourceType,
} from "metabase-types/api";
import { getQueryType } from "metabase-lib/parameters/utils/parameter-source";
import { UiParameter } from "metabase-lib/parameters/types";
import ValuesSourceModal from "../ValuesSourceModal";
import {
RadioLabelButton,
......@@ -17,7 +17,7 @@ import {
} from "./ValuesSourceSettings.styled";
export interface ValuesSourceSettingsProps {
parameter: Parameter;
parameter: UiParameter;
onChangeQueryType: (queryType: ValuesQueryType) => void;
onChangeSourceType: (sourceType: ValuesSourceType) => void;
onChangeSourceConfig: (sourceConfig: ValuesSourceConfig) => void;
......
import { Parameter } from "metabase-types/api";
import { SINGLE_OR_MULTI_SELECTABLE_TYPES } from "metabase-lib/parameters/constants";
import {
getParameterSubType,
CUSTOM_SOURCE_PARAMETER_TYPES,
SINGLE_OR_MULTI_SELECTABLE_TYPES,
} from "metabase-lib/parameters/constants";
import {
getParameterType,
getParameterSubType,
} from "metabase-lib/parameters/utils/parameter-type";
export function isSingleOrMultiSelectable(parameter: Parameter): boolean {
......@@ -17,3 +20,13 @@ export function isSingleOrMultiSelectable(parameter: Parameter): boolean {
}
return SINGLE_OR_MULTI_SELECTABLE_TYPES[type].includes(subType);
}
export const canUseCustomSource = (parameter: Parameter) => {
const type = getParameterType(parameter);
const subType = getParameterSubType(parameter);
return (
CUSTOM_SOURCE_PARAMETER_TYPES[type] != null &&
CUSTOM_SOURCE_PARAMETER_TYPES[type].includes(subType)
);
};
......@@ -11,7 +11,6 @@ import Toggle from "metabase/core/components/Toggle";
import InputBlurChange from "metabase/components/InputBlurChange";
import Select, { Option } from "metabase/core/components/Select";
import ValuesSourceSettings from "metabase/parameters/components/ValuesSourceSettings";
import { getParameterOptionsForField } from "metabase/parameters/utils/template-tag-options";
import { fetchField } from "metabase/redux/metadata";
......@@ -19,8 +18,6 @@ import { getMetadata } from "metabase/selectors/metadata";
import { SchemaTableAndFieldDataSelector } from "metabase/query_builder/components/DataSelector";
import MetabaseSettings from "metabase/lib/settings";
import { canUseCustomSource } from "metabase-lib/parameters/utils/parameter-source";
import {
ErrorSpan,
TagName,
......@@ -277,24 +274,6 @@ export class TagEditorParam extends Component {
/>
</InputContainer>
{parameter && canUseCustomSource(parameter) && (
<InputContainer>
<ContainerLabel>{t`How should users filter on this variable?`}</ContainerLabel>
<ValuesSourceSettings
parameter={parameter}
onChangeQueryType={value =>
this.setParameterAttribute("values_query_type", value)
}
onChangeSourceType={value =>
this.setParameterAttribute("values_source_type", value)
}
onChangeSourceConfig={value =>
this.setParameterAttribute("values_source_config", value)
}
/>
</InputContainer>
)}
{((tag.type !== "dimension" && tag.required) ||
tag.type === "dimension" ||
tag["widget-type"]) && (
......
......@@ -23,7 +23,6 @@ jest.mock("metabase/query_builder/components/DataSelector", () => ({
}));
jest.mock("metabase/entities/schemas", () => ({
load: () => children => children,
Loader: ({ children }) => children(),
}));
......
import {
modal,
popover,
} from "__support__/e2e/helpers/e2e-ui-elements-helpers";
export function setFilterQuestionSource({ question, field }) {
cy.findByText("Dropdown list").click();
cy.findByText("Edit").click();
modal().within(() => {
cy.findByText("From another model or question").click();
cy.findByText("Pick a model or question…").click();
});
modal().within(() => {
cy.findByPlaceholderText(/Search for a question/).type(question);
cy.findByText(question).click();
cy.button("Done").click();
});
modal().within(() => {
cy.findByText("Pick a column…").click();
});
popover().within(() => {
cy.findByText(field).click();
});
modal().within(() => {
cy.button("Done").click();
});
}
export function setFilterListSource({ values }) {
cy.findByText("Dropdown list").click();
cy.findByText("Edit").click();
modal().within(() => {
cy.findByText("Custom list").click();
cy.findByRole("textbox").clear().type(values.join("\n"));
cy.button("Done").click();
});
}
import { modal } from "__support__/e2e/helpers/e2e-ui-elements-helpers";
// Find a text field by label text, type it in, then blur the field.
// Commonly used in our Admin section as we auto-save settings.
export function typeAndBlurUsingLabel(label, value) {
......@@ -220,26 +218,3 @@ export function interceptIfNotPreviouslyDefined({ method, url, alias } = {}) {
cy.intercept(method, url).as(alias);
}
}
export function saveQuestion(
name,
{ wrapId = false, idAlias = "questionId" } = {},
) {
cy.intercept("POST", "/api/card").as("saveQuestion");
cy.findByText("Save").click();
modal().within(() => {
cy.findByLabelText("Name").type(name);
cy.button("Save").click();
});
cy.wait("@saveQuestion").then(({ response: { body } }) => {
if (wrapId) {
cy.wrap(body.id).as(idAlias);
}
});
modal().within(() => {
cy.button("Not now").click();
});
}
......@@ -6,7 +6,6 @@ export * from "./e2e-database-metadata-helpers";
export * from "./e2e-qa-databases-helpers";
export * from "./e2e-ad-hoc-question-helpers";
export * from "./e2e-enterprise-helpers";
export * from "./e2e-filter-helpers";
export * from "./e2e-mock-app-settings-helpers";
export * from "./e2e-notebook-helpers";
export * from "./e2e-cloud-helpers";
......
import {
editDashboard,
modal,
popover,
restore,
saveDashboard,
......@@ -7,8 +8,6 @@ import {
visitDashboard,
openQuestionActions,
visitQuestion,
setFilterQuestionSource,
setFilterListSource,
} from "__support__/e2e/helpers";
import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database";
......@@ -23,7 +22,7 @@ const dashboardQuestionDetails = {
};
const structuredQuestionDetails = {
name: "GUI source",
name: "Categories",
query: {
"source-table": PRODUCTS_ID,
aggregation: [["count"]],
......@@ -33,7 +32,7 @@ const structuredQuestionDetails = {
};
const nativeQuestionDetails = {
name: "SQL source",
name: "Categories",
native: {
query: "select distinct CATEGORY from PRODUCTS order by CATEGORY limit 2",
},
......@@ -48,7 +47,7 @@ describe("scenarios > dashboard > filters", () => {
});
it("should be able to use a structured question source", () => {
cy.createQuestion(structuredQuestionDetails, { wrapId: true });
cy.createQuestion(structuredQuestionDetails);
cy.createQuestionAndDashboard({
questionDetails: dashboardQuestionDetails,
}).then(({ body: { dashboard_id } }) => {
......@@ -58,16 +57,14 @@ describe("scenarios > dashboard > filters", () => {
editDashboard();
setFilter("Text or Category", "Is");
mapFilterToQuestion();
setFilterQuestionSource({ question: "GUI source", field: "Category" });
editDropdown();
setupStructuredQuestionSource();
saveDashboard();
filterDashboard();
cy.get("@questionId").then(visitQuestion);
archiveQuestion();
});
it("should be able to use a native question source", () => {
cy.createNativeQuestion(nativeQuestionDetails, { wrapId: true });
cy.createNativeQuestion(nativeQuestionDetails);
cy.createQuestionAndDashboard({
questionDetails: dashboardQuestionDetails,
}).then(({ body: { dashboard_id } }) => {
......@@ -77,12 +74,10 @@ describe("scenarios > dashboard > filters", () => {
editDashboard();
setFilter("Text or Category", "Is");
mapFilterToQuestion();
setFilterQuestionSource({ question: "SQL source", field: "CATEGORY" });
editDropdown();
setupNativeQuestionSource();
saveDashboard();
filterDashboard();
cy.get("@questionId").then(visitQuestion);
archiveQuestion();
});
it("should be able to use a static list source", () => {
......@@ -95,12 +90,113 @@ describe("scenarios > dashboard > filters", () => {
editDashboard();
setFilter("Text or Category", "Is");
mapFilterToQuestion();
setFilterListSource({ values: ["Doohickey", "Gadget"] });
editDropdown();
setupCustomList();
saveDashboard();
filterDashboard();
});
it("should result in a warning being shown when archiving a question it uses", () => {
cy.intercept("POST", "/api/dashboard/**/query").as("getCardQuery");
cy.createQuestion(structuredQuestionDetails, {
wrapId: true,
idAlias: "structuredQuestionId",
});
cy.createQuestionAndDashboard({
questionDetails: dashboardQuestionDetails,
}).then(({ body: { dashboard_id } }) => {
visitDashboard(dashboard_id);
});
editDashboard();
setFilter("Text or Category", "Is");
mapFilterToQuestion();
editDropdown();
setupStructuredQuestionSource();
saveDashboard();
cy.intercept("GET", "/api/collection/root/items**").as("getItems");
cy.get("@structuredQuestionId").then(question_id => {
visitQuestion(question_id);
openQuestionActions();
cy.findByTestId("archive-button").click();
modal().within(() => {
cy.findByText(
"This question will be removed from any dashboards or pulses using it. It will also be removed from the filter that uses it to populate values.",
);
});
});
});
});
const editDropdown = () => {
cy.findByText("Dropdown list").click();
cy.findByText("Edit").click();
};
const setupStructuredQuestionSource = () => {
modal().within(() => {
cy.findByText("From another model or question").click();
cy.findByText("Pick a model or question…").click();
});
modal().within(() => {
cy.findByPlaceholderText(/Search for a question/).type("Categories");
cy.findByText("Categories").click();
cy.button("Done").click();
});
modal().within(() => {
cy.findByText("Pick a column…").click();
});
popover().within(() => {
cy.findByText("Category").click();
});
modal().within(() => {
cy.wait("@dataset");
cy.findByDisplayValue(/Gadget/).should("be.visible");
cy.button("Done").click();
});
};
const setupNativeQuestionSource = () => {
modal().within(() => {
cy.findByText("From another model or question").click();
cy.findByText("Pick a model or question…").click();
});
modal().within(() => {
cy.findByText("Categories").click();
cy.button("Done").click();
});
modal().within(() => {
cy.findByText("Pick a column…").click();
});
popover().within(() => {
cy.findByText("CATEGORY").click();
});
modal().within(() => {
cy.wait("@dataset");
cy.findByDisplayValue(/Gadget/).should("be.visible");
cy.button("Done").click();
});
};
const setupCustomList = () => {
modal().within(() => {
cy.findByText("Custom list").click();
cy.findByRole("textbox").clear().type("Doohickey\nGadget");
cy.button("Done").click();
});
};
const mapFilterToQuestion = () => {
cy.findByText("Select…").click();
popover().within(() => cy.findByText("Category").click());
......@@ -121,11 +217,3 @@ const filterDashboard = () => {
cy.wait("@getCardQuery");
});
};
const archiveQuestion = () => {
openQuestionActions();
cy.findByTestId("archive-button").click();
cy.findByText(
"This question will be removed from any dashboards or pulses using it. It will also be removed from the filter that uses it to populate values.",
);
};
import {
openNativeEditor,
restore,
setFilterQuestionSource,
saveQuestion,
} from "__support__/e2e/helpers";
import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database";
import * as SQLFilter from "./helpers/e2e-sql-filter-helpers";
import * as FieldFilter from "./helpers/e2e-field-filter-helpers";
const { PRODUCTS_ID, PRODUCTS } = SAMPLE_DATABASE;
const structuredQuestionDetails = {
name: "GUI source",
query: {
"source-table": PRODUCTS_ID,
aggregation: [["count"]],
breakout: [["field", PRODUCTS.CATEGORY, null]],
filter: ["!=", ["field", PRODUCTS.CATEGORY, null], "Gizmo"],
},
};
const nativeQuestionDetails = {
name: "SQL source",
native: {
query: "select distinct CATEGORY from PRODUCTS order by CATEGORY limit 2",
},
};
describe("scenarios > filters > sql filters > values source", () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
});
it("should be able to use a custom source for a text filter", () => {
cy.createQuestion(structuredQuestionDetails);
openNativeEditor();
SQLFilter.enterParameterizedQuery("SELECT * FROM products WHERE {{f}}");
SQLFilter.openTypePickerFromDefaultFilterType();
setFilterQuestionSource({ question: "GUI source", field: "Category" });
saveQuestion("SQL filter");
});
it("should be able to use a custom source for a field filter", () => {
cy.createNativeQuestion(nativeQuestionDetails);
openNativeEditor();
SQLFilter.enterParameterizedQuery("SELECT * FROM products WHERE {{f}}");
SQLFilter.openTypePickerFromDefaultFilterType();
SQLFilter.chooseType("Field Filter");
FieldFilter.mapTo({ table: "Products", field: "Category" });
setFilterQuestionSource({ question: "SQL source", field: "CATEGORY" });
saveQuestion("SQL filter");
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment