From 4f7b562c909d287a045ac775ca4b3098c1be4e26 Mon Sep 17 00:00:00 2001 From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:40:21 +0100 Subject: [PATCH] Fix the stale collection information when the source in the data selector changes (#40063) * Bridge out of sync collection information between the card and the state * Account for models * Use existing helpers * Create virtual schema if it doesn't exist yet * Add E2E reproduction for #39812 * Create virtual schema if it doesn't exist yet * Create virtual schema if it doesn't exist yet * Create virtual schema if it doesn't exist yet * Expand the reproduction to include the nested question scenario * Improve assertions * Convert E2E repro to TS * Fix tests --------- Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com> --- ...virtual-table-collection-change.cy.spec.ts | 103 ++++++++++++++++++ frontend/src/metabase/entities/schemas.js | 28 +++-- .../metabase/entities/schemas.unit.spec.js | 12 +- frontend/src/metabase/entities/tables.js | 33 ++++-- 4 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 e2e/test/scenarios/question/reproductions/39812-data-selector-virtual-table-collection-change.cy.spec.ts diff --git a/e2e/test/scenarios/question/reproductions/39812-data-selector-virtual-table-collection-change.cy.spec.ts b/e2e/test/scenarios/question/reproductions/39812-data-selector-virtual-table-collection-change.cy.spec.ts new file mode 100644 index 00000000000..8df21cdaa0f --- /dev/null +++ b/e2e/test/scenarios/question/reproductions/39812-data-selector-virtual-table-collection-change.cy.spec.ts @@ -0,0 +1,103 @@ +import { + ORDERS_MODEL_ID, + ORDERS_COUNT_QUESTION_ID, +} from "e2e/support/cypress_sample_instance_data"; +import { + restore, + visitModel, + openNotebook, + openQuestionActions, + popover, + visitQuestion, +} from "e2e/support/helpers"; + +describe("issue 39812", () => { + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + }); + + it("moving the model to another collection should immediately be reflected in the data selector (metabase#39812-1)", () => { + visitModel(ORDERS_MODEL_ID); + openNotebook(); + + openDataSelector(); + assertSourceCollection("Our analytics"); + assertDataSource("Orders Model"); + + moveToCollection("First collection"); + + openDataSelector(); + assertSourceCollection("First collection"); + assertDataSource("Orders Model"); + }); + + it("moving the source question should immediately reflect in the data selector for the nested question that depends on it (metabase#39812-2)", () => { + const SOURCE_QUESTION_ID = ORDERS_COUNT_QUESTION_ID; + // Rename the source question to make assertions more explicit + const sourceQuestionName = "Source Question"; + cy.request("PUT", `/api/card/${ORDERS_COUNT_QUESTION_ID}`, { + name: sourceQuestionName, + }); + + const nestedQuestionDetails = { + name: "Nested Question", + query: { "source-table": `card__${SOURCE_QUESTION_ID}` }, + }; + + cy.createQuestion(nestedQuestionDetails, { + wrapId: true, + idAlias: "nestedQuestionId", + }); + + visitQuestion("@nestedQuestionId"); + openNotebook(); + + openDataSelector(); + assertSourceCollection("Our analytics"); + assertDataSource(sourceQuestionName); + + cy.log("Move the source question to another collection"); + visitQuestion(SOURCE_QUESTION_ID); + openNotebook(); + moveToCollection("First collection"); + + cy.log("Make sure the source change is reflected in a nested question"); + visitQuestion("@nestedQuestionId"); + openNotebook(); + + openDataSelector(); + assertSourceCollection("First collection"); + assertDataSource(sourceQuestionName); + }); +}); + +function moveToCollection(collection: string) { + openQuestionActions(); + popover().findByTextEnsureVisible("Move").click(); + cy.findByRole("dialog").within(() => { + cy.intercept("GET", "/api/collection/tree**").as("updateCollectionTree"); + cy.findAllByTestId("item-picker-item") + .filter(`:contains(${collection})`) + .click(); + + cy.button("Move").click(); + cy.wait("@updateCollectionTree"); + }); +} + +function openDataSelector() { + cy.findByTestId("data-step-cell").click(); +} + +function assertItemSelected(item: string) { + cy.findByLabelText(item).should("have.attr", "aria-selected", "true"); +} + +function assertSourceCollection(collection: string) { + return assertItemSelected(collection); +} + +function assertDataSource(questionOrModel: string) { + return assertItemSelected(questionOrModel); +} diff --git a/frontend/src/metabase/entities/schemas.js b/frontend/src/metabase/entities/schemas.js index 2f27ef88b17..59d711efac2 100644 --- a/frontend/src/metabase/entities/schemas.js +++ b/frontend/src/metabase/entities/schemas.js @@ -1,4 +1,4 @@ -import { updateIn } from "icepick"; +import { assocIn, updateIn } from "icepick"; import Questions from "metabase/entities/questions"; import { createEntity } from "metabase/lib/entities"; @@ -7,7 +7,9 @@ import { getMetadata } from "metabase/selectors/metadata"; import { MetabaseApi } from "metabase/services"; import { getCollectionVirtualSchemaId, + getCollectionVirtualSchemaName, getQuestionVirtualTableId, + SAVED_QUESTIONS_VIRTUAL_DB_ID, } from "metabase-lib/v1/metadata/utils/saved-questions"; import { generateSchemaId, @@ -84,16 +86,16 @@ export default createEntity({ } if (type === Questions.actionTypes.UPDATE && !error) { - const { question } = payload; - const schemaId = getCollectionVirtualSchemaId(question.collection, { - isDatasets: question.type === "model", + const { question: card } = payload; + const virtualSchemaId = getCollectionVirtualSchemaId(card.collection, { + isDatasets: card.type === "model", }); - - const virtualQuestionId = getQuestionVirtualTableId(question.id); + const virtualSchemaName = getCollectionVirtualSchemaName(card.collection); + const virtualQuestionId = getQuestionVirtualTableId(card.id); const previousSchemaContainingTheQuestion = getPreviousSchemaContainingTheQuestion( state, - schemaId, + virtualSchemaId, virtualQuestionId, ); @@ -105,16 +107,20 @@ export default createEntity({ ); } - if (!state[schemaId]) { - return state; + if (!state[virtualSchemaId]) { + state = assocIn(state, [virtualSchemaId], { + id: virtualSchemaId, + name: virtualSchemaName, + database: SAVED_QUESTIONS_VIRTUAL_DB_ID, + }); } - return updateIn(state, [schemaId, "tables"], tables => { + return updateIn(state, [virtualSchemaId, "tables"], tables => { if (!tables) { return tables; } - if (question.archived) { + if (card.archived) { return tables.filter(id => id !== virtualQuestionId); } return addTableAvoidingDuplicates(tables, virtualQuestionId); diff --git a/frontend/src/metabase/entities/schemas.unit.spec.js b/frontend/src/metabase/entities/schemas.unit.spec.js index 8a885388dd9..58bc34f975c 100644 --- a/frontend/src/metabase/entities/schemas.unit.spec.js +++ b/frontend/src/metabase/entities/schemas.unit.spec.js @@ -3,7 +3,10 @@ import fetchMock from "fetch-mock"; import { getStore } from "__support__/entities-store"; import Questions from "metabase/entities/questions"; import Schemas from "metabase/entities/schemas"; -import { ROOT_COLLECTION_VIRTUAL_SCHEMA } from "metabase-lib/v1/metadata/utils/saved-questions"; +import { + ROOT_COLLECTION_VIRTUAL_SCHEMA, + SAVED_QUESTIONS_VIRTUAL_DB_ID, +} from "metabase-lib/v1/metadata/utils/saved-questions"; describe("schema entity", () => { let store; @@ -251,7 +254,7 @@ describe("schema entity", () => { }); }); - it("should not add question ID when it is unarchived if collection schema is not present in store", () => { + it("should add question ID when it is unarchived if collection schema is not present in store", () => { const nextState = Schemas.reducer( { [ROOT_COLLECTION_VIRTUAL_SCHEMA]: { @@ -265,6 +268,11 @@ describe("schema entity", () => { [ROOT_COLLECTION_VIRTUAL_SCHEMA]: { tables: ["card__123"], }, + [`${SAVED_QUESTIONS_VIRTUAL_DB_ID}:foo`]: { + id: `${SAVED_QUESTIONS_VIRTUAL_DB_ID}:foo`, + name: "foo", + database: SAVED_QUESTIONS_VIRTUAL_DB_ID, + }, }); }); }); diff --git a/frontend/src/metabase/entities/tables.js b/frontend/src/metabase/entities/tables.js index 27b9f8546b5..2bc7fa6e82b 100644 --- a/frontend/src/metabase/entities/tables.js +++ b/frontend/src/metabase/entities/tables.js @@ -27,6 +27,8 @@ import { MetabaseApi } from "metabase/services"; import { convertSavedQuestionToVirtualTable, getQuestionVirtualTableId, + getCollectionVirtualSchemaId, + getCollectionVirtualSchemaName, } from "metabase-lib/v1/metadata/utils/saved-questions"; const listTablesForDatabase = async (...args) => @@ -169,25 +171,36 @@ const Tables = createEntity({ if (type === Questions.actionTypes.UPDATE && !error) { const card = payload.question; - const virtualQuestionId = getQuestionVirtualTableId(card.id); + const virtualTableId = getQuestionVirtualTableId(card.id); - if (card.archived && state[virtualQuestionId]) { - delete state[virtualQuestionId]; + if (card.archived && state[virtualTableId]) { + delete state[virtualTableId]; return state; } - if (state[virtualQuestionId]) { - const virtualQuestion = state[virtualQuestionId]; + if (state[virtualTableId]) { + const virtualTable = state[virtualTableId]; + const virtualSchemaId = getCollectionVirtualSchemaId(card.collection, { + isDatasets: card.type === "model", + }); + const virtualSchemaName = getCollectionVirtualSchemaName( + card.collection, + ); + if ( - virtualQuestion.display_name !== card.name || - virtualQuestion.moderated_status !== card.moderated_status || - virtualQuestion.description !== card.description + virtualTable.display_name !== card.name || + virtualTable.moderated_status !== card.moderated_status || + virtualTable.description !== card.description || + virtualTable.schema !== virtualSchemaId || + virtualTable.schema_name !== virtualSchemaName ) { - state = updateIn(state, [virtualQuestionId], table => ({ + state = updateIn(state, [virtualTableId], table => ({ ...table, display_name: card.name, moderated_status: card.moderated_status, description: card.description, + schema: virtualSchemaId, + schema_name: virtualSchemaName, })); } @@ -196,7 +209,7 @@ const Tables = createEntity({ return { ...state, - [virtualQuestionId]: convertSavedQuestionToVirtualTable(card), + [virtualTableId]: convertSavedQuestionToVirtualTable(card), }; } -- GitLab