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

Add card parameter source (#27354)

parent 6cc0e0fe
No related branches found
No related tags found
No related merge requests found
Showing
with 260 additions and 49 deletions
......@@ -580,7 +580,7 @@ class QuestionInner {
type: "query",
database: this.databaseId(),
query: {
"source-table": getQuestionVirtualTableId(this.card()),
"source-table": getQuestionVirtualTableId(this.id()),
},
},
};
......@@ -597,7 +597,7 @@ class QuestionInner {
type: "query",
database: this.databaseId(),
query: {
"source-table": getQuestionVirtualTableId(this.card()),
"source-table": getQuestionVirtualTableId(this.id()),
},
});
}
......@@ -1005,7 +1005,7 @@ class QuestionInner {
if (this.isDataset() && this.isSaved()) {
dependencies.push({
type: "table",
id: getQuestionVirtualTableId(this.card()),
id: getQuestionVirtualTableId(this.id()),
});
}
......
......@@ -122,7 +122,7 @@ export function isAdHocModelQuestionCard(card: Card, originalCard?: Card) {
const isSameCard = card.id === originalCard.id;
const { query } = card.dataset_query as StructuredDatasetQuery;
const isSelfReferencing =
query["source-table"] === getQuestionVirtualTableId(originalCard);
query["source-table"] === getQuestionVirtualTableId(originalCard.id);
return isModel && isSameCard && isSelfReferencing;
}
......
......@@ -28,8 +28,8 @@ export function getRootCollectionVirtualSchemaId({ isModels }) {
return getCollectionVirtualSchemaId(null, { isDatasets: isModels });
}
export function getQuestionVirtualTableId(card) {
return `card__${card.id}`;
export function getQuestionVirtualTableId(id) {
return `card__${id}`;
}
export function isVirtualCardId(tableId) {
......@@ -46,7 +46,7 @@ export function getQuestionIdFromVirtualTableId(tableId) {
export function convertSavedQuestionToVirtualTable(card) {
return {
id: getQuestionVirtualTableId(card),
id: getQuestionVirtualTableId(card.id),
display_name: card.name,
description: card.description,
moderated_status: card.moderated_status,
......
......@@ -82,7 +82,7 @@ describe("saved question helpers", () => {
describe("getQuestionVirtualTableId", () => {
it("returns question prefixed question ID", () => {
expect(getQuestionVirtualTableId({ id: 7 })).toBe("card__7");
expect(getQuestionVirtualTableId(7)).toBe("card__7");
});
});
......
......@@ -60,7 +60,7 @@ function createVirtualTableUsingQuestionMetadata(question: Question): Table {
});
return createVirtualTable({
id: getQuestionVirtualTableId(question.card()),
id: getQuestionVirtualTableId(question.id()),
name: questionDisplayName,
display_name: questionDisplayName,
db: question?.database(),
......
import { Parameter, ParameterSourceConfig } from "metabase-types/api";
import { Parameter, ValuesSourceConfig } from "metabase-types/api";
export const createMockParameter = (opts?: Partial<Parameter>): Parameter => ({
id: "1",
......@@ -8,8 +8,8 @@ export const createMockParameter = (opts?: Partial<Parameter>): Parameter => ({
...opts,
});
export const createMockParameterSourceOptions = (
opts?: Partial<ParameterSourceConfig>,
): ParameterSourceConfig => ({
export const createMockValuesSourceConfig = (
opts?: Partial<ValuesSourceConfig>,
): ValuesSourceConfig => ({
...opts,
});
import { CardId } from "./card";
export type StringParameterType =
| "string/="
| "string/!="
......@@ -40,12 +42,14 @@ export interface Parameter {
filteringParameters?: ParameterId[];
isMultiSelect?: boolean;
value?: any;
values_source_type?: ParameterSourceType;
values_source_config?: ParameterSourceConfig;
values_source_type?: ValuesSourceType;
values_source_config?: ValuesSourceConfig;
}
export type ParameterSourceType = null | "static-list";
export type ValuesSourceType = null | "card" | "static-list";
export interface ParameterSourceConfig {
export interface ValuesSourceConfig {
values?: string[];
card_id?: CardId;
value_field?: unknown[];
}
......@@ -129,8 +129,8 @@ const ListField = ({
)}
<OptionsList isDashboardFilter={isDashboardFilter}>
{filteredOptions.map(option => (
<OptionContainer key={option[0]}>
{filteredOptions.map((option, index) => (
<OptionContainer key={index}>
<Checkbox
data-testid={`${option[0]}-filter-value`}
checkedColor={
......
......@@ -23,6 +23,7 @@ interface DataSearchProps {
}
type TableSearchResult = {
id: number;
database_id: number;
table_schema: string;
table_id: number;
......@@ -71,7 +72,7 @@ function getValueForVirtualTable(table: TableSearchResult): DataPickerValue {
databaseId: SAVED_QUESTIONS_VIRTUAL_DB_ID,
schemaId,
collectionId: table.collection?.id || "root",
tableIds: [getQuestionVirtualTableId(table)],
tableIds: [getQuestionVirtualTableId(table.id)],
};
}
......
......@@ -7,8 +7,6 @@ import { color } from "metabase/lib/colors";
import { space } from "metabase/styled-components/theme";
export const List = styled(SelectList)`
padding: ${space(0)} ${space(1)} 12px ${space(1)};
${SelectList.BaseItem.Root} {
&:hover {
background-color: ${color("brand")};
......
......@@ -37,7 +37,6 @@ export const BackButton = styled.a`
color: ${color("text-dark")};
font-weight: 700;
margin-left: 1rem;
padding-bottom: 1rem;
&:hover {
......
......@@ -40,12 +40,18 @@ function cleanCollectionValue({
}
function cleanValue(value: Partial<DataPickerValue>): DataPickerValue {
const type = value.type;
const databaseId = cleanDatabaseValue({ ...value, type });
const schemaId = cleanSchemaValue({ ...value, databaseId });
const collectionId = cleanCollectionValue({ ...value, type, databaseId });
const tableIds = cleanTablesValue({ ...value, databaseId, schemaId });
return {
type: value.type,
databaseId: cleanDatabaseValue(value),
schemaId: cleanSchemaValue(value),
collectionId: cleanCollectionValue(value),
tableIds: cleanTablesValue(value),
type,
databaseId,
schemaId,
collectionId,
tableIds,
};
}
......
......@@ -207,16 +207,16 @@ export const setParameterSourceType = createThunkAction(
},
);
export const SET_PARAMETER_SOURCE_OPTIONS =
"metabase/dashboard/SET_PARAMETER_SOURCE_OPTIONS";
export const setParameterSourceOptions = createThunkAction(
SET_PARAMETER_SOURCE_OPTIONS,
(parameterId, sourceOptions) => (dispatch, getState) => {
export const SET_PARAMETER_SOURCE_CONFIG =
"metabase/dashboard/SET_PARAMETER_SOURCE_CONFIG";
export const setParameterSourceConfig = createThunkAction(
SET_PARAMETER_SOURCE_CONFIG,
(parameterId, sourceConfig) => (dispatch, getState) => {
updateParameter(dispatch, getState, parameterId, parameter => ({
...parameter,
values_source_config: sourceOptions,
values_source_config: sourceConfig,
}));
return { id: parameterId, sourceOptions };
return { id: parameterId, sourceConfig };
},
);
......
......@@ -28,7 +28,7 @@ DashboardSidebars.propTypes = {
setParameterDefaultValue: PropTypes.func.isRequired,
setParameterIsMultiSelect: PropTypes.func.isRequired,
setParameterSourceType: PropTypes.func.isRequired,
setParameterSourceOptions: PropTypes.func.isRequired,
setParameterSourceConfig: PropTypes.func.isRequired,
setParameterFilteringParameters: PropTypes.func.isRequired,
dashcardData: PropTypes.object,
isSharing: PropTypes.bool.isRequired,
......@@ -60,7 +60,7 @@ export function DashboardSidebars({
setParameterDefaultValue,
setParameterIsMultiSelect,
setParameterSourceType,
setParameterSourceOptions,
setParameterSourceConfig,
setParameterFilteringParameters,
dashcardData,
isFullscreen,
......@@ -125,7 +125,7 @@ export function DashboardSidebars({
onChangeDefaultValue={setParameterDefaultValue}
onChangeIsMultiSelect={setParameterIsMultiSelect}
onChangeSourceType={setParameterSourceType}
onChangeSourceOptions={setParameterSourceOptions}
onChangeSourceConfig={setParameterSourceConfig}
onChangeFilteringParameters={setParameterFilteringParameters}
onRemoveParameter={removeParameter}
onShowAddParameterPopover={showAddParameterPopover}
......
......@@ -20,6 +20,7 @@ const CONSUMED_PROPS = [
"loadingAndErrorWrapper",
"LoadingAndErrorWrapper",
"selectorName",
"requestType",
];
// NOTE: Memoize entityQuery so we don't re-render even if a new but identical
......@@ -31,6 +32,7 @@ const getMemoizedEntityQuery = createMemoizedSelector(
class EntityObjectLoaderInner extends React.Component {
static defaultProps = {
requestType: "fetch",
loadingAndErrorWrapper: true,
LoadingAndErrorWrapper: LoadingAndErrorWrapper,
reload: false,
......@@ -54,10 +56,15 @@ class EntityObjectLoaderInner extends React.Component {
);
}
fetch = (query, options) => {
const fetch = this.props[this.props.requestType];
return fetch(query, options);
};
UNSAFE_componentWillMount() {
const { entityId, entityQuery, fetch, dispatchApiErrorEvent } = this.props;
const { entityId, entityQuery, dispatchApiErrorEvent } = this.props;
if (entityId != null) {
fetch(
this.fetch(
{ id: entityId, ...entityQuery },
{
reload: this.props.reload,
......@@ -72,7 +79,7 @@ class EntityObjectLoaderInner extends React.Component {
nextProps.entityId !== this.props.entityId &&
nextProps.entityId != null
) {
nextProps.fetch(
this.fetch(
{ id: nextProps.entityId, ...nextProps.entityQuery },
{
reload: nextProps.reload,
......@@ -122,7 +129,7 @@ class EntityObjectLoaderInner extends React.Component {
}
reload = () => {
return this.props.fetch(
return this.fetch(
{ id: this.props.entityId },
{
reload: true,
......@@ -147,6 +154,7 @@ const EntityObjectLoader = _.compose(
entityId,
entityQuery,
selectorName = "getObject",
requestType = "fetch",
...props
},
) => {
......@@ -157,13 +165,15 @@ const EntityObjectLoader = _.compose(
entityQuery = entityQuery(state, props);
}
const entityOptions = { entityId, requestType };
return {
entityId,
entityQuery: getMemoizedEntityQuery(state, entityQuery),
object: entityDef.selectors[selectorName](state, { entityId }),
fetched: entityDef.selectors.getFetched(state, { entityId }),
loading: entityDef.selectors.getLoading(state, { entityId }),
error: entityDef.selectors.getError(state, { entityId }),
object: entityDef.selectors[selectorName](state, entityOptions),
fetched: entityDef.selectors.getFetched(state, entityOptions),
loading: entityDef.selectors.getLoading(state, entityOptions),
error: entityDef.selectors.getError(state, entityOptions),
};
},
),
......
......@@ -63,7 +63,7 @@ export default createEntity({
if (!state[schema]) {
return state;
}
const virtualQuestionId = getQuestionVirtualTableId(question);
const virtualQuestionId = getQuestionVirtualTableId(question.id);
return updateIn(state, [schema, "tables"], tables =>
addTableAvoidingDuplicates(tables, virtualQuestionId),
);
......@@ -82,7 +82,7 @@ export default createEntity({
isDatasets: question.dataset,
});
const virtualQuestionId = getQuestionVirtualTableId(question);
const virtualQuestionId = getQuestionVirtualTableId(question.id);
const previousSchemaContainingTheQuestion =
getPreviousSchemaContainingTheQuestion(
state,
......
......@@ -158,7 +158,7 @@ const Tables = createEntity({
if (type === Questions.actionTypes.UPDATE && !error) {
const card = payload.question;
const virtualQuestionId = getQuestionVirtualTableId(card);
const virtualQuestionId = getQuestionVirtualTableId(card.id);
if (card.archived && state[virtualQuestionId]) {
delete state[virtualQuestionId];
......
import styled from "@emotion/styled";
export const ModalBody = styled.div`
height: 50vh;
overflow-y: auto;
`;
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { t } from "ttag";
import _ from "underscore";
import Button from "metabase/core/components/Button/Button";
import ModalContent from "metabase/components/ModalContent";
import DataPickerContainer, {
DataPickerValue,
useDataPickerValue,
} from "metabase/containers/DataPicker";
import Questions from "metabase/entities/questions";
import Collections from "metabase/entities/collections";
import { getMetadata } from "metabase/selectors/metadata";
import { Card, CardId, Collection } from "metabase-types/api";
import { State } from "metabase-types/store";
import Question from "metabase-lib/Question";
import {
getCollectionVirtualSchemaId,
getQuestionIdFromVirtualTableId,
getQuestionVirtualTableId,
} from "metabase-lib/metadata/utils/saved-questions";
import { ModalBody } from "./CardStepModal.styled";
interface CardStepModalOwnProps {
cardId: CardId | undefined;
onChangeCard: (cardId: CardId | undefined) => void;
onSubmit: () => void;
onClose: () => void;
}
interface CardStepModalCardProps {
card: Card | undefined;
}
interface CardStepModalCollectionProps {
collection: Collection | undefined;
}
interface CardStepModalStateProps {
question: Question | undefined;
}
type CardStepModalProps = CardStepModalOwnProps &
CardStepModalCardProps &
CardStepModalCollectionProps &
CardStepModalStateProps;
const CardStepModal = ({
question,
collection,
onChangeCard,
onSubmit,
onClose,
}: CardStepModalProps): JSX.Element => {
const initialValue = getInitialValue(question, collection);
const [value, setValue] = useDataPickerValue(initialValue);
const cardId = getCardIdFromValue(value);
const handleSubmit = useCallback(() => {
onChangeCard(cardId);
onSubmit();
}, [cardId, onChangeCard, onSubmit]);
return (
<ModalContent
title={t`Pick a model or question to use for the values of this widget`}
footer={[
<Button key="cancel" onClick={onClose}>{t`Cancel`}</Button>,
<Button
key="submit"
primary
disabled={cardId == null}
onClick={handleSubmit}
>{t`Select column`}</Button>,
]}
onClose={onClose}
>
<ModalBody>
<DataPickerContainer value={value} onChange={setValue} />
</ModalBody>
</ModalContent>
);
};
const getInitialValue = (
question?: Question,
collection?: Collection,
): Partial<DataPickerValue> | undefined => {
if (question) {
const id = question.id();
const isDatasets = question.isDataset();
return {
type: isDatasets ? "models" : "questions",
schemaId: getCollectionVirtualSchemaId(collection, { isDatasets }),
collectionId: collection?.id,
tableIds: [getQuestionVirtualTableId(id)],
};
}
};
const getCardIdFromValue = ({ tableIds }: DataPickerValue) => {
if (tableIds.length) {
const cardId = getQuestionIdFromVirtualTableId(tableIds[0]);
if (cardId != null) {
return cardId;
}
}
};
export default _.compose(
Questions.load({
id: (state: State, { cardId }: CardStepModalOwnProps) => cardId,
entityAlias: "card",
}),
Collections.load({
id: (state: State, { card }: CardStepModalCardProps) =>
card?.collection_id ?? "root",
}),
connect((state: State, { card }: CardStepModalCardProps) => ({
question: card ? new Question(card, getMetadata(state)) : undefined,
})),
)(CardStepModal);
import React, { useCallback, useState } from "react";
import { ValuesSourceConfig } from "metabase-types/api";
import CardStepModal from "./CardStepModal";
import FieldStepModal from "./FieldStepModal";
type ModalStep = "card" | "field";
export interface CardValuesSourceModalProps {
sourceConfig: ValuesSourceConfig;
onChangeSourceConfig: (sourceConfig: ValuesSourceConfig) => void;
onClose: () => void;
}
const CardValuesSourceModal = ({
sourceConfig,
onChangeSourceConfig,
onClose,
}: CardValuesSourceModalProps): JSX.Element | null => {
const [step, setStep] = useState<ModalStep>("card");
const [cardId, setCardId] = useState(sourceConfig.card_id);
const [fieldReference, setFieldReference] = useState(
sourceConfig.value_field,
);
const handleCardSubmit = useCallback(() => {
setStep("field");
}, []);
const handleFieldSubmit = useCallback(() => {
onChangeSourceConfig({ card_id: cardId, value_field: fieldReference });
onClose();
}, [cardId, fieldReference, onChangeSourceConfig, onClose]);
const handleFieldCancel = useCallback(() => {
setStep("card");
}, []);
switch (step) {
case "card":
return (
<CardStepModal
cardId={cardId}
onChangeCard={setCardId}
onSubmit={handleCardSubmit}
onClose={onClose}
/>
);
case "field":
return (
<FieldStepModal
cardId={cardId}
fieldReference={fieldReference}
onChangeField={setFieldReference}
onSubmit={handleFieldSubmit}
onCancel={handleFieldCancel}
onClose={onClose}
/>
);
default:
return null;
}
};
export default CardValuesSourceModal;
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