diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 45d4fb08fcd7842d0e4555719ba8edd7eb8042e5..f484dcbf53c509544938be6561d4c1d0f496ec9b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -88,6 +88,7 @@ jobs: java-version: [11] edition: [ee] folder: + - "auditing" - "actions" - "admin" - "binning" @@ -256,6 +257,7 @@ jobs: java-version: [11] edition: [ee] folder: + - "auditing" - "actions" - "admin" - "binning" diff --git a/e2e/test/scenarios/auditing/ad-hoc.cy.spec.js b/e2e/test/scenarios/auditing/ad-hoc.cy.spec.js index d04799941da0b361778203024c4d5bd0f64d5b9d..57b795f24ca55cc4f2cf3d7ed4712585286c1cd4 100644 --- a/e2e/test/scenarios/auditing/ad-hoc.cy.spec.js +++ b/e2e/test/scenarios/auditing/ad-hoc.cy.spec.js @@ -54,7 +54,6 @@ describeEE("audit > ad-hoc", () => { cy.signInAsNormalUser(); openOrdersTable(); - cy.button("Visualize").click(); cy.wait("@dataset"); // Sign in as admin to be able to access audit logs in tests @@ -77,7 +76,11 @@ describeEE("audit > ad-hoc", () => { cy.get(".PageTitle").contains("Query"); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("Open in Metabase").should("be.visible"); + cy.findByText("Open in Metabase") + .should("have.attr", "href") + .then(href => { + expect(href.startsWith("/question#")).to.be.true; + }); cy.findByTestId("read-only-notebook").within(() => { cy.findByTestId("data-step-cell").within(() => { diff --git a/e2e/test/scenarios/auditing/approved-domains.cy.spec.js b/e2e/test/scenarios/auditing/approved-domains.cy.spec.js index 7ed3debb2dec9699ea1c4d526f5708dfdc1ae1b2..0c1bc4decbd377cae52dd68107121ba92088ebfd 100644 --- a/e2e/test/scenarios/auditing/approved-domains.cy.spec.js +++ b/e2e/test/scenarios/auditing/approved-domains.cy.spec.js @@ -50,9 +50,9 @@ describeEE( it("should validate approved email domains for a dashboard subscription in the audit app", () => { visitDashboard(1); - cy.icon("share").click(); + cy.icon("subscription").click(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("Dashboard subscriptions").click(); + cy.findByText("Create a dashboard subscription").click(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("Email it").click(); diff --git a/e2e/test/scenarios/auditing/auditing.cy.spec.js b/e2e/test/scenarios/auditing/auditing.cy.spec.js index a72f15fe86f78bb5202e372d2c67f1f1d0c2d791..c2b51533b5171fd866431ae1999e12545eb6aa8a 100644 --- a/e2e/test/scenarios/auditing/auditing.cy.spec.js +++ b/e2e/test/scenarios/auditing/auditing.cy.spec.js @@ -89,7 +89,7 @@ describeEE("audit > auditing", () => { beforeEach(cy.signInAsAdmin); describe("See expected info on team member pages", () => { - it("should load the Overview tab", () => { + it.skip(`should load the Overview tab (metabase#32244)`, () => { cy.visit("/admin/audit/members/overview"); // We haven't created any new members yet so this should be empty @@ -204,7 +204,7 @@ describeEE("audit > auditing", () => { // All tables tab cy.visit("/admin/audit/tables/all"); cy.findByPlaceholderText("Table name"); - cy.findAllByText("PUBLIC").should("have.length", 4); + cy.findAllByText("PUBLIC").should("have.length", 8); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("REVIEWS"); // Table name in DB // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage @@ -226,7 +226,7 @@ describeEE("audit > auditing", () => { // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("Query views and speed per day"); cy.findAllByText("No results!").should("not.exist"); - cy.get(".LineAreaBarChart").should("have.length", 3); + cy.get(".LineAreaBarChart").should("have.length", 1); cy.get("rect"); cy.get(".voronoi"); diff --git a/e2e/test/scenarios/auditing/questions-audit.cy.spec.js b/e2e/test/scenarios/auditing/questions-audit.cy.spec.js index 950d5204cfb509e07019a5da5aaa658c07541ff4..2b527bca96a7ea735a5b4b76a52725476d97c844 100644 --- a/e2e/test/scenarios/auditing/questions-audit.cy.spec.js +++ b/e2e/test/scenarios/auditing/questions-audit.cy.spec.js @@ -22,9 +22,24 @@ describeEE("audit > auditing > questions", () => { const QUERY_RUNS_ASC_ORDER = [...QUERY_RUNS_DESC_ORDER].reverse(); - _.times(1, () => visitQuestion(ORDERS_QUESTION_ID)); - _.times(2, () => visitQuestion(ORDERS_COUNT_QUESTION_ID)); - _.times(3, () => visitQuestion(ORDERS_BY_YEAR_QUESTION_ID)); + _.times(1, () => + visitQuestionAndVerifyTitle( + ORDERS_QUESTION_ID, + QUERY_RUNS_DESC_ORDER[2], + ), + ); + _.times(2, () => + visitQuestionAndVerifyTitle( + ORDERS_COUNT_QUESTION_ID, + QUERY_RUNS_DESC_ORDER[1], + ), + ); + _.times(3, () => + visitQuestionAndVerifyTitle( + ORDERS_BY_YEAR_QUESTION_ID, + QUERY_RUNS_DESC_ORDER[0], + ), + ); cy.visit("/admin/audit/questions/all"); @@ -51,7 +66,7 @@ describeEE("audit > auditing > questions", () => { }); it("should support filtering by collection name", () => { - const FIRST_COLLECTION_ID = 9; + const FIRST_COLLECTION_ID = 10; cy.createNativeQuestion({ name: "My question", @@ -90,3 +105,8 @@ const assertRowsOrder = names => { cy.wrap(nameColumn).should("have.text", names[index]); }); }; + +const visitQuestionAndVerifyTitle = (id, title) => { + visitQuestion(id); + cy.findByTestId("saved-question-header-title").should("have.value", title); +}; diff --git a/e2e/test/scenarios/dashboard/x-rays.cy.spec.js b/e2e/test/scenarios/dashboard/x-rays.cy.spec.js index feb7fdd1dfa9f812b0edbaa960cd09139d7830eb..47863be0f3c06acd08772ff5f116da4907708c7d 100644 --- a/e2e/test/scenarios/dashboard/x-rays.cy.spec.js +++ b/e2e/test/scenarios/dashboard/x-rays.cy.spec.js @@ -6,6 +6,9 @@ import { visualize, startNewQuestion, main, + addOrUpdateDashboardCard, + visitDashboardAndCreateTab, + popover, } from "e2e/support/helpers"; import { SAMPLE_DB_ID } from "e2e/support/cypress_data"; @@ -226,4 +229,33 @@ describe("scenarios > x-rays", () => { // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("463"); }); + + it("should be able to open x-ray on a dashcard from a dashboard with multiple tabs", () => { + cy.intercept("POST", "/api/dataset").as("dataset"); + + return cy.createDashboard(name).then(({ body: { id: dashboard_id } }) => { + addOrUpdateDashboardCard({ + card_id: 3, + dashboard_id, + card: { + row: 0, + col: 0, + size_x: 24, + size_y: 10, + visualization_settings: {}, + }, + }); + + visitDashboardAndCreateTab({ dashboardId: dashboard_id }); + cy.findByRole("tab", { name: "Tab 1" }).click(); + + cy.get("circle").eq(0).click({ force: true }); + popover().findByText("Automatic insights…").click(); + popover().findByText("X-ray").click(); + cy.wait("@dataset", { timeout: 30000 }); + + // Ensure charts actually got rendered + cy.get("text.x-axis-label").contains("Created At"); + }); + }); }); diff --git a/frontend/src/metabase/dashboard/actions/data-fetching.js b/frontend/src/metabase/dashboard/actions/data-fetching.js index fefbd0912875824783d01433b02d6de74204e011..c1d1b02c6bb9b55990893743a8e7aa30224f516a 100644 --- a/frontend/src/metabase/dashboard/actions/data-fetching.js +++ b/frontend/src/metabase/dashboard/actions/data-fetching.js @@ -44,6 +44,7 @@ import { getDashboardType, fetchDataOrError, getDatasetQueryParams, + getCurrentTabDashboardCards, } from "../utils"; import { DASHBOARD_SLOW_TIMEOUT } from "../constants"; import { loadMetadataForDashboard } from "./metadata"; @@ -424,23 +425,14 @@ export const fetchDashboardCardData = createThunkAction( const dashboard = getDashboardComplete(getState()); const selectedTabId = getSelectedTabId(getState()); const dashcardIds = []; - - const promises = getAllDashboardCards(dashboard) + const promises = getCurrentTabDashboardCards(dashboard, selectedTabId) + .filter(({ dashcard }) => !isVirtualDashCard(dashcard)) .map(({ card, dashcard }) => { - if ( - isVirtualDashCard(dashcard) || - (selectedTabId !== undefined && - dashcard.dashboard_tab_id !== selectedTabId) - ) { - return; - } - dashcardIds.push(dashcard.id); return dispatch(fetchCardData(card, dashcard, options)).then(() => { return dispatch(updateLoadingTitle()); }); - }) - .filter(p => !!p); + }); dispatch(setDocumentTitle(t`0/${promises.length} loaded`)); diff --git a/frontend/src/metabase/dashboard/actions/tabs.ts b/frontend/src/metabase/dashboard/actions/tabs.ts index e9c9db2c93eb8f23f1279816ff4175010d3940a9..9312e58e600126025953f888578d75ab65caee58 100644 --- a/frontend/src/metabase/dashboard/actions/tabs.ts +++ b/frontend/src/metabase/dashboard/actions/tabs.ts @@ -11,6 +11,7 @@ import { DashboardTabId, } from "metabase-types/api"; import { DashboardState, TabDeletionId } from "metabase-types/store"; +import { INITIALIZE } from "metabase/dashboard/actions/core"; import { INITIAL_DASHBOARD_STATE } from "../constants"; @@ -307,6 +308,16 @@ export const tabsReducer = createReducer<DashboardState>( }, ); + builder.addCase< + string, + { type: string; payload?: { clearCache: boolean } } + >(INITIALIZE, (state, { payload: { clearCache = true } = {} }) => { + if (clearCache) { + state.selectedTabId = INITIAL_DASHBOARD_STATE.selectedTabId; + state.tabDeletions = INITIAL_DASHBOARD_STATE.tabDeletions; + } + }); + builder.addCase(initTabs, (state, { payload: { slug } }) => { const { prevTabs } = getPrevDashAndTabs({ state }); diff --git a/frontend/src/metabase/dashboard/utils.ts b/frontend/src/metabase/dashboard/utils.ts index d5d45fb7a31ab501b8a045ccda472921286d45e0..96486b9d2e969f42616afc23a157caca75ad40e2 100644 --- a/frontend/src/metabase/dashboard/utils.ts +++ b/frontend/src/metabase/dashboard/utils.ts @@ -7,7 +7,7 @@ import { getPermissionErrorMessage, } from "metabase/visualizations/lib/errors"; import { IS_EMBED_PREVIEW } from "metabase/lib/embed"; -import { +import type { Card, CardId, DashCardId, @@ -20,6 +20,7 @@ import { StructuredDatasetQuery, ActionDashboardCard, } from "metabase-types/api"; +import type { SelectedTabId } from "metabase-types/store"; import Question from "metabase-lib/Question"; import { isDateParameter, @@ -127,6 +128,17 @@ export function getAllDashboardCards(dashboard: Dashboard) { return results; } +export function getCurrentTabDashboardCards( + dashboard: Dashboard, + selectedTabId: SelectedTabId, +) { + return getAllDashboardCards(dashboard).filter( + ({ dashcard }) => + (dashcard.dashboard_tab_id == null && selectedTabId == null) || + dashcard.dashboard_tab_id === selectedTabId, + ); +} + export function hasDatabaseActionsEnabled(database: Database) { return database.settings?.["database-enable-actions"] ?? false; } diff --git a/frontend/src/metabase/dashboard/utils.unit.spec.ts b/frontend/src/metabase/dashboard/utils.unit.spec.ts index 8f17aa9abc1208aa84ee9b7f664ef644ae2de627..4e812a0e6be90ef96f81cbc9b327b7444817ba2b 100644 --- a/frontend/src/metabase/dashboard/utils.unit.spec.ts +++ b/frontend/src/metabase/dashboard/utils.unit.spec.ts @@ -1,5 +1,6 @@ import { fetchDataOrError, + getCurrentTabDashboardCards, getDashcardResultsError, getVisibleCardIds, hasDatabaseActionsEnabled, @@ -7,6 +8,7 @@ import { syncParametersAndEmbeddingParams, } from "metabase/dashboard/utils"; import { + createMockDashboard, createMockDashboardCardWithVirtualCard, createMockDashboardOrderedCard, createMockDatabase, @@ -284,4 +286,48 @@ describe("Dashboard utils", () => { ); }); }); + + describe("getCurrentTabDashboardCards", () => { + it("when selectedTabId=null returns cards with dashboard_tab_id=undefined", () => { + const selectedTabId = null; + const dashcard = createMockDashboardOrderedCard({ + dashboard_tab_id: undefined, + }); + const dashboard = createMockDashboard({ + ordered_cards: [dashcard], + }); + + expect( + getCurrentTabDashboardCards(dashboard, selectedTabId), + ).toStrictEqual([ + { + card: dashcard.card, + dashcard, + }, + ]); + }); + + it("returns cards from selected tab only", () => { + const selectedTabId = 1; + const visibleDashcard = createMockDashboardOrderedCard({ + dashboard_tab_id: 1, + }); + const hiddenDashcard = createMockDashboardOrderedCard({ + dashboard_tab_id: 2, + }); + + const dashboard = createMockDashboard({ + ordered_cards: [visibleDashcard, hiddenDashcard], + }); + + expect( + getCurrentTabDashboardCards(dashboard, selectedTabId), + ).toStrictEqual([ + { + card: visibleDashcard.card, + dashcard: visibleDashcard, + }, + ]); + }); + }); });