diff --git a/e2e/support/helpers/e2e-dashboard-helpers.ts b/e2e/support/helpers/e2e-dashboard-helpers.ts index 8e1885dc86a1d08cbad2c0c6373683f178e61da8..582964ffc8325f183a2c82e2f6acc6188e4b8ac5 100644 --- a/e2e/support/helpers/e2e-dashboard-helpers.ts +++ b/e2e/support/helpers/e2e-dashboard-helpers.ts @@ -6,7 +6,7 @@ import type { } from "metabase-types/api"; import { visitDashboard } from "./e2e-misc-helpers"; -import { menu, popover, sidebar } from "./e2e-ui-elements-helpers"; +import { menu, popover, sidebar, sidesheet } from "./e2e-ui-elements-helpers"; // Metabase utility functions for commonly-used patterns export function selectDashboardFilter( @@ -260,9 +260,12 @@ export function resizeDashboardCard({ }); } -export function toggleDashboardInfoSidebar() { +export function openDashboardInfoSidebar() { dashboardHeader().icon("info").click(); } +export function closeDashboardInfoSidebar() { + sidesheet().findByLabelText("Close").click(); +} export function openDashboardMenu() { dashboardHeader().findByLabelText("Move, trash, and more…").click(); diff --git a/e2e/test/scenarios/admin-2/api-keys.cy.spec.ts b/e2e/test/scenarios/admin-2/api-keys.cy.spec.ts index e678b56509796514f7cc61dc905acc289c8dbaf1..69fac0029fa37c7cb46ec7eb6da4c75fe54313ad 100644 --- a/e2e/test/scenarios/admin-2/api-keys.cy.spec.ts +++ b/e2e/test/scenarios/admin-2/api-keys.cy.spec.ts @@ -214,9 +214,10 @@ describe("scenarios > admin > settings > API keys", () => { visitDashboard(dashboardId); cy.findByTestId("dashboard-header").findByText("Test Dashboard"); cy.findByTestId("dashboard-header").icon("info").click(); - cy.findByTestId("sidebar-right").findByText( - "Test API Key One created this.", - ); + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); + cy.findByText("Test API Key One created this."); + }); }); }, ); @@ -256,7 +257,8 @@ describe("scenarios > admin > settings > API keys", () => { "Edited Dashboard Name", ); cy.findByTestId("dashboard-header").icon("info").click(); - cy.findByTestId("sidebar-right").within(() => { + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); cy.findByText("You created this."); cy.findByText( 'Test API Key One renamed this Dashboard from "Orders in a dashboard" to "Edited Dashboard Name".', diff --git a/e2e/test/scenarios/admin/performance/helpers/e2e-strategy-form-helpers.ts b/e2e/test/scenarios/admin/performance/helpers/e2e-strategy-form-helpers.ts index 813291bd5a0d791bae75e5c471a152582b25aa33..2570c11f6121847555d52feb586a46ec141bebb8 100644 --- a/e2e/test/scenarios/admin/performance/helpers/e2e-strategy-form-helpers.ts +++ b/e2e/test/scenarios/admin/performance/helpers/e2e-strategy-form-helpers.ts @@ -178,16 +178,6 @@ export const selectCacheStrategy = ({ } saveCacheStrategyForm({ strategyType: strategy.type, model: item?.model }); - - if (item?.model === "question") { - cy.findByLabelText("Close").click(); - } - - // Once dashboard sidesheets is merged, we can change this to use the same - // approach that we use for questions. - if (item?.model === "dashboard") { - cy.findByLabelText("More info").click(); - } }; export const disableCaching = ( diff --git a/e2e/test/scenarios/collections/revision-history.cy.spec.js b/e2e/test/scenarios/collections/revision-history.cy.spec.js index 076329b1744d434a9805b093f1728da6b71849d4..5e10477ff5996d743c301e23d03b7d3527170e00 100644 --- a/e2e/test/scenarios/collections/revision-history.cy.spec.js +++ b/e2e/test/scenarios/collections/revision-history.cy.spec.js @@ -9,9 +9,9 @@ import { openQuestionsSidebar, questionInfoButton, restore, - rightSidebar, saveDashboard, sidebar, + sidesheet, visitDashboard, visitQuestion, } from "e2e/support/helpers"; @@ -94,7 +94,8 @@ describe("revision history", () => { cy.wait(100); openRevisionHistory(); - rightSidebar().within(() => { + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); cy.findByText(/added a card/) .siblings("button") .should("not.exist"); @@ -206,9 +207,10 @@ function openRevisionHistory() { cy.get("main header").within(() => { cy.icon("info").click(); }); + cy.findByRole("tab", { name: "History" }).click(); cy.wait("@revisionHistory"); - rightSidebar().within(() => { + sidesheet().within(() => { cy.findByText("History"); cy.findByTestId("dashboard-history-list").should("be.visible"); }); diff --git a/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js b/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js index d2fb9357b8bd50f6ad5ecc4928b1d61955e844cd..ad97648a01ac801f5868cdc0d862867426c9743f 100644 --- a/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js +++ b/e2e/test/scenarios/dashboard-filters/dashboard-filters-auto-apply.cy.spec.js @@ -1,5 +1,6 @@ import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; import { + closeDashboardInfoSidebar, dashboardParametersContainer, describeWithSnowplow, editDashboard, @@ -8,14 +9,14 @@ import { expectNoBadSnowplowEvents, filterWidget, getDashboardCard, + openDashboardInfoSidebar, popover, resetSnowplow, restore, - rightSidebar, saveDashboard, setFilter, sidebar, - toggleDashboardInfoSidebar, + sidesheet, undoToast, visitDashboard, visitEmbeddedPage, @@ -93,12 +94,13 @@ describe( cy.log( "parameter values should be preserved when disabling auto applying filters", ); - toggleDashboardInfoSidebar(); - rightSidebar().within(() => { + openDashboardInfoSidebar(); + sidesheet().within(() => { cy.findByText(filterToggleLabel).click(); cy.wait("@updateDashboard"); cy.findByLabelText(filterToggleLabel).should("not.be.checked"); }); + closeDashboardInfoSidebar(); filterWidget().findByText("Gadget").should("be.visible"); getDashboardCard().findByText("Rows 1-4 of 53").should("be.visible"); @@ -126,11 +128,15 @@ describe( }); filterWidget().findByText("Widget").should("be.visible"); dashboardParametersContainer().button("Apply").should("be.visible"); - rightSidebar().within(() => { + + openDashboardInfoSidebar(); + sidesheet().within(() => { cy.findByText(filterToggleLabel).click(); cy.wait("@updateDashboard"); cy.findByLabelText(filterToggleLabel).should("be.checked"); }); + closeDashboardInfoSidebar(); + filterWidget().findByText("Widget").should("be.visible"); getDashboardCard().findByText("Rows 1-4 of 54").should("be.visible"); cy.get("@cardQuery.all").should("have.length", 4); @@ -143,11 +149,15 @@ describe( cy.findByText("Gadget").click(); cy.button("Update filter").click(); }); - rightSidebar().within(() => { + + openDashboardInfoSidebar(); + sidesheet().within(() => { cy.findByText(filterToggleLabel).click(); cy.wait("@updateDashboard"); cy.findByLabelText(filterToggleLabel).should("not.be.checked"); }); + closeDashboardInfoSidebar(); + filterWidget().findByText("2 selections").should("be.visible"); cy.get("@cardQuery.all").should("have.length", 5); @@ -230,19 +240,20 @@ describe( it("should handle toggling auto applying filters on and off", () => { openDashboard(); - toggleDashboardInfoSidebar(); + openDashboardInfoSidebar(); getDashboardCard().findByText("Rows 1-4 of 53").should("be.visible"); cy.log( "parameter with default value should still be applied after turning auto-apply filter off", ); - rightSidebar().within(() => { + sidesheet().within(() => { cy.findByLabelText(filterToggleLabel).should("be.checked"); cy.findByText(filterToggleLabel).click(); cy.wait("@updateDashboard"); cy.findByLabelText(filterToggleLabel).should("not.be.checked"); }); + closeDashboardInfoSidebar(); getDashboardCard().findByText("Rows 1-4 of 53").should("be.visible"); @@ -260,7 +271,8 @@ describe( cy.log( "should not use the default parameter after turning auto-apply filter on again since the parameter was manually updated", ); - rightSidebar().within(() => { + openDashboardInfoSidebar(); + sidesheet().within(() => { cy.findByLabelText(filterToggleLabel).should("not.be.checked"); cy.findAllByText(filterToggleLabel).click(); cy.wait("@updateDashboard"); @@ -282,10 +294,8 @@ describe( cy.wait("@updateDashboard"); }); - toggleDashboardInfoSidebar(); - rightSidebar() - .findByLabelText(filterToggleLabel) - .should("not.be.checked"); + openDashboardInfoSidebar(); + sidesheet().findByLabelText(filterToggleLabel).should("not.be.checked"); // Gadget const filterDefaultValue = FILTER_WITH_DEFAULT_VALUE.default[0]; filterWidget().findByText(filterDefaultValue).should("be.visible"); @@ -317,10 +327,9 @@ describe( cy.wait("@updateDashboard"); }); - toggleDashboardInfoSidebar(); - rightSidebar() - .findByLabelText(filterToggleLabel) - .should("not.be.checked"); + openDashboardInfoSidebar(); + sidesheet().findByLabelText(filterToggleLabel).should("not.be.checked"); + closeDashboardInfoSidebar(); filterWidget().findByText("Gadget").should("be.visible"); getDashboardCard().findByText("Rows 1-4 of 53").should("be.visible"); }); @@ -396,8 +405,8 @@ describe( openDashboard(); cy.wait("@cardQuery"); - toggleDashboardInfoSidebar(); - rightSidebar().findByLabelText(filterToggleLabel).should("be.disabled"); + openDashboardInfoSidebar(); + sidesheet().findByLabelText(filterToggleLabel).should("be.disabled"); }); it.skip("should not display a toast even when a dashboard takes longer than 15s to load", () => { @@ -557,8 +566,8 @@ describe( // so to make sure callback in `setTimeout` is called, we need to advance the clock using cy.tick(). cy.tick(); - toggleDashboardInfoSidebar(); - rightSidebar() + openDashboardInfoSidebar(); + sidesheet() .findByLabelText(filterToggleLabel) .should("not.be.checked"); filterWidget().findByText("Gadget").should("be.visible"); @@ -616,8 +625,8 @@ describeWithSnowplow("scenarios > dashboards > filters > auto apply", () => { openDashboard(); cy.wait("@cardQuery"); - toggleDashboardInfoSidebar(); - rightSidebar().within(() => { + openDashboardInfoSidebar(); + sidesheet().within(() => { expectGoodSnowplowEvents( NUMBERS_OF_GOOD_SNOWPLOW_EVENTS_BEFORE_DISABLING_AUTO_APPLY_FILTERS, ); @@ -635,8 +644,8 @@ describeWithSnowplow("scenarios > dashboards > filters > auto apply", () => { openDashboard(); cy.wait("@cardQuery"); - toggleDashboardInfoSidebar(); - rightSidebar().within(() => { + openDashboardInfoSidebar(); + sidesheet().within(() => { expectGoodSnowplowEvents( NUMBERS_OF_GOOD_SNOWPLOW_EVENTS_BEFORE_DISABLING_AUTO_APPLY_FILTERS, ); diff --git a/e2e/test/scenarios/dashboard/dashboard-management.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard-management.cy.spec.js index d4b0061f647f6b8d31f78f119c18051c36bae76f..3fd958db3b4d0da6c63cfdfa4c1c58a953bbdcd1 100644 --- a/e2e/test/scenarios/dashboard/dashboard-management.cy.spec.js +++ b/e2e/test/scenarios/dashboard/dashboard-management.cy.spec.js @@ -4,17 +4,18 @@ import { USERS } from "e2e/support/cypress_data"; import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data"; import { appBar, + closeDashboardInfoSidebar, collectionOnTheGoModal, entityPickerModal, getDashboardCard, modal, navigationSidebar, + openDashboardInfoSidebar, openDashboardMenu, openNavigationSidebar, popover, restore, - rightSidebar, - toggleDashboardInfoSidebar, + sidesheet, undoToast, visitDashboard, } from "e2e/support/helpers"; @@ -71,12 +72,13 @@ describe("managing dashboard from the dashboard's edit menu", () => { assertOnRequest("updateDashboard"); assertOnRequest("getDashboard"); - toggleDashboardInfoSidebar(); + openDashboardInfoSidebar(); - rightSidebar() + sidesheet() .findByPlaceholderText("Add description") .type("Foo") .blur(); + closeDashboardInfoSidebar(); assertOnRequest("updateDashboard"); assertOnRequest("getDashboard"); diff --git a/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js index 613aa2b2dad647a6c5c2b374667b8427d351a371..38ed2ae30639cf9c6b6e411ece3f2cbc65016b7f 100644 --- a/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js +++ b/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js @@ -11,6 +11,7 @@ import { addTextBox, appBar, cartesianChartCircle, + closeDashboardInfoSidebar, createDashboard, createDashboardWithTabs, dashboardGrid, @@ -27,18 +28,18 @@ import { main, modal, navigationSidebar, + openDashboardInfoSidebar, openQuestionsSidebar, popover, queryBuilderHeader, removeDashboardCard, restore, - rightSidebar, saveDashboard, setFilter, setTokenFeatures, showDashboardCardActions, sidebar, - toggleDashboardInfoSidebar, + sidesheet, undo, undoToast, updateDashboardCards, @@ -329,58 +330,82 @@ describe("issue 16559", () => { "should always show the most recent revision (metabase#16559)", { tags: "@flaky" }, () => { - toggleDashboardInfoSidebar(); - - cy.log("Dashboard creation"); - cy.findByTestId("dashboard-history-list") - .findAllByRole("listitem") - .eq(0) - .findByText("You created this.") - .should("be.visible"); + openDashboardInfoSidebar(); + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); + cy.log("Dashboard creation"); + cy.findByTestId("dashboard-history-list") + .findAllByRole("listitem") + .eq(0) + .findByText("You created this.") + .should("be.visible"); + }); + closeDashboardInfoSidebar(); cy.log("Edit dashboard"); editDashboard(); openQuestionsSidebar(); sidebar().findByText("Orders, Count").click(); cy.button("Save").click(); - toggleDashboardInfoSidebar(); - cy.findByTestId("dashboard-history-list") - .findAllByRole("listitem") - .eq(0) - .findByText("You added a card.") - .should("be.visible"); + + openDashboardInfoSidebar(); + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); + cy.findByTestId("dashboard-history-list") + .findAllByRole("listitem") + .eq(0) + .findByText("You added a card.") + .should("be.visible"); + }); + closeDashboardInfoSidebar(); cy.log("Change dashboard name"); cy.findByTestId("dashboard-name-heading") .click() .type(" modified") .blur(); - cy.findByTestId("dashboard-history-list") - .findAllByRole("listitem") - .eq(0) - .findByText( - 'You renamed this Dashboard from "16559 Dashboard" to "16559 Dashboard modified".', - ) - .should("be.visible"); - - cy.log("Add description"); - cy.findByPlaceholderText("Add description") - .click() - .type("16559 description") - .blur(); - cy.findByTestId("dashboard-history-list") - .findAllByRole("listitem") - .eq(0) - .findByText("You added a description.") - .should("be.visible"); - - cy.log("Toggle auto-apply filters"); - rightSidebar().findByText("Auto-apply filters").click(); - cy.findByTestId("dashboard-history-list") - .findAllByRole("listitem") - .eq(0) - .findByText("You set auto apply filters to false.") - .should("be.visible"); + + openDashboardInfoSidebar(); + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); + + cy.findByTestId("dashboard-history-list") + .findAllByRole("listitem") + .eq(0) + .findByText( + 'You renamed this Dashboard from "16559 Dashboard" to "16559 Dashboard modified".', + ) + .should("be.visible"); + + cy.log("Add description"); + cy.findByRole("tab", { name: "Overview" }).click(); + + cy.findByPlaceholderText("Add description") + .click() + .type("16559 description") + .blur(); + + cy.findByRole("tab", { name: "History" }).click(); + + cy.findByTestId("dashboard-history-list") + .findAllByRole("listitem") + .eq(0) + .findByText("You added a description.") + .should("be.visible"); + + cy.log("Toggle auto-apply filters"); + + cy.findByRole("tab", { name: "Overview" }).click(); + cy.findByText("Auto-apply filters").click(); + cy.findByRole("tab", { name: "History" }).click(); + + cy.findByTestId("dashboard-history-list") + .findAllByRole("listitem") + .eq(0) + .findByText("You set auto apply filters to false.") + .should("be.visible"); + }); + closeDashboardInfoSidebar(); cy.log("Move dashboard to another collection"); dashboardHeader().icon("ellipsis").click(); @@ -389,11 +414,16 @@ describe("issue 16559", () => { cy.findByText("First collection").click(); cy.button("Move").click(); }); - cy.findByTestId("dashboard-history-list") - .findAllByRole("listitem") - .eq(0) - .findByText("You moved this Dashboard to First collection.") - .should("be.visible"); + + openDashboardInfoSidebar(); + sidesheet().within(() => { + cy.findByRole("tab", { name: "History" }).click(); + cy.findByTestId("dashboard-history-list") + .findAllByRole("listitem") + .eq(0) + .findByText("You moved this Dashboard to First collection.") + .should("be.visible"); + }); }, ); }); diff --git a/e2e/test/scenarios/dashboard/dashboard.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard.cy.spec.js index 609c83e592417bc22de5dc13d3c2e16dd806d698..6107bfc6e352c3b66e72e7138f7d9250ba9e9421 100644 --- a/e2e/test/scenarios/dashboard/dashboard.cy.spec.js +++ b/e2e/test/scenarios/dashboard/dashboard.cy.spec.js @@ -13,6 +13,7 @@ import { appBar, assertDashboardFixedWidth, assertDashboardFullWidth, + closeDashboardInfoSidebar, closeNavigationSidebar, collectionOnTheGoModal, commandPalette, @@ -32,6 +33,7 @@ import { getDashboardCards, getTextCardDetails, modal, + openDashboardInfoSidebar, openDashboardMenu, openProductsTable, openQuestionsSidebar, @@ -41,14 +43,13 @@ import { removeDashboardCard, resetSnowplow, restore, - rightSidebar, saveDashboard, selectDashboardFilter, setFilter, setTokenFeatures, showDashboardCardActions, sidebar, - toggleDashboardInfoSidebar, + sidesheet, updateDashboardCards, visitDashboard, } from "e2e/support/helpers"; @@ -403,7 +404,7 @@ describe("scenarios > dashboard", () => { }); }); - context("title and description", () => { + describe("title and description", () => { beforeEach(() => { cy.intercept("GET", "/api/dashboard/*").as("getDashboard"); cy.intercept( @@ -422,9 +423,9 @@ describe("scenarios > dashboard", () => { cy.wait("@updateDashboard"); cy.wait("@getDashboard"); - toggleDashboardInfoSidebar(); + openDashboardInfoSidebar(); - rightSidebar() + sidesheet() .findByPlaceholderText("Add description") .type(newDescription) .blur(); @@ -439,8 +440,9 @@ describe("scenarios > dashboard", () => { cy.wait("@getDashboard"); dashboardHeader().findByDisplayValue(newTitle); - toggleDashboardInfoSidebar(); - sidebar().findByText(newDescription); + openDashboardInfoSidebar(); + sidesheet().findByText(newDescription); + closeDashboardInfoSidebar(); cy.log("should not call unnecessary API requests (metabase#31721)"); cy.get("@updateDashboardSpy").should("have.callCount", 2); @@ -451,8 +453,11 @@ describe("scenarios > dashboard", () => { cy.get("@updateDashboardSpy").should("have.callCount", 2); cy.log("Should revert the description change if escaped"); - sidebar().findByText(newDescription).type("Baz{esc}"); - sidebar().findByText(newDescription); + openDashboardInfoSidebar(); + sidesheet().within(() => { + cy.findByText(newDescription).type("Baz{esc}"); + cy.findByText(newDescription); + }); cy.get("@updateDashboardSpy").should("have.callCount", 2); }); @@ -478,7 +483,7 @@ describe("scenarios > dashboard", () => { .findByText(/^Edited a few seconds ago/) .click(); - rightSidebar() + sidesheet() .findByPlaceholderText("Add description") .type(newDescription) .blur(); @@ -487,6 +492,7 @@ describe("scenarios > dashboard", () => { // This might be a bug! We're applying the description while still in the edit mode! // OTOH, the title is preserved only on save. cy.wait("@updateDashboard"); + closeDashboardInfoSidebar(); saveDashboard(); cy.wait("@updateDashboard"); @@ -494,19 +500,19 @@ describe("scenarios > dashboard", () => { }); it("should not have markdown content overflow the description area (metabase#31326)", () => { - toggleDashboardInfoSidebar(); + openDashboardInfoSidebar(); const testMarkdownContent = "# Heading 1{enter}{enter}**bold** https://www.metabase.com/community_posts/how-to-measure-the-success-of-new-product-features-and-why-it-is-important{enter}{enter}{enter}{enter}This is my description. "; - rightSidebar() + sidesheet() .findByPlaceholderText("Add description") .type(testMarkdownContent, { delay: 0 }) .blur(); cy.wait("@updateDashboard"); - rightSidebar().within(() => { + sidesheet().within(() => { cy.log("Markdown content should not be bigger than its container"); cy.findByTestId("editable-text").then($markdown => { const el = $markdown[0]; @@ -1266,10 +1272,8 @@ describeEE("scenarios > dashboard > caching", () => { openSidebarCacheStrategyForm("dashboard"); - rightSidebar().within(() => { - cy.findByRole("heading", { name: /Caching settings/ }).should( - "be.visible", - ); + sidesheet().within(() => { + cy.findByText(/Caching settings/).should("be.visible"); durationRadioButton().click(); cy.findByLabelText("Cache results for this many hours").type("48"); cy.findByRole("button", { name: /Save/ }).click(); @@ -1293,20 +1297,18 @@ describeEE("scenarios > dashboard > caching", () => { openSidebarCacheStrategyForm("dashboard"); - rightSidebar().within(() => { - cy.findByRole("heading", { name: /Caching settings/ }).should( - "be.visible", - ); + sidesheet().within(() => { + cy.findByText(/Caching settings/).should("be.visible"); cy.findByRole("button", { name: /Clear cache for this dashboard/, }).click(); }); - modal().within(() => { + cy.findByTestId("confirm-modal").within(() => { cy.findByRole("button", { name: /Clear cache/ }).click(); }); cy.wait("@invalidateCache"); - rightSidebar().within(() => { + sidesheet().within(() => { cy.findByText("Cache cleared").should("be.visible"); }); }); diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/DashboardStrategySidebar.styled.tsx b/enterprise/frontend/src/metabase-enterprise/caching/components/DashboardStrategySidebar.styled.tsx deleted file mode 100644 index 59b10af52db504c3befa2569a166ffc069acfa54..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/caching/components/DashboardStrategySidebar.styled.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import styled from "@emotion/styled"; - -import { - FormBox, - StyledFormButtonsGroup, -} from "metabase/admin/performance/components/StrategyForm.styled"; -import { Group } from "metabase/ui"; - -export const DashboardStrategySidebarBody = styled(Group)` - display: flex; - flex-flow: column nowrap; - height: 100%; - ${StyledFormButtonsGroup} { - border-top: 1px solid var(--mb-color-border); - position: sticky; - bottom: 0; - } - ${FormBox} { - border-bottom: 0 !important; - } -`; diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/DashboardStrategySidebar.tsx b/enterprise/frontend/src/metabase-enterprise/caching/components/DashboardStrategySidebar.tsx deleted file mode 100644 index e4150df74cbed3ac9378fa897bbdf593cac69cb0..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/caching/components/DashboardStrategySidebar.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useCallback, useMemo } from "react"; -import type { InjectedRouter, Route } from "react-router"; -import { withRouter } from "react-router"; -import _ from "underscore"; - -import { StrategyForm } from "metabase/admin/performance/components/StrategyForm"; -import { useCacheConfigs } from "metabase/admin/performance/hooks/useCacheConfigs"; -import { useConfirmIfFormIsDirty } from "metabase/admin/performance/hooks/useConfirmIfFormIsDirty"; -import { useSaveStrategy } from "metabase/admin/performance/hooks/useSaveStrategy"; -import { DelayedLoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper/DelayedLoadingAndErrorWrapper"; -import type { DashboardSidebarPageProps } from "metabase/dashboard/components/DashboardInfoSidebar"; -import { color } from "metabase/lib/colors"; -import { Button, Flex, Icon, Title } from "metabase/ui"; -import type { CacheStrategy, CacheableModel } from "metabase-types/api"; - -import { DashboardStrategySidebarBody } from "./DashboardStrategySidebar.styled"; - -const configurableModels: CacheableModel[] = ["dashboard"]; - -const DashboardStrategySidebar_Base = ({ - dashboard, - setPage, - router, - route, -}: DashboardSidebarPageProps & { - router: InjectedRouter; - route: Route; -}) => { - if (typeof dashboard.id === "string") { - throw new Error("This dashboard has an invalid id"); - } - const dashboardId: number = dashboard.id; - const { configs, setConfigs, loading, error } = useCacheConfigs({ - configurableModels, - id: dashboardId, - }); - - const { savedStrategy, filteredConfigs } = useMemo(() => { - const targetConfig = _.findWhere(configs, { model_id: dashboardId }); - const savedStrategy = targetConfig?.strategy; - const filteredConfigs = _.compact([targetConfig]); - return { savedStrategy, filteredConfigs }; - }, [configs, dashboardId]); - - const saveStrategy = useSaveStrategy( - dashboardId, - filteredConfigs, - setConfigs, - "dashboard", - ); - const saveAndCloseSidebar = useCallback( - async (values: CacheStrategy) => { - await saveStrategy(values); - setPage("default"); - }, - [saveStrategy, setPage], - ); - - const closeSidebar = useCallback(async () => { - setPage("default"); - }, [setPage]); - - const { - askBeforeDiscardingChanges, - confirmationModal, - isStrategyFormDirty, - setIsStrategyFormDirty, - } = useConfirmIfFormIsDirty(router, route); - - const goBack = () => setPage("default"); - - const headingId = "dashboard-sidebar-caching-settings-heading"; - - return ( - <DashboardStrategySidebarBody - align="flex-start" - spacing="md" - aria-labelledby={headingId} - > - <Flex align="center"> - <BackButton - onClick={() => { - isStrategyFormDirty ? askBeforeDiscardingChanges(goBack) : goBack(); - }} - /> - <Title order={2} id={headingId}> - Caching settings - </Title> - </Flex> - <DelayedLoadingAndErrorWrapper - loadingMessages={[]} - loading={loading} - error={error} - > - <StrategyForm - targetId={dashboardId} - targetModel="dashboard" - targetName={dashboard.name} - isInSidebar - setIsDirty={setIsStrategyFormDirty} - saveStrategy={saveAndCloseSidebar} - savedStrategy={savedStrategy} - shouldAllowInvalidation - shouldShowName={false} - onReset={closeSidebar} - /> - </DelayedLoadingAndErrorWrapper> - {confirmationModal} - </DashboardStrategySidebarBody> - ); -}; - -export const DashboardStrategySidebar = withRouter( - DashboardStrategySidebar_Base, -); - -const BackButton = ({ onClick }: { onClick: () => void }) => ( - <Button - lh={0} - style={{ marginInlineStart: "1rem" }} - variant="subtle" - onClick={onClick} - > - <Icon name="chevronleft" color={color("text-dark")} /> - </Button> -); diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/SidebarCacheForm.tsx b/enterprise/frontend/src/metabase-enterprise/caching/components/SidebarCacheForm.tsx index 36c4d9ae1386f6b0240b191b923c98ab972c2652..dfbaee8aba6870bfd1118c549d0770bdb268f670 100644 --- a/enterprise/frontend/src/metabase-enterprise/caching/components/SidebarCacheForm.tsx +++ b/enterprise/frontend/src/metabase-enterprise/caching/components/SidebarCacheForm.tsx @@ -61,7 +61,9 @@ const SidebarCacheForm_Base = ({ onBack={() => isStrategyFormDirty ? askBeforeDiscardingChanges(onBack) : onBack() } - onClose={onClose} + onClose={() => { + isStrategyFormDirty ? askBeforeDiscardingChanges(onClose) : onClose(); + }} > <Stack align="space-between" diff --git a/enterprise/frontend/src/metabase-enterprise/caching/index.tsx b/enterprise/frontend/src/metabase-enterprise/caching/index.tsx index 30de2d38fd1c61f3c1e524cd50c8b4074bbc4cad..e96725b0367b07d9739fd36f40c2778b6c84252b 100644 --- a/enterprise/frontend/src/metabase-enterprise/caching/index.tsx +++ b/enterprise/frontend/src/metabase-enterprise/caching/index.tsx @@ -2,7 +2,6 @@ import { PLUGIN_CACHING } from "metabase/plugins"; import { hasPremiumFeature } from "metabase-enterprise/settings"; import { DashboardAndQuestionCachingTab } from "./components/DashboardAndQuestionCachingTab"; -import { DashboardStrategySidebar } from "./components/DashboardStrategySidebar"; import { GranularControlsExplanation } from "./components/GranularControlsExplanation"; import { InvalidateNowButton } from "./components/InvalidateNowButton"; import { SidebarCacheForm } from "./components/SidebarCacheForm"; @@ -22,7 +21,6 @@ if (hasPremiumFeature("cache_granular_controls")) { PLUGIN_CACHING.canOverrideRootStrategy = true; PLUGIN_CACHING.GranularControlsExplanation = GranularControlsExplanation; PLUGIN_CACHING.InvalidateNowButton = InvalidateNowButton; - PLUGIN_CACHING.DashboardStrategySidebar = DashboardStrategySidebar; PLUGIN_CACHING.SidebarCacheSection = SidebarCacheSection; PLUGIN_CACHING.SidebarCacheForm = SidebarCacheForm; PLUGIN_CACHING.strategies = { diff --git a/frontend/src/metabase/common/components/Sidesheet/Sidesheet.tsx b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.tsx index 001bc5436dc6bd27ce87f732ae1124e76290463c..cfc43a9483624610fdd7d80d474bfa5ac2f7329d 100644 --- a/frontend/src/metabase/common/components/Sidesheet/Sidesheet.tsx +++ b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.tsx @@ -5,19 +5,19 @@ import { Modal, Stack } from "metabase/ui"; import Styles from "./sidesheet.module.css"; -type Size = "xs" | "sm" | "md" | "lg" | "xl" | "auto"; +export type SidesheetSize = "xs" | "sm" | "md" | "lg" | "xl" | "auto"; interface SidesheetProps { title?: React.ReactNode; isOpen: boolean; onClose: () => void; - size?: Size; + size?: SidesheetSize; children: React.ReactNode; /** use this if you want to enable interior scrolling of tab panels */ removeBodyPadding?: boolean; } -const sizes: Record<Size, string> = { +const sizes: Record<SidesheetSize, string> = { xs: "20rem", sm: "30rem", md: "40rem", diff --git a/frontend/src/metabase/common/components/Sidesheet/SidesheetSubPage.tsx b/frontend/src/metabase/common/components/Sidesheet/SidesheetSubPage.tsx index 9be8a9cadd7ec21c4492a1a7e64c9dcf44633d66..09ea1724dc5c90c50414e1738b48a62ee0d9ccea 100644 --- a/frontend/src/metabase/common/components/Sidesheet/SidesheetSubPage.tsx +++ b/frontend/src/metabase/common/components/Sidesheet/SidesheetSubPage.tsx @@ -2,7 +2,7 @@ import type React from "react"; import { Button, Flex, Icon, Title } from "metabase/ui"; -import { Sidesheet } from "./Sidesheet"; +import { Sidesheet, type SidesheetSize } from "./Sidesheet"; interface SidesheetSubPageTitleProps { title: React.ReactNode; @@ -15,6 +15,7 @@ interface SidesheetSubPageProps { onClose: () => void; onBack: () => void; children: React.ReactNode; + size?: SidesheetSize; } export const SidesheetSubPageTitle = ({ @@ -37,11 +38,13 @@ export const SidesheetSubPage = ({ onBack, children, isOpen, + size, }: SidesheetSubPageProps) => ( <Sidesheet isOpen={isOpen} title={<SidesheetSubPageTitle title={title} onClick={onBack} />} onClose={onClose} + size={size} > {children} </Sidesheet> diff --git a/frontend/src/metabase/core/components/EditableText/EditableText.tsx b/frontend/src/metabase/core/components/EditableText/EditableText.tsx index efdea791c6e54f92b08dc715040b393650d3ecb2..6c6860fe2d971a55642e6e5542738590fd75183a 100644 --- a/frontend/src/metabase/core/components/EditableText/EditableText.tsx +++ b/frontend/src/metabase/core/components/EditableText/EditableText.tsx @@ -101,6 +101,7 @@ const EditableText = forwardRef(function EditableText( const handleKeyDown = useCallback( (event: KeyboardEvent<HTMLTextAreaElement>) => { if (event.key === "Escape") { + event.stopPropagation(); // don't close modal setInputValue(submitValue); submitOnBlur.current = false; event.currentTarget.blur(); diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeaderView.tsx b/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeaderView.tsx index fb5f9f9c49d55bbd1491b372aea075ecdee0df0f..c16bf2213af9a04108951ed2e2809c86b8bf3bb9 100644 --- a/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeaderView.tsx +++ b/frontend/src/metabase/dashboard/components/DashboardHeader/DashboardHeaderView.tsx @@ -18,6 +18,7 @@ import { getCanResetFilters, getIsEditing, getIsHeaderVisible, + getIsShowDashboardInfoSidebar, getIsSidebarOpen, } from "metabase/dashboard/selectors"; import type { @@ -85,6 +86,8 @@ export function DashboardHeaderView({ const canResetFilters = useSelector(getCanResetFilters); const isSidebarOpen = useSelector(getIsSidebarOpen); + const isInfoSidebarOpen = useSelector(getIsShowDashboardInfoSidebar); + const isDashboardHeaderVisible = useSelector(getIsHeaderVisible); const isAnalyticsDashboard = isInstanceAnalyticsCollection(collection); @@ -163,7 +166,7 @@ export function DashboardHeaderView({ )} <HeaderContainer isFixedWidth={dashboard?.width === "fixed"} - isSidebarOpen={isSidebarOpen} + offsetSidebar={isSidebarOpen && !isInfoSidebarOpen} > {isDashboardHeaderVisible && ( <HeaderRow diff --git a/frontend/src/metabase/dashboard/components/DashboardHeaderView.styled.tsx b/frontend/src/metabase/dashboard/components/DashboardHeaderView.styled.tsx index dbd7fef4e3545309f95d50aec9e951786be79309..d8b779005c64e243482b67578e66858258d6ce32 100644 --- a/frontend/src/metabase/dashboard/components/DashboardHeaderView.styled.tsx +++ b/frontend/src/metabase/dashboard/components/DashboardHeaderView.styled.tsx @@ -40,12 +40,12 @@ export const HeaderFixedWidthContainer = styled( `; export const HeaderContainer = styled.div<{ - isSidebarOpen: boolean; + offsetSidebar: boolean; isFixedWidth: boolean; }>` ${props => props.isFixedWidth && - props.isSidebarOpen && + props.offsetSidebar && css` margin-right: ${SIDEBAR_WIDTH}px; `} diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css new file mode 100644 index 0000000000000000000000000000000000000000..70613d52e255e14ce983454bafd680171ffefea6 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.module.css @@ -0,0 +1,5 @@ +.EditableTextContainer { + max-height: 19rem; + overflow: auto; + line-height: 1.38rem; /* magic number to keep line-height from changing in edit mode */ +} diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.styled.tsx b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.styled.tsx deleted file mode 100644 index 7401b9159368863d3ff70d106b4ef1ab1cc2b94d..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.styled.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; - -import EditableText from "metabase/core/components/EditableText"; -import FormField from "metabase/core/components/FormField/FormField"; -import { breakpointMaxSmall } from "metabase/styled-components/theme"; - -import { SIDEBAR_WIDTH } from "../Sidebar"; - -export const DashboardInfoSidebarRoot = styled.aside` - width: ${SIDEBAR_WIDTH}px; - min-width: ${SIDEBAR_WIDTH}px; - background: var(--mb-color-bg-white); - border-left: 1px solid var(--mb-color-border); - align-self: stretch; - box-sizing: border-box; - display: flex; - flex-direction: column; - - ${breakpointMaxSmall} { - position: absolute; - right: 0; - z-index: 2; - height: auto; - border-bottom: 1px solid var(--mb-color-border); - } -`; - -export const HistoryHeader = styled.h3` - margin-bottom: 1rem; -`; - -export const ContentSection = styled.div` - padding: 2rem 0; - border-bottom: 1px solid var(--mb-color-border); - - &:first-of-type { - padding-top: 1.5rem; - } - - &:last-of-type { - border-bottom: none; - } - - ${EditableText.Root} { - font-size: 1rem; - line-height: 1.4rem; - margin-left: -0.3rem; - - h1 { - line-height: 1em; - } - } - - ${FormField.Root}:last-child { - margin-bottom: 0; - } -`; - -export const DescriptionHeader = styled.h3` - margin-bottom: 0.5rem; -`; - -export const EditableDescription = styled(EditableText)<{ hasError?: boolean }>` - ${props => - props.hasError && - css` - border-color: var(--mb-color-error); - - &:hover { - border-color: var(--mb-color-error); - } - `} -`; diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx index 13fa27b33cc47c612053119835cc79fe90bd6775..fec2025b3ccd7d0e604047db0429baf646d3a919 100644 --- a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.tsx @@ -1,11 +1,19 @@ -import type { Dispatch, FocusEvent, SetStateAction } from "react"; +import type { FocusEvent, SetStateAction } from "react"; import { useCallback, useState } from "react"; +import { useMount } from "react-use"; import { t } from "ttag"; import ErrorBoundary from "metabase/ErrorBoundary"; +import { + Sidesheet, + SidesheetCard, + SidesheetTabPanelContainer, +} from "metabase/common/components/Sidesheet"; +import SidesheetS from "metabase/common/components/Sidesheet/sidesheet.module.css"; import { Timeline } from "metabase/common/components/Timeline"; import { getTimelineEvents } from "metabase/common/components/Timeline/utils"; import { useRevisionListQuery } from "metabase/common/hooks"; +import EditableText from "metabase/core/components/EditableText"; import { revertToRevision, toggleAutoApplyFilters, @@ -17,16 +25,15 @@ import { useUniqueId } from "metabase/hooks/use-unique-id"; import { useDispatch, useSelector } from "metabase/lib/redux"; import { PLUGIN_CACHING } from "metabase/plugins"; import { getUser } from "metabase/selectors/user"; -import { Stack, Switch, Text } from "metabase/ui"; -import type { Dashboard } from "metabase-types/api"; +import { Stack, Switch, Tabs, Text } from "metabase/ui"; +import type { + CacheableDashboard, + Dashboard, + Revision, + User, +} from "metabase-types/api"; -import { - ContentSection, - DashboardInfoSidebarRoot, - DescriptionHeader, - EditableDescription, - HistoryHeader, -} from "./DashboardInfoSidebar.styled"; +import DashboardInfoSidebarS from "./DashboardInfoSidebar.module.css"; interface DashboardInfoSidebarProps { dashboard: Dashboard; @@ -34,49 +41,29 @@ interface DashboardInfoSidebarProps { attribute: Key, value: Dashboard[Key], ) => void; + onClose: () => void; +} + +enum Tab { + Overview = "overview", + History = "history", } export function DashboardInfoSidebar({ dashboard, setDashboardAttribute, + onClose, }: DashboardInfoSidebarProps) { + const [isOpen, setIsOpen] = useState(false); const [page, setPage] = useState<"default" | "caching">("default"); - return ( - <DashboardInfoSidebarRoot - style={{ padding: page === "default" ? "0 2rem 0.5rem" : "1rem 0 0 0" }} - data-testid="sidebar-right" - > - <ErrorBoundary> - {page === "default" && ( - <DashboardInfoSidebarBody - dashboard={dashboard} - setDashboardAttribute={setDashboardAttribute} - setPage={setPage} - /> - )} - {page === "caching" && ( - <PLUGIN_CACHING.DashboardStrategySidebar - dashboard={dashboard} - setPage={setPage} - /> - )} - </ErrorBoundary> - </DashboardInfoSidebarRoot> - ); -} - -export type DashboardSidebarPageProps = { - dashboard: Dashboard; - setPage: Dispatch<SetStateAction<"default" | "caching">>; - setDashboardAttribute: DashboardInfoSidebarProps["setDashboardAttribute"]; -}; + useMount(() => { + // this component is not rendered until it is "open" + // but we want to set isOpen after it mounts to get + // pretty animations + setIsOpen(true); + }); -const DashboardInfoSidebarBody = ({ - dashboard, - setDashboardAttribute, - setPage, -}: DashboardSidebarPageProps) => { const [descriptionError, setDescriptionError] = useState<string | null>(null); const { data: revisions } = useRevisionListQuery({ @@ -107,6 +94,91 @@ const DashboardInfoSidebarBody = ({ [], ); + const canWrite = dashboard.can_write && !dashboard.archived; + const showCaching = canWrite && PLUGIN_CACHING.isGranularCachingEnabled(); + + if (page === "caching") { + return ( + <PLUGIN_CACHING.SidebarCacheForm + item={dashboard as CacheableDashboard} + model="dashboard" + onBack={() => setPage("default")} + onClose={onClose} + pt="md" + /> + ); + } + + return ( + <div data-testid="sidebar-right"> + <ErrorBoundary> + <Sidesheet + isOpen={isOpen} + title={t`Info`} + onClose={onClose} + removeBodyPadding + size="md" + > + <Tabs + defaultValue={Tab.Overview} + className={SidesheetS.FlexScrollContainer} + > + <Tabs.List mx="lg"> + <Tabs.Tab value={Tab.Overview}>{t`Overview`}</Tabs.Tab> + <Tabs.Tab value={Tab.History}>{t`History`}</Tabs.Tab> + </Tabs.List> + <SidesheetTabPanelContainer> + <Tabs.Panel value={Tab.Overview}> + <OverviewTab + dashboard={dashboard} + handleDescriptionChange={handleDescriptionChange} + handleDescriptionBlur={handleDescriptionBlur} + descriptionError={descriptionError} + setDescriptionError={setDescriptionError} + canWrite={canWrite} + setPage={setPage} + showCaching={showCaching} + /> + </Tabs.Panel> + <Tabs.Panel value={Tab.History}> + <HistoryTab + canWrite={canWrite} + revisions={revisions} + currentUser={currentUser} + /> + </Tabs.Panel> + </SidesheetTabPanelContainer> + </Tabs> + </Sidesheet> + </ErrorBoundary> + </div> + ); +} + +const OverviewTab = ({ + dashboard, + handleDescriptionChange, + handleDescriptionBlur, + descriptionError, + setDescriptionError, + canWrite, + setPage, + showCaching, +}: { + dashboard: Dashboard; + handleDescriptionChange: (description: string) => void; + handleDescriptionBlur: (event: FocusEvent<HTMLTextAreaElement>) => void; + descriptionError: string | null; + setDescriptionError: (error: string | null) => void; + canWrite: boolean; + setPage: ( + page: "default" | "caching" | SetStateAction<"default" | "caching">, + ) => void; + showCaching: boolean; +}) => { + const isCacheable = isDashboardCacheable(dashboard); + const autoApplyFilterToggleId = useUniqueId(); + const dispatch = useDispatch(); const handleToggleAutoApplyFilters = useCallback( (isAutoApplyingFilters: boolean) => { dispatch(toggleAutoApplyFilters(isAutoApplyingFilters)); @@ -114,70 +186,75 @@ const DashboardInfoSidebarBody = ({ [dispatch], ); - const autoApplyFilterToggleId = useUniqueId(); - const canWrite = dashboard.can_write && !dashboard.archived; - const isCacheable = isDashboardCacheable(dashboard); - - const showCaching = canWrite && PLUGIN_CACHING.isGranularCachingEnabled(); - return ( - <> - <ContentSection> - <DescriptionHeader>{t`About`}</DescriptionHeader> - <EditableDescription - initialValue={dashboard.description} - isDisabled={!canWrite} - onChange={handleDescriptionChange} - onFocus={() => setDescriptionError("")} - onBlur={handleDescriptionBlur} - isOptional - isMultiline - isMarkdown - hasError={!!descriptionError} - placeholder={t`Add description`} - key={`dashboard-description-${dashboard.description}`} - style={{ fontSize: ".875rem" }} - /> + <Stack spacing="lg"> + <SidesheetCard title={t`Description`} pb="md"> + <div className={DashboardInfoSidebarS.EditableTextContainer}> + <EditableText + initialValue={dashboard.description} + isDisabled={!canWrite} + onChange={handleDescriptionChange} + onFocus={() => setDescriptionError("")} + onBlur={handleDescriptionBlur} + isOptional + isMultiline + isMarkdown + placeholder={t`Add description`} + /> + </div> {!!descriptionError && ( <Text color="error" size="xs" mt="xs"> {descriptionError} </Text> )} - </ContentSection> + </SidesheetCard> {!dashboard.archived && ( - <ContentSection> - <Stack spacing="md"> - <Switch - disabled={!canWrite} - label={t`Auto-apply filters`} - labelPosition="left" - variant="stretch" - size="sm" - id={autoApplyFilterToggleId} - checked={dashboard.auto_apply_filters} - onChange={e => handleToggleAutoApplyFilters(e.target.checked)} - /> - {showCaching && isCacheable && ( - <PLUGIN_CACHING.SidebarCacheSection - model="dashboard" - item={dashboard} - setPage={setPage} - /> - )} - </Stack> - </ContentSection> + <SidesheetCard> + <Switch + disabled={!canWrite} + label={t`Auto-apply filters`} + labelPosition="left" + variant="stretch" + size="sm" + id={autoApplyFilterToggleId} + checked={dashboard.auto_apply_filters} + onChange={e => handleToggleAutoApplyFilters(e.target.checked)} + /> + </SidesheetCard> )} - <ContentSection> - <HistoryHeader>{t`History`}</HistoryHeader> - <Timeline - events={getTimelineEvents({ revisions, currentUser })} - data-testid="dashboard-history-list" - revert={revision => dispatch(revertToRevision(revision))} - canWrite={canWrite} - /> - </ContentSection> - </> + {showCaching && isCacheable && ( + <SidesheetCard title={t`Caching`} pb="md"> + <PLUGIN_CACHING.SidebarCacheSection + model="dashboard" + item={dashboard} + setPage={setPage} + /> + </SidesheetCard> + )} + </Stack> + ); +}; + +const HistoryTab = ({ + canWrite, + revisions, + currentUser, +}: { + canWrite: boolean; + revisions?: Revision[]; + currentUser: User | null; +}) => { + const dispatch = useDispatch(); + return ( + <SidesheetCard> + <Timeline + events={getTimelineEvents({ revisions, currentUser })} + data-testid="dashboard-history-list" + revert={revision => dispatch(revertToRevision(revision))} + canWrite={canWrite} + /> + </SidesheetCard> ); }; diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.unit.spec.tsx b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts similarity index 52% rename from frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.unit.spec.tsx rename to frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts index 816a2574b2258ff57c5770fa50fe79740255578a..bd7e54d6fa46539bfcfd405c1d75fb906ac53695 100644 --- a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/DashboardInfoSidebar.unit.spec.tsx +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/common.unit.spec.ts @@ -1,32 +1,9 @@ import userEvent from "@testing-library/user-event"; -import { setupRevisionsEndpoints } from "__support__/server-mocks/revision"; -import { renderWithProviders, screen } from "__support__/ui"; -import type { Dashboard } from "metabase-types/api"; +import { screen } from "__support__/ui"; import { createMockDashboard } from "metabase-types/api/mocks"; -import { DashboardInfoSidebar } from "./DashboardInfoSidebar"; - -interface SetupOpts { - dashboard?: Dashboard; -} - -function setup({ dashboard = createMockDashboard() }: SetupOpts = {}) { - const setDashboardAttribute = jest.fn(); - - setupRevisionsEndpoints([]); - - renderWithProviders( - <DashboardInfoSidebar - dashboard={dashboard} - setDashboardAttribute={setDashboardAttribute} - />, - ); - - return { - setDashboardAttribute, - }; -} +import { setup } from "./setup"; jest.mock("metabase/dashboard/constants", () => ({ ...jest.requireActual("metabase/dashboard/constants"), @@ -37,11 +14,44 @@ describe("DashboardInfoSidebar", () => { it("should render the component", () => { setup(); - expect(screen.getByText("About")).toBeInTheDocument(); + expect(screen.getByText("Info")).toBeInTheDocument(); + expect(screen.getByTestId("sidesheet")).toBeInTheDocument(); + }); + + it("should render overview tab", () => { + setup(); + expect(screen.getByRole("tab", { name: "Overview" })).toBeInTheDocument(); + }); + + it("should render history tab", () => { + setup(); + expect(screen.getByRole("tab", { name: "History" })).toBeInTheDocument(); + }); + + it("should show description when clicking on overview tab", async () => { + await setup(); + await userEvent.click(screen.getByRole("tab", { name: "History" })); + await userEvent.click(screen.getByRole("tab", { name: "Overview" })); + + expect(screen.getByText("Description")).toBeInTheDocument(); + }); + + it("should show history when clicking on history tab", async () => { + await setup(); + await userEvent.click(screen.getByRole("tab", { name: "History" })); + + expect(screen.getByTestId("dashboard-history-list")).toBeInTheDocument(); + }); + + it("should close when clicking the close button", async () => { + const { onClose } = await setup(); + await userEvent.click(screen.getByRole("button", { name: "Close" })); + + expect(onClose).toHaveBeenCalledTimes(1); }); it("should allow to set description", async () => { - const { setDashboardAttribute } = setup(); + const { setDashboardAttribute } = await setup(); await userEvent.click(screen.getByTestId("editable-text")); await userEvent.type( @@ -58,7 +68,7 @@ describe("DashboardInfoSidebar", () => { it("should validate description length", async () => { const expectedErrorMessage = "Must be 20 characters or less"; - const { setDashboardAttribute } = setup(); + const { setDashboardAttribute } = await setup(); await userEvent.click(screen.getByTestId("editable-text")); await userEvent.type( @@ -79,7 +89,7 @@ describe("DashboardInfoSidebar", () => { }); it("should allow to clear description", async () => { - const { setDashboardAttribute } = setup({ + const { setDashboardAttribute } = await setup({ dashboard: createMockDashboard({ description: "some description" }), }); @@ -89,4 +99,14 @@ describe("DashboardInfoSidebar", () => { expect(setDashboardAttribute).toHaveBeenCalledWith("description", ""); }); + + it("should show dashboard auto-apply filter toggle", async () => { + await setup(); + expect(screen.getByText("Auto-apply filters")).toBeInTheDocument(); + }); + + it("should not render caching section in OSS", async () => { + await setup(); + expect(screen.queryByText("Caching")).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/enterprise.unit.spec.ts b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/enterprise.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f9d5650225176d5afa3c1ccfc5d88317a0b877a --- /dev/null +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/enterprise.unit.spec.ts @@ -0,0 +1,18 @@ +import { screen } from "__support__/ui"; + +import { setupEnterprise } from "./setup"; + +describe("DashboardInfoSidebar > enterprise", () => { + it("should render the component", async () => { + await setupEnterprise(); + + expect(screen.getByText("Info")).toBeInTheDocument(); + expect(screen.getByTestId("sidesheet")).toBeInTheDocument(); + }); + + it("should not render caching section without caching feature", async () => { + await setupEnterprise({}, { cache_granular_controls: false }); + + expect(screen.queryByText("Caching")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/premium.unit.spec.ts b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/premium.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..959b17e035e4420969d17b11d4c98e58ae15f4ed --- /dev/null +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/premium.unit.spec.ts @@ -0,0 +1,33 @@ +import userEvent from "@testing-library/user-event"; + +import { screen } from "__support__/ui"; + +import { setupEnterprise } from "./setup"; + +const tokenFeatures = { + cache_granular_controls: true, +}; + +describe("DashboardInfoSidebar > premium enterprise", () => { + it("should render the component", async () => { + await setupEnterprise({}, tokenFeatures); + + expect(screen.getByText("Info")).toBeInTheDocument(); + expect(screen.getByTestId("sidesheet")).toBeInTheDocument(); + }); + + it("should render caching section with caching feature", async () => { + await setupEnterprise({}, tokenFeatures); + + expect(await screen.findByText("Caching")).toBeInTheDocument(); + expect(await screen.findByText("Caching policy")).toBeInTheDocument(); + }); + + it("should show cache form when clicking on caching section", async () => { + await setupEnterprise({}, tokenFeatures); + + await userEvent.click(await screen.findByText("Use default")); + + expect(await screen.findByText("Caching settings")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/setup.tsx b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/setup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..16a79a2d323df1fb6d12213f8716249134ab6a40 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/DashboardInfoSidebar/tests/setup.tsx @@ -0,0 +1,91 @@ +import { setupEnterprisePlugins } from "__support__/enterprise"; +import { + setupDashboardEndpoints, + setupPerformanceEndpoints, + setupRevisionsEndpoints, + setupUsersEndpoints, +} from "__support__/server-mocks"; +import { mockSettings } from "__support__/settings"; +import { createMockEntitiesState } from "__support__/store"; +import { renderWithProviders, waitForLoaderToBeRemoved } from "__support__/ui"; +import type { Dashboard, Settings, TokenFeatures } from "metabase-types/api"; +import { + createMockDashboard, + createMockSettings, + createMockTokenFeatures, + createMockUser, +} from "metabase-types/api/mocks"; +import { createSampleDatabase } from "metabase-types/api/mocks/presets"; +import { createMockState } from "metabase-types/store/mocks"; + +import { DashboardInfoSidebar } from "../DashboardInfoSidebar"; + +export interface SetupOpts { + dashboard?: Dashboard; + settings?: Settings; + hasEnterprisePlugins?: boolean; +} + +export async function setup({ + dashboard = createMockDashboard(), + settings = createMockSettings(), + hasEnterprisePlugins, +}: SetupOpts = {}) { + const setDashboardAttribute = jest.fn(); + const onClose = jest.fn(); + + const currentUser = createMockUser(); + setupDashboardEndpoints(dashboard); + setupUsersEndpoints([currentUser]); + setupRevisionsEndpoints([]); + setupPerformanceEndpoints([]); + + const state = createMockState({ + currentUser, + settings: mockSettings({ + ...settings, + "token-features": createMockTokenFeatures( + settings["token-features"] || {}, + ), + }), + entities: createMockEntitiesState({ + databases: [createSampleDatabase()], + dashboards: [dashboard], + }), + }); + + if (hasEnterprisePlugins) { + setupEnterprisePlugins(); + } + + renderWithProviders( + <DashboardInfoSidebar + dashboard={dashboard} + setDashboardAttribute={setDashboardAttribute} + onClose={onClose} + />, + { storeInitialState: state }, + ); + await waitForLoaderToBeRemoved(); + + return { + setDashboardAttribute, + onClose, + }; +} + +export const setupEnterprise = ( + opts: SetupOpts = {}, + tokenFeatures: Partial<TokenFeatures> = {}, +) => { + return setup({ + ...opts, + settings: createMockSettings({ + ...opts.settings, + "token-features": createMockTokenFeatures({ + ...tokenFeatures, + }), + }), + hasEnterprisePlugins: true, + }); +}; diff --git a/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx b/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx index 13573c618d528718298a40586592572537da18da..7b114927c512905025d52699a933ad01fde7dd63 100644 --- a/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx +++ b/frontend/src/metabase/dashboard/components/DashboardSidebars.tsx @@ -214,6 +214,7 @@ export function DashboardSidebars({ <DashboardInfoSidebar dashboard={dashboard} setDashboardAttribute={setDashboardAttribute} + onClose={closeSidebar} /> ); default: diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts index 10e0d15ea16a8feecb88867eb9359f17e0003fd2..5fedbd87c5194b3ec719798c7e71c0de4b14fec1 100644 --- a/frontend/src/metabase/plugins/index.ts +++ b/frontend/src/metabase/plugins/index.ts @@ -405,7 +405,6 @@ export const PLUGIN_CACHING = { isGranularCachingEnabled: () => false, StrategyFormLauncherPanel: PluginPlaceholder as any, GranularControlsExplanation: PluginPlaceholder as any, - DashboardStrategySidebar: PluginPlaceholder as any, SidebarCacheSection: PluginPlaceholder as ComponentType<SidebarCacheSectionProps>, SidebarCacheForm: PluginPlaceholder as ComponentType< diff --git a/frontend/test/__support__/server-mocks/index.ts b/frontend/test/__support__/server-mocks/index.ts index effd00dea93f5742f859b26afb4c775245060f61..8e3c67d55037006dd5e148565bb8d31ca00c15bc 100644 --- a/frontend/test/__support__/server-mocks/index.ts +++ b/frontend/test/__support__/server-mocks/index.ts @@ -18,6 +18,7 @@ export * from "./metabot"; export * from "./model-indexes"; export * from "./native-query-snippet"; export * from "./revision"; +export * from "./performance"; export * from "./permissions"; export * from "./public"; export * from "./pulse";