diff --git a/frontend/src/metabase-lib/parameters/utils/click-behavior.js b/frontend/src/metabase-lib/parameters/utils/click-behavior.js index f94ef827255f27df4fd50617291d01d81f034752..b29f22b93915178a0263c4e9444183bc3082ae75 100644 --- a/frontend/src/metabase-lib/parameters/utils/click-behavior.js +++ b/frontend/src/metabase-lib/parameters/utils/click-behavior.js @@ -9,7 +9,6 @@ import { } from "metabase-lib/parameters/utils/filters"; import { isa, isDate } from "metabase-lib/types/utils/isa"; import { TYPE } from "metabase-lib/types/constants"; -import Question from "metabase-lib/Question"; import TemplateTagVariable from "metabase-lib/variables/TemplateTagVariable"; import { TemplateTagDimension } from "metabase-lib/Dimension"; @@ -76,14 +75,13 @@ export function getTargetsWithSourceFilters({ isAction, dashcard, object, - metadata, }) { if (isAction) { return getTargetsForAction(object); } return isDash ? getTargetsForDashboard(object, dashcard) - : getTargetsForQuestion(object, metadata); + : getTargetsForQuestion(object); } function getTargetsForAction(action) { @@ -106,8 +104,8 @@ function getTargetsForAction(action) { }); } -function getTargetsForQuestion(question, metadata) { - const query = new Question(question, metadata).query(); +function getTargetsForQuestion(question) { + const query = question.query(); return query .dimensionOptions() .all() diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts index 09612cf90e7e9b98dfa0a98c5d81e375587abf78..18cffba6c6176f478c17fac152572d28ce2ee803 100644 --- a/frontend/src/metabase-types/api/card.ts +++ b/frontend/src/metabase-types/api/card.ts @@ -129,3 +129,23 @@ export interface ModerationReview { export type CardId = number; export type ModerationReviewStatus = "verified"; + +export type CardFilterOption = + | "all" + | "mine" + | "bookmarked" + | "database" + | "table" + | "recent" + | "popular" + | "using_model" + | "archived"; + +export interface CardQuery { + ignore_view?: boolean; +} + +export interface CardListQuery { + f?: CardFilterOption; + model_id?: CardId; +} diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx index 85622d34631cde942e89753040ce9d2de30bbf7c..19f7f1ff8944124432f16f17eec56fe9511ed020 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx +++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx @@ -14,7 +14,6 @@ import Questions from "metabase/entities/questions"; import { getMetadata } from "metabase/selectors/metadata"; import type { - Card, CardId, DatabaseId, WritebackActionId, @@ -47,11 +46,10 @@ interface ActionLoaderProps { } interface ModelLoaderProps { - modelCard: Card; + model?: Question; } interface StateProps { - model: Question; metadata: Metadata; } @@ -68,8 +66,7 @@ type Props = OwnProps & StateProps & DispatchProps; -const mapStateToProps = (state: State, { modelCard }: ModelLoaderProps) => ({ - model: new Question(modelCard, getMetadata(state)), +const mapStateToProps = (state: State) => ({ metadata: getMetadata(state), }); @@ -98,7 +95,7 @@ function ActionCreator({ const [isSaveModalShown, setShowSaveModal] = useState(false); - const isEditable = isNew || model.canWriteActions(); + const isEditable = isNew || (model != null && model.canWriteActions()); const handleCreate = async (values: CreateActionFormValues) => { if (action.type !== "query") { @@ -124,7 +121,7 @@ function ActionCreator({ if (isSavedAction(action)) { const reduxAction = await onUpdateAction({ ...action, - model_id: model.id(), + model_id: model?.id(), visualization_settings: formSettings, }); const updatedAction = Actions.HACK_getObjectFromAction(reduxAction); @@ -170,7 +167,7 @@ function ActionCreator({ initialValues={{ name: action.name, description: action.description, - model_id: model.id(), + model_id: model?.id(), }} onCreate={handleCreate} onCancel={handleCloseNewActionModal} @@ -218,7 +215,7 @@ export default _.compose( }), Questions.load({ id: (state: State, props: OwnProps) => props?.modelId, - entityAlias: "modelCard", + entityAlias: "model", }), Database.loadList(), connect(mapStateToProps, mapDispatchToProps), diff --git a/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx b/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx index 662c77e357102c3ba0a3c1ace1442a8fc5590d13..99acb76099e3db0a5ea5ffca26030dfa30b7ff48 100644 --- a/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx +++ b/frontend/src/metabase/actions/containers/ActionCreatorModal/ActionCreatorModal.tsx @@ -9,8 +9,9 @@ import Actions from "metabase/entities/actions"; import Models from "metabase/entities/questions"; import { setErrorPage } from "metabase/redux/app"; -import type { Card, WritebackAction } from "metabase-types/api"; +import type { WritebackAction } from "metabase-types/api"; import type { AppErrorDescriptor, State } from "metabase-types/store"; +import Question from "metabase-lib/Question"; import ActionCreator from "../ActionCreator"; @@ -25,7 +26,7 @@ interface OwnProps { interface EntityLoaderProps { action?: WritebackAction; - model: Card; + model: Question; loading?: boolean; } @@ -52,7 +53,7 @@ function ActionCreatorModal({ }: ActionCreatorModalProps) { const actionId = Urls.extractEntityId(params.actionId); const modelId = Urls.extractEntityId(params.slug); - const databaseId = model.database_id || model.dataset_query.database; + const databaseId = model.databaseId(); useEffect(() => { if (loading === false) { @@ -60,7 +61,7 @@ function ActionCreatorModal({ const hasModelMismatch = action != null && action.model_id !== modelId; if (notFound || action?.archived) { - const nextLocation = Urls.modelDetail(model, "actions"); + const nextLocation = Urls.modelDetail(model.card(), "actions"); onChangeLocation(nextLocation); } else if (hasModelMismatch) { setErrorPage({ status: 404 }); diff --git a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx index 7f75e860cf2f3fd3b6fbf1062496964a9f332ae0..b92d6261fc7dd5a3202cb22d8588c9e74f9333d0 100644 --- a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx +++ b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx @@ -10,7 +10,6 @@ import PinnedItemSortDropTarget from "metabase/collections/components/PinnedItem import { isPreviewShown, isRootCollection } from "metabase/collections/utils"; import PinDropZone from "metabase/collections/components/PinDropZone"; import ItemDragSource from "metabase/containers/dnd/ItemDragSource"; -import Metadata from "metabase-lib/metadata/Metadata"; import Database from "metabase-lib/metadata/Database"; import { @@ -27,7 +26,6 @@ type Props = { deleteBookmark: (id: string, collection: string) => void; items: CollectionItem[]; collection: Collection; - metadata: Metadata; onCopy: (items: CollectionItem[]) => void; onMove: (items: CollectionItem[]) => void; }; @@ -39,7 +37,6 @@ function PinnedItemOverview({ deleteBookmark, items, collection, - metadata, onCopy, onMove, }: Props) { @@ -74,7 +71,6 @@ function PinnedItemOverview({ <div> <PinnedQuestionCard item={item} - metadata={metadata} collection={collection} databases={databases} bookmarks={bookmarks} diff --git a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx index 77f9a0c1338e07084fdbb1caac3a14a3f746c5bf..a564fab4d279da96d15e5dee02ab28cf311f7333 100644 --- a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx +++ b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionCard.tsx @@ -6,7 +6,6 @@ import { } from "metabase/collections/utils"; import Visualization from "metabase/visualizations/components/Visualization"; import { Bookmark, Collection, CollectionItem } from "metabase-types/api"; -import Metadata from "metabase-lib/metadata/Metadata"; import Database from "metabase-lib/metadata/Database"; import PinnedQuestionLoader from "./PinnedQuestionLoader"; import { @@ -19,7 +18,6 @@ import { export interface PinnedQuestionCardProps { item: CollectionItem; collection: Collection; - metadata: Metadata; databases?: Database[]; bookmarks?: Bookmark[]; onCopy: (items: CollectionItem[]) => void; @@ -31,7 +29,6 @@ export interface PinnedQuestionCardProps { const PinnedQuestionCard = ({ item, collection, - metadata, databases, bookmarks, onCopy, @@ -54,7 +51,7 @@ const PinnedQuestionCard = ({ deleteBookmark={onDeleteBookmark} /> {isPreview ? ( - <PinnedQuestionLoader id={item.id} metadata={metadata}> + <PinnedQuestionLoader id={item.id}> {({ question, rawSeries, loading, error, errorIcon }) => loading ? ( <CardPreviewSkeleton diff --git a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx index f535cb0c111edbaab157d84fa638973a36a522fc..fee51ba6c49ad317d24865ad52a027cd6f4ac21e 100644 --- a/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx +++ b/frontend/src/metabase/collections/components/PinnedQuestionCard/PinnedQuestionLoader.tsx @@ -5,12 +5,10 @@ import { getGenericErrorMessage, getPermissionErrorMessage, } from "metabase/visualizations/lib/errors"; -import Metadata from "metabase-lib/metadata/Metadata"; import Question from "metabase-lib/Question"; export interface PinnedQuestionLoaderProps { id: number; - metadata: Metadata; children: (props: PinnedQuestionChildrenProps) => JSX.Element; } @@ -24,7 +22,7 @@ export interface PinnedQuestionChildrenProps { export interface QuestionLoaderProps { loading: boolean; - question: any; + question: Question; } export interface QuestionResultLoaderProps { @@ -37,19 +35,18 @@ export interface QuestionResultLoaderProps { const PinnedQuestionLoader = ({ id, - metadata, children, }: PinnedQuestionLoaderProps): JSX.Element => { const questionRef = useRef<Question>(); return ( <Questions.Loader id={id} loadingAndErrorWrapper={false}> - {({ loading, question: card }: QuestionLoaderProps) => { - if (loading || !card.dataset_query) { + {({ loading, question: loadedQuestion }: QuestionLoaderProps) => { + if (loading || !loadedQuestion.query()) { return children({ loading: true }); } - const question = questionRef.current ?? new Question(card, metadata); + const question = questionRef.current ?? loadedQuestion; questionRef.current = question; return ( diff --git a/frontend/src/metabase/collections/containers/CollectionContent.jsx b/frontend/src/metabase/collections/containers/CollectionContent.jsx index 4f2dee99540ed802398975db9bdfcc78a631d8cf..cca66c11025bd034d60461ef345134d88a84a107 100644 --- a/frontend/src/metabase/collections/containers/CollectionContent.jsx +++ b/frontend/src/metabase/collections/containers/CollectionContent.jsx @@ -10,7 +10,6 @@ import Collection from "metabase/entities/collections"; import Search from "metabase/entities/search"; import { getUserIsAdmin } from "metabase/selectors/user"; -import { getMetadata } from "metabase/selectors/metadata"; import { getIsBookmarked } from "metabase/collections/selectors"; import { getSetting } from "metabase/selectors/settings"; import { getIsNavbarOpen, openNavbar } from "metabase/redux/app"; @@ -66,7 +65,6 @@ function mapStateToProps(state, props) { return { isAdmin: getUserIsAdmin(state), isBookmarked: getIsBookmarked(state, props), - metadata: getMetadata(state), isNavbarOpen: getIsNavbarOpen(state), uploadsEnabled: canAccessUploadsDb, }; @@ -88,7 +86,6 @@ function CollectionContent({ createBookmark, deleteBookmark, isAdmin, - metadata, isNavbarOpen, openNavbar, uploadFile, @@ -263,7 +260,6 @@ function CollectionContent({ deleteBookmark={deleteBookmark} items={pinnedItems} collection={collection} - metadata={metadata} onMove={handleMove} onCopy={handleCopy} onToggleSelected={toggleItem} diff --git a/frontend/src/metabase/common/hooks/index.ts b/frontend/src/metabase/common/hooks/index.ts index 4875b686164a9e1471096fe653f434fc1d582dc5..0feb14e2cdfa656293d9f34e604d5331ca3035d5 100644 --- a/frontend/src/metabase/common/hooks/index.ts +++ b/frontend/src/metabase/common/hooks/index.ts @@ -3,6 +3,8 @@ export * from "./use-database-list-query"; export * from "./use-database-query"; export * from "./use-metric-list-query"; export * from "./use-metric-query"; +export * from "./use-question-list-query"; +export * from "./use-question-query"; export * from "./use-schema-list-query"; export * from "./use-segment-list-query"; export * from "./use-segment-query"; diff --git a/frontend/src/metabase/common/hooks/use-question-list-query/index.ts b/frontend/src/metabase/common/hooks/use-question-list-query/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0005e0b915fc18e5781cbe8a906a6a1471f6fff --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-question-list-query/index.ts @@ -0,0 +1 @@ +export * from "./use-question-list-query"; diff --git a/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.ts b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed7f8da7d20cf40637bb7950571db3e0a6c0427c --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.ts @@ -0,0 +1,19 @@ +import Questions from "metabase/entities/questions"; +import { + useEntityListQuery, + UseEntityListQueryProps, + UseEntityListQueryResult, +} from "metabase/common/hooks/use-entity-list-query"; +import { CardListQuery } from "metabase-types/api"; +import Question from "metabase-lib/Question"; + +export const useQuestionListQuery = ( + props: UseEntityListQueryProps<CardListQuery> = {}, +): UseEntityListQueryResult<Question> => { + return useEntityListQuery(props, { + fetchList: Questions.actions.fetchList, + getList: Questions.selectors.getList, + getLoading: Questions.selectors.getLoading, + getError: Questions.selectors.getError, + }); +}; diff --git a/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b2661b4a81547e983da6c2a98eaf701dbd40cce0 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-question-list-query/use-question-list-query.unit.spec.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createMockCard } from "metabase-types/api/mocks"; +import { setupCardsEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useQuestionListQuery } from "./use-question-list-query"; + +const TEST_CARD = createMockCard(); + +const TestComponent = () => { + const { data = [], isLoading, error } = useQuestionListQuery(); + + if (isLoading || error) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return ( + <div> + {data.map(question => ( + <div key={question.id()}>{question.displayName()}</div> + ))} + </div> + ); +}; + +const setup = () => { + setupCardsEndpoints([TEST_CARD]); + renderWithProviders(<TestComponent />); +}; + +describe("useQuestionListQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText(TEST_CARD.name)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/common/hooks/use-question-query/index.ts b/frontend/src/metabase/common/hooks/use-question-query/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a87e38417b3829c7e32f8bd7c3d3fee0e69a6562 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-question-query/index.ts @@ -0,0 +1 @@ +export * from "./use-question-query"; diff --git a/frontend/src/metabase/common/hooks/use-question-query/use-question-query.ts b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..82761274f9fbb9678db93142c4f5f6883942a7af --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.ts @@ -0,0 +1,19 @@ +import Questions from "metabase/entities/questions"; +import { + useEntityQuery, + UseEntityQueryProps, + UseEntityQueryResult, +} from "metabase/common/hooks/use-entity-query"; +import { CardId, CardQuery } from "metabase-types/api"; +import Question from "metabase-lib/Question"; + +export const useQuestionQuery = ( + props: UseEntityQueryProps<CardId, CardQuery>, +): UseEntityQueryResult<Question> => { + return useEntityQuery(props, { + fetch: Questions.actions.fetch, + getObject: Questions.selectors.getObject, + getLoading: Questions.selectors.getLoading, + getError: Questions.selectors.getError, + }); +}; diff --git a/frontend/src/metabase/common/hooks/use-question-query/use-question-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4e8d0922ef42a54af535bb8dd7d99c536d872446 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-question-query/use-question-query.unit.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createMockCard } from "metabase-types/api/mocks"; +import { setupCardsEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useQuestionQuery } from "./use-question-query"; + +const TEST_CARD = createMockCard(); + +const TestComponent = () => { + const { data, isLoading, error } = useQuestionQuery({ + id: TEST_CARD.id, + }); + + if (isLoading || error || !data) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return <div>{data.displayName()}</div>; +}; + +const setup = () => { + setupCardsEndpoints([TEST_CARD]); + renderWithProviders(<TestComponent />); +}; + +describe("useQuestionQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText(TEST_CARD.name)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/containers/SavedQuestionLoader.jsx b/frontend/src/metabase/containers/SavedQuestionLoader.jsx index f454e59f7ca74a3e24119789b6a7ab76df8cd807..fd6f754adb92e18ab3729119ad4698742ce1e077 100644 --- a/frontend/src/metabase/containers/SavedQuestionLoader.jsx +++ b/frontend/src/metabase/containers/SavedQuestionLoader.jsx @@ -36,21 +36,26 @@ import Question from "metabase-lib/Question"; * * @example */ -const SavedQuestionLoader = ({ children, card, error, loading }) => { +const SavedQuestionLoader = ({ + children, + question: loadedQuestion, + error, + loading, +}) => { const metadata = useSelector(getMetadata); const dispatch = useDispatch(); const [question, setQuestion] = useState(null); const cardMetadataState = useAsync(async () => { - if (card?.id == null) { + if (loadedQuestion?.id() == null) { return; } - await dispatch(loadMetadataForCard(card)); - }, [card?.id]); + await dispatch(loadMetadataForCard(loadedQuestion.card())); + }, [loadedQuestion?.id()]); useEffect(() => { - if (card?.id == null) { + if (loadedQuestion?.id() == null) { return; } @@ -63,9 +68,9 @@ const SavedQuestionLoader = ({ children, card, error, loading }) => { } if (!question) { - setQuestion(new Question(card, metadata)); + setQuestion(new Question(loadedQuestion.card(), metadata)); } - }, [card, metadata, cardMetadataState, question]); + }, [loadedQuestion, metadata, cardMetadataState, question]); return ( children?.({ @@ -80,6 +85,5 @@ export default _.compose( Questions.load({ id: (_state, props) => props.questionId, loadingAndErrorWrapper: false, - entityAlias: "card", }), )(SavedQuestionLoader); diff --git a/frontend/src/metabase/dashboard/actions/cards.js b/frontend/src/metabase/dashboard/actions/cards.js index fe78bd94b9598f69e28fd5f235fa0ff6f897aa6b..74fe0ce976fd457b4ab4ad6fb5eece9be53994ea 100644 --- a/frontend/src/metabase/dashboard/actions/cards.js +++ b/frontend/src/metabase/dashboard/actions/cards.js @@ -27,9 +27,9 @@ export const addCardToDashboard = ({ dashId, cardId, tabId }) => async (dispatch, getState) => { await dispatch(Questions.actions.fetch({ id: cardId })); - const card = Questions.selectors.getObject(getState(), { - entityId: cardId, - }); + const card = Questions.selectors + .getObject(getState(), { entityId: cardId }) + .card(); const { dashboards, dashcards } = getState().dashboard; const dashboard = dashboards[dashId]; const existingCards = dashboard.ordered_cards diff --git a/frontend/src/metabase/dashboard/actions/metadata.js b/frontend/src/metabase/dashboard/actions/metadata.js index 5d39e9547528e01a362fe44b14152297e89f26fb..5291eecf64bf836de02d16476867144d6c326c6b 100644 --- a/frontend/src/metabase/dashboard/actions/metadata.js +++ b/frontend/src/metabase/dashboard/actions/metadata.js @@ -47,7 +47,7 @@ const loadMetadataForLinkedTargets = const cards = linkTargets .filter(({ entityType }) => entityType === "question") .map(({ entityId }) => - Questions.selectors.getObject(getState(), { entityId }), + Questions.selectors.getObject(getState(), { entityId })?.card(), ) .filter(card => card != null); diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx index 619bf81dcc18a8ac61eb783d4aab9f572d73b55f..87a7a5a65bac5dabbb7b754005b9647cc6a90abf 100644 --- a/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx +++ b/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx @@ -4,7 +4,6 @@ import PropTypes from "prop-types"; import { t } from "ttag"; import { getIn } from "icepick"; import { connect } from "react-redux"; -import { createSelector } from "@reduxjs/toolkit"; import _ from "underscore"; import Visualization from "metabase/visualizations/components/Visualization"; @@ -13,20 +12,12 @@ import * as MetabaseAnalytics from "metabase/lib/analytics"; import { color } from "metabase/lib/colors"; import Questions from "metabase/entities/questions"; -import { getMetadataWithHiddenTables } from "metabase/selectors/metadata"; import { loadMetadataForQueries } from "metabase/redux/metadata"; import { getVisualizationRaw } from "metabase/visualizations"; -import Question from "metabase-lib/Question"; import { QuestionList } from "./QuestionList"; -const getQuestions = createSelector( - [getMetadataWithHiddenTables, (_state, props) => props.questions], - (metadata, questions) => - questions && questions.map(card => new Question(card, metadata)), -); - // TODO: rework this so we don't have to load all cards up front class AddSeriesModal extends Component { @@ -239,11 +230,9 @@ class AddSeriesModal extends Component { } export default _.compose( - Questions.loadList({ query: { f: "all" } }), - connect( - (state, ownProps) => ({ - questions: getQuestions(state, ownProps), - }), - { loadMetadataForQueries }, - ), + Questions.loadList({ + query: { f: "all" }, + selectorName: "getListUnfiltered", + }), + connect(null, { loadMetadataForQueries }), )(AddSeriesModal); diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx index 471e427ac30d2acb4c5b49796d419af947ec5732..fd7ec07b727aad49daa0af7a3967a8b39f86f6d7 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkedEntityPicker.tsx @@ -19,11 +19,11 @@ import type { Dashboard, DashboardId, DashboardOrderedCard, - Card, CardId, ClickBehavior, EntityCustomDestinationClickBehavior, } from "metabase-types/api"; +import Question from "metabase-lib/Question"; import { SidebarItem } from "../SidebarItem"; import { Heading } from "../ClickBehaviorSidebar.styled"; @@ -82,7 +82,7 @@ function PickerControl({ ); } -function getTargetClickMappingsHeading(entity: Card | Dashboard) { +function getTargetClickMappingsHeading(entity: Question | Dashboard) { return { dashboard: t`Pass values to this dashboard's filters (optional)`, native: t`Pass values to this question's variables (optional)`, @@ -104,7 +104,7 @@ function TargetClickMappings({ const Entity = isDash ? Dashboards : Questions; return ( <Entity.Loader id={clickBehavior.targetId}> - {({ object }: { object: Card | Dashboard }) => ( + {({ object }: { object: Question | Dashboard }) => ( <div className="pt1"> <Heading>{getTargetClickMappingsHeading(object)}</Heading> <ClickMappings diff --git a/frontend/src/metabase/dashboard/components/ClickMappings.jsx b/frontend/src/metabase/dashboard/components/ClickMappings.jsx index 3e51acf75878a2bb40540626223b9ebc054e568c..6fbad9ab509e3041a6efa42927c49734a62692ff 100644 --- a/frontend/src/metabase/dashboard/components/ClickMappings.jsx +++ b/frontend/src/metabase/dashboard/components/ClickMappings.jsx @@ -13,7 +13,6 @@ import { isPivotGroupColumn } from "metabase/lib/data_grid"; import { GTAPApi } from "metabase/services"; import { loadMetadataForQuery } from "metabase/redux/metadata"; -import { getMetadata } from "metabase/selectors/metadata"; import { getParameters } from "metabase/dashboard/selectors"; import { getTargetsWithSourceFilters } from "metabase-lib/parameters/utils/click-behavior"; import Question from "metabase-lib/Question"; @@ -120,7 +119,6 @@ const ClickMappings = _.compose( withUserAttributes, connect((state, props) => { const { object, isDash, dashcard, clickBehavior } = props; - const metadata = getMetadata(state, props); let parameters = getParameters(state, props); if (props.excludeParametersSources) { @@ -142,7 +140,6 @@ const ClickMappings = _.compose( isDash, dashcard, object, - metadata, }), ({ id }) => getIn(clickBehavior, ["parameterMapping", id, "source"]) != null, @@ -286,9 +283,9 @@ function loadQuestionMetadata(getQuestion) { } fetch() { - const { question, metadata, loadMetadataForQuery } = this.props; + const { question, loadMetadataForQuery } = this.props; if (question) { - loadMetadataForQuery(new Question(question, metadata).query()); + loadMetadataForQuery(question.query()); } } @@ -301,7 +298,6 @@ function loadQuestionMetadata(getQuestion) { return connect( (state, props) => ({ - metadata: getMetadata(state), question: getQuestion && getQuestion(state, props), }), { loadMetadataForQuery }, @@ -335,9 +331,9 @@ export function isMappableColumn(column) { } export function clickTargetObjectType(object) { - if (!object.dataset_query) { + if (!(object instanceof Question)) { return "dashboard"; - } else if (new Question(object).isNative()) { + } else if (object.isNative()) { return "native"; } else { return "gui"; diff --git a/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js b/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js index ac6d20cddda21fa832cd42c522bb94b8e26a4223..64edefa938f2f0b6b7c4d4d90938db089eb190b0 100644 --- a/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js +++ b/frontend/src/metabase/dashboard/hoc/WithVizSettingsData.js @@ -17,8 +17,10 @@ const WithVizSettingsData = ComposedComponent => { .groupBy(target => target.entity.name) .mapObject(targets => _.chain(targets) - .map(({ entity, entityId }) => - entity.selectors.getObject(state, { entityId }), + .map(({ entity, entityType, entityId }) => + entityType === "question" + ? entity.selectors.getObject(state, { entityId })?.card() + : entity.selectors.getObject(state, { entityId }), ) .filter(object => object != null) .indexBy(object => object.id) diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js index 5999db91f6f112a4c551a6a332c7fb4543b8e45c..01eb1b394e87f93d01f2fd1474c2ab834bccab11 100644 --- a/frontend/src/metabase/entities/questions.js +++ b/frontend/src/metabase/entities/questions.js @@ -4,18 +4,21 @@ import { updateIn } from "icepick"; import { createEntity, undo } from "metabase/lib/entities"; import * as Urls from "metabase/lib/urls"; import { color } from "metabase/lib/colors"; +import { + getMetadata, + getMetadataUnfiltered, +} from "metabase/selectors/metadata"; import Collections, { getCollectionType, normalizedCollection, } from "metabase/entities/collections"; - import { API_UPDATE_QUESTION, SOFT_RELOAD_CARD, } from "metabase/query_builder/actions"; -import { canonicalCollectionId } from "metabase/collections/utils"; +import { canonicalCollectionId } from "metabase/collections/utils"; import forms from "./questions/forms"; const Questions = createEntity({ @@ -83,6 +86,19 @@ const Questions = createEntity({ Questions.actions.update({ id }, { collection_preview }, opts), }, + selectors: { + getObject: (state, { entityId }) => getMetadata(state).question(entityId), + getObjectUnfiltered: (state, { entityId }) => + getMetadataUnfiltered(state).question(entityId), + getListUnfiltered: (state, { entityQuery }) => { + const entityIds = + Questions.selectors.getEntityIds(state, { entityQuery }) ?? []; + return entityIds.map(entityId => + Questions.selectors.getObjectUnfiltered(state, { entityId }), + ); + }, + }, + objectSelectors: { getName: question => question && question.name, getUrl: (question, opts) => question && Urls.question(question, opts), diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js index 49a0882a15363b80a4d7edb6da15b64839c5b1ca..7f811d407fbf6183e22e64908dc10efa50f632c6 100644 --- a/frontend/src/metabase/lib/card.js +++ b/frontend/src/metabase/lib/card.js @@ -27,10 +27,10 @@ export function startNewCard(type, databaseId, tableId) { export async function loadCard(cardId, { dispatch, getState }) { try { await dispatch(Questions.actions.fetch({ id: cardId }, { reload: true })); - const card = Questions.selectors.getObject(getState(), { + const question = Questions.selectors.getObject(getState(), { entityId: cardId, }); - return card; + return question?.card(); } catch (error) { console.error("error loading card", error); throw error; diff --git a/frontend/src/metabase/lib/entities.js b/frontend/src/metabase/lib/entities.js index 2610d56e66878b0af936913f7a2231ed276d9eaa..ab0d9ae9cae972fa4ab7e102039593b968896827 100644 --- a/frontend/src/metabase/lib/entities.js +++ b/frontend/src/metabase/lib/entities.js @@ -243,7 +243,7 @@ export function createEntity(def) { (entityObject, updatedObject = null, { notify } = {}) => async (dispatch, getState) => { // save the original object for undo - const originalObject = entity.selectors.getObject(getState(), { + const originalObject = getObject(getState(), { entityId: entityObject.id, }); // If a second object is provided just take the id from the first and diff --git a/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx b/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx index 72d14052436d4ba9a1a1ee3e41ec4bfa790946f4..10c56ba5df670979fe5d02419d9d877efcf86af4 100644 --- a/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx +++ b/frontend/src/metabase/metabot/components/MetabotWidget/MetabotWidget.tsx @@ -8,8 +8,7 @@ import Databases from "metabase/entities/databases"; import Questions from "metabase/entities/questions"; import Search from "metabase/entities/search"; import { getUser } from "metabase/selectors/user"; -import { getMetadata } from "metabase/selectors/metadata"; -import { Card, CollectionItem, DatabaseId, User } from "metabase-types/api"; +import { CollectionItem, DatabaseId, User } from "metabase-types/api"; import { Dispatch, State } from "metabase-types/store"; import { canUseMetabotOnDatabase } from "metabase/metabot/utils"; import Question from "metabase-lib/Question"; @@ -28,12 +27,11 @@ interface SearchLoaderProps { } interface CardLoaderProps { - card?: Card; + model?: Question; } interface StateProps { user: User | null; - model: Question | null; databases: Database[]; } @@ -41,14 +39,16 @@ interface DispatchProps { onSubmitQuery: (databaseId: DatabaseId, query: string) => void; } -type MetabotWidgetProps = StateProps & DispatchProps & DatabaseLoaderProps; +type MetabotWidgetProps = StateProps & + DispatchProps & + CardLoaderProps & + DatabaseLoaderProps; const mapStateToProps = ( state: State, - { card, databases }: CardLoaderProps & DatabaseLoaderProps, + { databases }: DatabaseLoaderProps, ): StateProps => ({ user: getUser(state), - model: card ? new Question(card, getMetadata(state)) : null, databases: databases.filter(canUseMetabotOnDatabase), }); @@ -103,7 +103,7 @@ const getGreetingMessage = (user: User | null) => { } }; -const getPromptPlaceholder = (model: Question | null) => { +const getPromptPlaceholder = (model: Question | undefined) => { if (model) { return t`Ask something like, how many ${model?.displayName()} have we had over time?`; } else { @@ -122,7 +122,7 @@ export default _.compose( }), Questions.load({ id: (state: State, { models }: SearchLoaderProps) => models[0]?.id, - entityAlias: "card", + entityAlias: "model", }), Databases.loadList(), connect(mapStateToProps, mapDispatchToProps), diff --git a/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx b/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx index 7b396cdabe4da123bcae19c136171eb532042d18..cfa8e246911a2fbd63781bf2b826048846de802e 100644 --- a/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx +++ b/frontend/src/metabase/metabot/containers/ModelMetabotApp/ModelMetabotApp.tsx @@ -3,9 +3,8 @@ import _ from "underscore"; import { LocationDescriptorObject } from "history"; import { checkNotNull } from "metabase/core/utils/types"; import { extractEntityId } from "metabase/lib/urls"; -import { getMetadata } from "metabase/selectors/metadata"; import Questions from "metabase/entities/questions"; -import { Card, CardId } from "metabase-types/api"; +import { CardId } from "metabase-types/api"; import { MetabotEntityType, State } from "metabase-types/store"; import Question from "metabase-lib/Question"; import Metabot from "../../components/Metabot"; @@ -20,26 +19,24 @@ interface RouteProps { } interface CardLoaderProps { - card: Card; + model: Question; } interface StateProps { entityId: CardId; entityType: MetabotEntityType; - model: Question; initialPrompt?: string; } const mapStateToProps = ( state: State, - { card, params, location }: CardLoaderProps & RouteProps, + { params, location }: CardLoaderProps & RouteProps, ): StateProps => { const entityId = checkNotNull(extractEntityId(params.slug)); return { entityId, entityType: "model", - model: new Question(card, getMetadata(state)), initialPrompt: location?.query?.prompt, }; }; @@ -48,7 +45,7 @@ const mapStateToProps = ( export default _.compose( Questions.load({ id: (state: State, { params }: RouteProps) => extractEntityId(params.slug), - entityAlias: "card", + entityAlias: "model", }), connect(mapStateToProps), )(Metabot); diff --git a/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx b/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx index 543561459599dd050238611325310d0187155784..ce70b1cc126492759f1d358afde4adf0431d8ddf 100644 --- a/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx +++ b/frontend/src/metabase/models/components/ModelDetailPage/ModelUsageDetails/ModelUsageDetails.tsx @@ -10,7 +10,6 @@ import Questions, { getIcon as getQuestionIcon, } from "metabase/entities/questions"; -import type { Card } from "metabase-types/api"; import type { State } from "metabase-types/store"; import type Question from "metabase-lib/Question"; @@ -28,13 +27,13 @@ interface OwnProps { } interface EntityLoaderProps { - cards: Card[]; + questions: Question[]; } type Props = OwnProps & EntityLoaderProps; -function ModelUsageDetails({ model, cards, hasNewQuestionLink }: Props) { - if (cards.length === 0) { +function ModelUsageDetails({ model, questions, hasNewQuestionLink }: Props) { + if (questions.length === 0) { return ( <EmptyStateContainer> <EmptyStateTitle>{t`This model is not used by any questions yet.`}</EmptyStateTitle> @@ -53,11 +52,14 @@ function ModelUsageDetails({ model, cards, hasNewQuestionLink }: Props) { return ( <ul> - {cards.map(card => ( - <li key={card.id}> - <CardListItem to={Urls.question(card)} aria-label={card.name}> - <Icon name={getQuestionIcon(card).name} /> - <CardTitle>{card.name}</CardTitle> + {questions.map(question => ( + <li key={question.id()}> + <CardListItem + to={Urls.question(question.card())} + aria-label={question.displayName() ?? ""} + > + <Icon name={getQuestionIcon(question.card()).name} /> + <CardTitle>{question.displayName()}</CardTitle> </CardListItem> </li> ))} @@ -74,6 +76,5 @@ function getCardListingQuery(state: State, { model }: OwnProps) { // eslint-disable-next-line import/no-default-export -- deprecated usage export default Questions.loadList({ - listName: "cards", query: getCardListingQuery, })(ModelUsageDetails); diff --git a/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx b/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx index 3b4a2abe885a543fa96c2f6410e8d552911bd417..5d8b7f90f723452dc53040f4d014adfbf5ccfa3b 100644 --- a/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx +++ b/frontend/src/metabase/models/containers/ModelDetailPage/ModelDetailPage.tsx @@ -13,7 +13,6 @@ import Actions from "metabase/entities/actions"; import Databases from "metabase/entities/databases"; import Questions from "metabase/entities/questions"; import Tables from "metabase/entities/tables"; -import { getMetadata } from "metabase/selectors/metadata"; import title from "metabase/hoc/Title"; import { loadMetadataForCard } from "metabase/questions/actions"; @@ -38,10 +37,6 @@ type OwnProps = { type EntityLoadersProps = { actions: WritebackAction[]; - modelCard: Card; -}; - -type StateProps = { model: Question; }; @@ -64,13 +59,7 @@ type DispatchProps = { onChangeLocation: (location: LocationDescriptor) => void; }; -type Props = OwnProps & EntityLoadersProps & StateProps & DispatchProps; - -function mapStateToProps(state: State, props: OwnProps & EntityLoadersProps) { - const metadata = getMetadata(state); - const model = new Question(props.modelCard, metadata); - return { model }; -} +type Props = OwnProps & EntityLoadersProps & DispatchProps; const mapDispatchToProps = { loadMetadataForCard, @@ -201,21 +190,21 @@ function getModelId(state: State, props: OwnProps) { return Urls.extractEntityId(props.params.slug); } -function getPageTitle({ modelCard }: Props) { - return modelCard?.name; +function getPageTitle({ model }: Props) { + return model?.displayName(); } // eslint-disable-next-line import/no-default-export -- deprecated usage export default _.compose( - Questions.load({ id: getModelId, entityAlias: "modelCard" }), + Questions.load({ id: getModelId, entityAlias: "model" }), Databases.loadList(), Actions.loadList({ query: (state: State, props: OwnProps) => ({ "model-id": getModelId(state, props), }), }), - connect<StateProps, DispatchProps, OwnProps & EntityLoadersProps, State>( - mapStateToProps, + connect<null, DispatchProps, OwnProps & EntityLoadersProps, State>( + null, mapDispatchToProps, ), title(getPageTitle), diff --git a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx index 6f3ffb4ccbf096fb589617b9a64725c278da9d1c..0a503945ec566b7226ebb87f0fb52872d8212ee8 100644 --- a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx +++ b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbar.tsx @@ -11,8 +11,9 @@ import Questions from "metabase/entities/questions"; import { getDashboard } from "metabase/dashboard/selectors"; -import type { Card, Dashboard } from "metabase-types/api"; +import type { Dashboard } from "metabase-types/api"; import type { State } from "metabase-types/store"; +import Question from "metabase-lib/Question"; import MainNavbarContainer from "./MainNavbarContainer"; @@ -28,7 +29,7 @@ import getSelectedItems, { import { NavRoot, Sidebar } from "./MainNavbar.styled"; interface EntityLoaderProps { - card?: Card; + question?: Question; } interface StateProps { @@ -63,7 +64,7 @@ function MainNavbar({ isOpen, location, params, - card, + question, dashboard, openNavbar, closeNavbar, @@ -92,10 +93,10 @@ function MainNavbar({ getSelectedItems({ pathname: location.pathname, params, - card, + question, dashboard, }), - [location, params, card, dashboard], + [location, params, question, dashboard], ); return ( @@ -136,6 +137,6 @@ export default _.compose( Questions.load({ id: maybeGetQuestionId, loadingAndErrorWrapper: false, - entityAlias: "card", + entityAlias: "question", }), )(MainNavbar); diff --git a/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts b/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts index 91131cb2b0cbefec685b0694e3ae1bed68d40c0a..502ade34bd18e019cfa4794f3f72f9f917072700 100644 --- a/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts +++ b/frontend/src/metabase/nav/containers/MainNavbar/getSelectedItems.ts @@ -2,7 +2,8 @@ import * as Urls from "metabase/lib/urls"; import { coerceCollectionId } from "metabase/collections/utils"; -import type { Card, Dashboard } from "metabase-types/api"; +import type { Dashboard } from "metabase-types/api"; +import Question from "metabase-lib/Question"; import { SelectedItem } from "./types"; @@ -12,7 +13,7 @@ type Opts = { slug?: string; pageId?: string; }; - card?: Card; + question?: Question; dashboard?: Dashboard; }; @@ -39,7 +40,7 @@ function isDashboardPath(pathname: string): boolean { function getSelectedItems({ pathname, params, - card, + question, dashboard, }: Opts): SelectedItem[] { const { slug } = params; @@ -66,14 +67,14 @@ function getSelectedItems({ }, ]; } - if ((isQuestionPath(pathname) || isModelPath(pathname)) && card) { + if ((isQuestionPath(pathname) || isModelPath(pathname)) && question) { return [ { - id: card.id, + id: question.id(), type: "card", }, { - id: coerceCollectionId(card.collection_id), + id: coerceCollectionId(question.collectionId()), type: "collection", }, ]; diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx index 0b3486b0fe6993b8bf1b3f4eea21fe56066b6ad9..42562394c6fee935d217c4b08258e2216a973ce0 100644 --- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx +++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceCardModal.tsx @@ -14,10 +14,8 @@ import DataPicker, { import Questions from "metabase/entities/questions"; import Collections from "metabase/entities/collections"; import Tables from "metabase/entities/tables"; -import { getMetadata } from "metabase/selectors/metadata"; import { coerceCollectionId } from "metabase/collections/utils"; import { - Card, CardId, Collection, Parameter, @@ -50,27 +48,22 @@ interface ModalOwnProps { onClose: () => void; } -interface ModalCardProps { - card: Card | undefined; +interface ModalQuestionProps { + question: Question | undefined; } interface ModalCollectionProps { collection: Collection | undefined; } -interface ModalStateProps { - question: Question | undefined; -} - interface ModalDispatchProps { onFetchCard: (cardId: CardId) => void; onFetchMetadata: (cardId: CardId) => void; } type ModalProps = ModalOwnProps & - ModalCardProps & + ModalQuestionProps & ModalCollectionProps & - ModalStateProps & ModalDispatchProps; const ValuesSourceCardModal = ({ @@ -181,13 +174,6 @@ const getCardIdFromValue = ({ tableIds }: DataPickerValue) => { } }; -const mapStateToProps = ( - state: State, - { card }: ModalCardProps, -): ModalStateProps => ({ - question: card ? new Question(card, getMetadata(state)) : undefined, -}); - const mapDispatchToProps: ModalDispatchProps = { onFetchCard: (cardId: CardId) => Questions.actions.fetch({ id: cardId }), onFetchMetadata: (cardId: CardId) => @@ -198,13 +184,12 @@ const mapDispatchToProps: ModalDispatchProps = { export default _.compose( Questions.load({ id: (state: State, { sourceConfig: { card_id } }: ModalOwnProps) => card_id, - entityAlias: "card", LoadingAndErrorWrapper: ModalLoadingAndErrorWrapper, }), Collections.load({ - id: (state: State, { card }: ModalCardProps) => - card ? coerceCollectionId(card.collection_id) : undefined, + id: (state: State, { question }: ModalQuestionProps) => + question ? coerceCollectionId(question?.collectionId()) : undefined, LoadingAndErrorWrapper: ModalLoadingAndErrorWrapper, }), - connect(mapStateToProps, mapDispatchToProps), + connect(null, mapDispatchToProps), )(ValuesSourceCardModal); diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx index 0b0c09a1e7383b5761c1126f3c904743fec75805..8d4274776620988bf41f3151c186f4ce714acd90 100644 --- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx +++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx @@ -19,9 +19,7 @@ import ModalContent from "metabase/components/ModalContent"; import { useSafeAsyncFunction } from "metabase/hooks/use-safe-async-function"; import Tables from "metabase/entities/tables"; import Questions from "metabase/entities/questions"; -import { getMetadata } from "metabase/selectors/metadata"; import { - Card, ValuesSourceConfig, ValuesSourceType, Parameter, @@ -62,11 +60,7 @@ interface ModalOwnProps { onClose: () => void; } -interface ModalCardProps { - card: Card | undefined; -} - -interface ModalStateProps { +interface ModalQuestionProps { question: Question | undefined; } @@ -76,10 +70,7 @@ interface ModalDispatchProps { ) => Promise<ParameterValues>; } -type ModalProps = ModalOwnProps & - ModalCardProps & - ModalStateProps & - ModalDispatchProps; +type ModalProps = ModalOwnProps & ModalQuestionProps & ModalDispatchProps; const ValuesSourceTypeModal = ({ parameter, @@ -495,13 +486,6 @@ const useParameterValues = ({ return state; }; -const mapStateToProps = ( - state: State, - { card }: ModalOwnProps & ModalCardProps, -): ModalStateProps => ({ - question: card ? new Question(card, getMetadata(state)) : undefined, -}); - const mapDispatchToProps = { onFetchParameterValues: fetchParameterValues, }; @@ -517,8 +501,7 @@ export default _.compose( }), Questions.load({ id: (state: State, { sourceConfig: { card_id } }: ModalOwnProps) => card_id, - entityAlias: "card", LoadingAndErrorWrapper: ModalLoadingAndErrorWrapper, }), - connect(mapStateToProps, mapDispatchToProps), + connect(null, mapDispatchToProps), )(ValuesSourceTypeModal); diff --git a/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx b/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx index 7bf8b90b90733b720bf3b0a32a05e1a953f96f22..cffd2a37f27f14afba21d99988a1cfe5f296389a 100644 --- a/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx +++ b/frontend/src/metabase/query_builder/components/dataref/QuestionPane/QuestionPane.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { connect } from "react-redux"; import { t, jt } from "ttag"; import _ from "underscore"; @@ -11,8 +10,6 @@ import { import Collections from "metabase/entities/collections"; import Questions from "metabase/entities/questions"; import SidebarContent from "metabase/query_builder/components/SidebarContent"; -import { getQuestionFromCard } from "metabase/query_builder/selectors"; -import type { Card } from "metabase-types/api/card"; import type { Collection } from "metabase-types/api/collection"; import type { State } from "metabase-types/store"; import Table from "metabase-lib/metadata/Table"; @@ -32,15 +29,10 @@ interface QuestionPaneProps { onItemClick: (type: string, item: unknown) => void; onBack: () => void; onClose: () => void; - card: Card; question: Question; collection: Collection | null; } -const mapStateToProps = (state: State, { card }: QuestionPaneProps) => ({ - question: getQuestionFromCard(state, card), -}); - const QuestionPane = ({ onItemClick, question, @@ -113,11 +105,10 @@ const QuestionPane = ({ export default _.compose( Questions.load({ id: (_state: State, props: QuestionPaneProps) => props.question.id, - entityAlias: "card", }), Collections.load({ - id: (_state: State, props: QuestionPaneProps) => props.card.collection_id, + id: (_state: State, props: QuestionPaneProps) => + props.question.collectionId(), loadingAndErrorWrapper: false, }), - connect(mapStateToProps), )(QuestionPane); diff --git a/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx b/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx index 2cb200a4d681d9f05493a25efb4b08cdda49e22b..6ddbf98be36a4dd81856f67821f13607cf86cd04 100644 --- a/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/Notebook.tsx @@ -1,11 +1,8 @@ import React from "react"; -import { connect } from "react-redux"; import { t } from "ttag"; import _ from "underscore"; import Button from "metabase/core/components/Button"; import Questions from "metabase/entities/questions"; -import { getMetadata } from "metabase/selectors/metadata"; -import { Card } from "metabase-types/api"; import { State } from "metabase-types/store"; import Question from "metabase-lib/Question"; import StructuredQuery from "metabase-lib/queries/StructuredQuery"; @@ -30,15 +27,11 @@ interface NotebookOwnProps { readOnly?: boolean; } -interface NotebookCardProps { - sourceCard?: Card; -} - -interface NotebookStateProps { +interface EntityLoaderProps { sourceQuestion?: Question; } -type NotebookProps = NotebookOwnProps & NotebookCardProps & NotebookStateProps; +type NotebookProps = NotebookOwnProps & EntityLoaderProps; const Notebook = ({ className, ...props }: NotebookProps) => { const { @@ -89,7 +82,7 @@ const Notebook = ({ className, ...props }: NotebookProps) => { ); }; -function getSourceCardId(question: Question) { +function getSourceQuestionId(question: Question) { const query = question.query(); if (query instanceof StructuredQuery) { const sourceTableId = query.sourceTableId(); @@ -99,22 +92,12 @@ function getSourceCardId(question: Question) { } } -function mapStateToProps( - state: State, - { sourceCard }: NotebookCardProps, -): NotebookStateProps { - return { - sourceQuestion: sourceCard && new Question(sourceCard, getMetadata(state)), - }; -} - // eslint-disable-next-line import/no-default-export -- deprecated usage export default _.compose( Questions.load({ id: (state: State, { question }: NotebookOwnProps) => - getSourceCardId(question), - entityAlias: "sourceCard", + getSourceQuestionId(question), + entityAlias: "sourceQuestion", loadingAndErrorWrapper: false, }), - connect(mapStateToProps), )(Notebook); diff --git a/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx b/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx index 38f72e53b7b0451432a54b64924662b6a158e8dd..ba12f498d6ca130d6962a8efc0b019417e8fbfe7 100644 --- a/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx +++ b/frontend/src/metabase/query_builder/components/view/QuestionDataSource.jsx @@ -4,6 +4,7 @@ import PropTypes from "prop-types"; import { color } from "metabase/lib/colors"; import * as Urls from "metabase/lib/urls"; +import Collections from "metabase/entities/collections"; import Questions from "metabase/entities/questions"; import Tooltip from "metabase/core/components/Tooltip"; @@ -48,7 +49,7 @@ function QuestionDataSource({ question, originalQuestion, subHead, ...props }) { if (originalQuestion?.id() === sourceQuestionId) { return ( <SourceDatasetBreadcrumbs - dataset={originalQuestion.card()} + model={originalQuestion} variant={variant} {...props} /> @@ -57,23 +58,35 @@ function QuestionDataSource({ question, originalQuestion, subHead, ...props }) { return ( <Questions.Loader id={sourceQuestionId} loadingAndErrorWrapper={false}> - {({ question: sourceQuestion }) => { - if (!sourceQuestion) { - return null; - } - if (sourceQuestion.dataset) { - return ( - <SourceDatasetBreadcrumbs - dataset={sourceQuestion} - variant={variant} - {...props} - /> - ); - } - return ( - <DataSourceCrumbs question={question} variant={variant} {...props} /> - ); - }} + {({ question: sourceQuestion }) => ( + <Collections.Loader + id={sourceQuestion?.collectionId()} + loadingAndErrorWrapper={false} + > + {({ collection, loading }) => { + if (!sourceQuestion || loading) { + return null; + } + if (sourceQuestion.isDataset()) { + return ( + <SourceDatasetBreadcrumbs + model={sourceQuestion} + collection={collection} + variant={variant} + {...props} + /> + ); + } + return ( + <DataSourceCrumbs + question={question} + variant={variant} + {...props} + /> + ); + }} + </Collections.Loader> + )} </Questions.Loader> ); } @@ -94,11 +107,11 @@ function DataSourceCrumbs({ question, variant, isObjectDetail, ...props }) { } SourceDatasetBreadcrumbs.propTypes = { - dataset: PropTypes.object.isRequired, + model: PropTypes.object.isRequired, + collection: PropTypes.object.isRequired, }; -function SourceDatasetBreadcrumbs({ dataset, ...props }) { - const { collection } = dataset; +function SourceDatasetBreadcrumbs({ model, collection, ...props }) { return ( <HeadBreadcrumbs {...props} @@ -111,7 +124,7 @@ function SourceDatasetBreadcrumbs({ dataset, ...props }) { > {collection?.name || t`Our analytics`} </HeadBreadcrumbs.Badge>, - dataset.archived ? ( + model.isArchived() ? ( <Tooltip key="dataset-name" tooltip={t`This model is archived and shouldn't be used.`} @@ -122,15 +135,15 @@ function SourceDatasetBreadcrumbs({ dataset, ...props }) { inactiveColor="text-light" icon={{ name: "warning", color: color("danger") }} > - {dataset.name} + {model.displayName()} </HeadBreadcrumbs.Badge> </Tooltip> ) : ( <HeadBreadcrumbs.Badge - to={Urls.question(dataset)} + to={Urls.question(model.card())} inactiveColor="text-light" > - {dataset.name} + {model.displayName()} </HeadBreadcrumbs.Badge> ), ]} diff --git a/frontend/test/metabase/lib/click-behavior.unit.spec.js b/frontend/test/metabase/lib/click-behavior.unit.spec.js index e2b7889c4c40dfd1737b51648221052a318dcedb..2aed2215e2c53e976d1653c57b78cdfe3478dbdc 100644 --- a/frontend/test/metabase/lib/click-behavior.unit.spec.js +++ b/frontend/test/metabase/lib/click-behavior.unit.spec.js @@ -1,11 +1,13 @@ import _ from "underscore"; import { metadata, PRODUCTS } from "__support__/sample_database_fixture"; import * as dateFormatUtils from "metabase/lib/formatting/date"; +import { createMockCard } from "metabase-types/api/mocks"; import { getDataFromClicked, getTargetsWithSourceFilters, formatSourceForTarget, } from "metabase-lib/parameters/utils/click-behavior"; +import Question from "metabase-lib/Question"; describe("metabase/lib/click-behavior", () => { describe("getDataFromClicked", () => { @@ -78,22 +80,25 @@ describe("metabase/lib/click-behavior", () => { it("should produce a template tag target", () => { const [{ id, name, target }] = getTargetsWithSourceFilters({ isDash: false, - object: { - dataset_query: { - type: "native", - native: { - query: "{{foo}}", - "template-tags": { - my_variable: { - "display-name": "My Variable", - id: "foo123", - name: "my_variable", - type: "text", + object: new Question( + createMockCard({ + dataset_query: { + type: "native", + native: { + query: "{{foo}}", + "template-tags": { + my_variable: { + "display-name": "My Variable", + id: "foo123", + name: "my_variable", + type: "text", + }, }, }, }, - }, - }, + }), + metadata, + ), metadata: {}, }); expect(id).toEqual("foo123"); @@ -104,25 +109,28 @@ describe("metabase/lib/click-behavior", () => { it("should produce a template tag dimension target", () => { const [{ id, name, target }] = getTargetsWithSourceFilters({ isDash: false, - object: { - dataset_query: { - type: "native", - native: { - query: "{{my_field_filter}}", - "template-tags": { - my_field_filter: { - default: null, - dimension: ["field", PRODUCTS.CATEGORY.id, null], - "display-name": "My Field Filter", - id: "foo123", - name: "my_field_filter", - type: "dimension", - "widget-type": "category", + object: new Question( + createMockCard({ + dataset_query: { + type: "native", + native: { + query: "{{my_field_filter}}", + "template-tags": { + my_field_filter: { + default: null, + dimension: ["field", PRODUCTS.CATEGORY.id, null], + "display-name": "My Field Filter", + id: "foo123", + name: "my_field_filter", + type: "dimension", + "widget-type": "category", + }, }, }, }, - }, - }, + }), + metadata, + ), metadata, }); expect(id).toEqual("foo123"); @@ -279,22 +287,25 @@ describe("metabase/lib/click-behavior", () => { it(`should filter sources for a ${targetVariableType} variable target`, () => { const [{ sourceFilters }] = getTargetsWithSourceFilters({ isDash: false, - object: { - dataset_query: { - type: "native", - native: { - query: "{{foo}}", - "template-tags": { - my_variable: { - "display-name": "My Variable", - id: "foo123", - name: "my_variable", - type: targetVariableType, + object: new Question( + createMockCard({ + dataset_query: { + type: "native", + native: { + query: "{{foo}}", + "template-tags": { + my_variable: { + "display-name": "My Variable", + id: "foo123", + name: "my_variable", + type: targetVariableType, + }, }, }, }, - }, - }, + }), + metadata, + ), metadata, }); @@ -359,25 +370,28 @@ describe("metabase/lib/click-behavior", () => { it(`should filter sources for a ${field.base_type} dimension target`, () => { const [{ sourceFilters }] = getTargetsWithSourceFilters({ isDash: false, - object: { - dataset_query: { - type: "native", - native: { - query: "{{my_field_filter}}", - "template-tags": { - my_field_filter: { - default: null, - dimension: ["field", field.id, null], - "display-name": "My Field Filter", - id: "foo123", - name: "my_field_filter", - type: "dimension", - "widget-type": "category", + object: new Question( + createMockCard({ + dataset_query: { + type: "native", + native: { + query: "{{my_field_filter}}", + "template-tags": { + my_field_filter: { + default: null, + dimension: ["field", field.id, null], + "display-name": "My Field Filter", + id: "foo123", + name: "my_field_filter", + type: "dimension", + "widget-type": "category", + }, }, }, }, - }, - }, + }), + metadata, + ), metadata, }); const filteredSources = _.mapObject(sources, (sources, sourceType) =>