diff --git a/bin/build/resources/overrides.edn b/bin/build/resources/overrides.edn index 1856ea25f6078..69c1aa0afb88d 100644 --- a/bin/build/resources/overrides.edn +++ b/bin/build/resources/overrides.edn @@ -20,6 +20,7 @@ "amalloy" {"ring-gzip-middleware" {:resource "MIT.txt"}} "buddy" {"buddy-core" {:resource "apache2_0.txt"} "buddy-sign" {:resource "apache2_0.txt"}} + "dev.failsafe" {"failsafe" {:resource "LICENSE"}} "colorize" {"colorize" {:resource "EPL.txt"}} "com.github.jnr" {"jffi$native" {:resource "apache2_0.txt"}} "com.google.api-client" {"google-api-client" {:resource "apache2_0.txt"}} diff --git a/deps.edn b/deps.edn index c59823f3e95f1..b763a1ffdf918 100644 --- a/deps.edn +++ b/deps.edn @@ -66,6 +66,7 @@ compojure/compojure {:mvn/version "1.7.1" ; HTTP Routing library built on Ring :exclusions [ring/ring-codec]} crypto-random/crypto-random {:mvn/version "1.2.1"} ; library for generating cryptographically secure random bytes and strings + diehard/diehard {:mvn/version "0.11.12"} dk.ative/docjure {:mvn/version "1.19.0" ; excel export :exclusions [org.apache.poi/poi org.apache.poi/poi-ooxml]} diff --git a/docs/developers-guide/driver-changelog.md b/docs/developers-guide/driver-changelog.md index fad52eda73072..d48ebf16b6b78 100644 --- a/docs/developers-guide/driver-changelog.md +++ b/docs/developers-guide/driver-changelog.md @@ -126,6 +126,11 @@ title: Driver interface changelog - `:test/dynamic-dataset-loading` feature has been added. It enables drivers to bail out of tests that require creation of new, not pre-loaded, dataset during test run time. +- The `:temporal/requires-default-unit` feature has been added. It should be false for most drivers, but it's necessary + for a few (like the old, pre-JDBC Druid driver) to find all temporal field refs and put a `:temporal-unit :default` on them. + That default setting was previously done for all drivers, but it introduced some downstream issues, so now only those + drivers which need it can set the feature. + ## Metabase 0.50.17 - Added method `metabase.driver/incorporate-auth-provider-details` for driver specific behavior required to diff --git a/docs/developers-guide/partner-and-community-drivers.md b/docs/developers-guide/partner-and-community-drivers.md index 98d6300455c44..c02ea4b619bcb 100644 --- a/docs/developers-guide/partner-and-community-drivers.md +++ b/docs/developers-guide/partner-and-community-drivers.md @@ -75,6 +75,7 @@ Anyone can build a community driver. These are the currently known third-party d | [Netsuite SuiteAnalytics Connect](https://github.com/ericcj/metabase-netsuite-driver) | ![GitHub stars](https://img.shields.io/github/stars/ericcj/metabase-netsuite-driver) | ![GitHub (Pre-)Release Date](https://img.shields.io/github/release-date-pre/ericcj/metabase-netsuite-driver) | | [Databend](https://github.com/databendcloud/metabase-databend-driver) | ![GitHub stars](https://img.shields.io/github/stars/databendcloud/metabase-databend-driver) | ![GitHub (Pre-)Release Date](https://img.shields.io/github/release-date-pre/databendcloud/metabase-databend-driver) | | [Peaka](https://github.com/peakacom/metabase-driver) | ![GitHub stars](https://img.shields.io/github/stars/peakacom/metabase-driver) | ![GitHub (Pre-)Release Date](https://img.shields.io/github/release-date-pre/peakacom/metabase-driver) | +| [GreptimeDB](https://github.com/greptimeteam/greptimedb-metabase-driver) | ![GitHub stars](https://img.shields.io/github/stars/greptimeteam/greptimedb-metabase-driver) | ![GitHub (Pre-)Release Date](https://img.shields.io/github/release-date-pre/greptimeteam/greptimedb-metabase-driver) | If you don't see a driver for your database, then try looking in the comments of the [issue related to the database](https://github.com/metabase/metabase/labels/Database%2F). You might also find more by [searching on GitHub](https://github.com/search?q=metabase+driver). diff --git a/e2e/snapshot-creators/default.cy.snap.js b/e2e/snapshot-creators/default.cy.snap.js index 3ba07cbb2c0fd..1951da5c3d55f 100644 --- a/e2e/snapshot-creators/default.cy.snap.js +++ b/e2e/snapshot-creators/default.cy.snap.js @@ -7,7 +7,12 @@ import { USERS, USER_GROUPS, } from "e2e/support/cypress_data"; -import { restore, snapshot, withSampleDatabase } from "e2e/support/helpers"; +import { + restore, + snapshot, + updateSetting, + withSampleDatabase, +} from "e2e/support/helpers"; const { STATIC_ORDERS_ID, @@ -85,14 +90,10 @@ describe("snapshots", () => { } function updateSettings() { - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); - cy.request("PUT", "/api/setting/enable-embedding", { value: true }).then( - () => { - cy.request("PUT", "/api/setting/embedding-secret-key", { - value: METABASE_SECRET_KEY, - }); - }, - ); + updateSetting("enable-public-sharing", true); + updateSetting("enable-embedding", true).then(() => { + updateSetting("embedding-secret-key", METABASE_SECRET_KEY); + }); // update the Sample db connection string so it is valid in both CI and locally cy.request("GET", `/api/database/${SAMPLE_DB_ID}`).then(response => { diff --git a/e2e/support/helpers/api/index.ts b/e2e/support/helpers/api/index.ts index a889503db956c..47d6073d9dcc1 100644 --- a/e2e/support/helpers/api/index.ts +++ b/e2e/support/helpers/api/index.ts @@ -24,3 +24,4 @@ export { createTimelineWithEvents } from "./createTimelineWithEvents"; export { getCurrentUser } from "./getCurrentUser"; export { remapDisplayValueToFK } from "./remapDisplayValueToFK"; export { updateDashboardCards } from "./updateDashboardCards"; +export { updateSetting } from "./updateSetting"; diff --git a/e2e/support/helpers/api/updateSetting.ts b/e2e/support/helpers/api/updateSetting.ts new file mode 100644 index 0000000000000..e20773441bb09 --- /dev/null +++ b/e2e/support/helpers/api/updateSetting.ts @@ -0,0 +1,11 @@ +import type { Settings } from "metabase-types/api"; + +export const updateSetting = < + TKey extends keyof Settings, + TValue extends Settings[TKey], +>( + setting: TKey, + value: TValue, +): Cypress.Chainable> => { + return cy.request("PUT", `/api/setting/${setting}`, { value }); +}; diff --git a/e2e/support/helpers/e2e-cloud-helpers.js b/e2e/support/helpers/e2e-cloud-helpers.js index 193fb9a47460d..d90d31bc39dda 100644 --- a/e2e/support/helpers/e2e-cloud-helpers.js +++ b/e2e/support/helpers/e2e-cloud-helpers.js @@ -1,5 +1,5 @@ +import { updateSetting } from "./api"; + export const setupMetabaseCloud = () => { - cy.request("PUT", "/api/setting/site-url", { - value: "https://CYPRESSTESTENVIRONMENT.metabaseapp.com", - }); + updateSetting("site-url", "https://CYPRESSTESTENVIRONMENT.metabaseapp.com"); }; diff --git a/e2e/support/helpers/e2e-filter-helpers.js b/e2e/support/helpers/e2e-filter-helpers.js index 2d34c01694865..cf89cb8eaa4e7 100644 --- a/e2e/support/helpers/e2e-filter-helpers.js +++ b/e2e/support/helpers/e2e-filter-helpers.js @@ -4,6 +4,8 @@ import { popover, } from "e2e/support/helpers/e2e-ui-elements-helpers"; +import { updateSetting } from "./api"; + export function setDropdownFilterType() { cy.findByText("Dropdown list").click(); } @@ -83,7 +85,5 @@ export function setConnectedFieldSource(table, field) { } export function changeSynchronousBatchUpdateSetting(value) { - cy.request("PUT", "/api/setting/synchronous-batch-updates", { - value: value, - }); + updateSetting("synchronous-batch-updates", value); } diff --git a/e2e/support/helpers/e2e-snowplow-helpers.js b/e2e/support/helpers/e2e-snowplow-helpers.js index 4f193c556e95e..5549444691b87 100644 --- a/e2e/support/helpers/e2e-snowplow-helpers.js +++ b/e2e/support/helpers/e2e-snowplow-helpers.js @@ -1,4 +1,4 @@ -import { isEE } from "e2e/support/helpers"; +import { isEE, updateSetting } from "e2e/support/helpers"; const HAS_SNOWPLOW = Cypress.env("HAS_SNOWPLOW_MICRO"); const SNOWPLOW_URL = Cypress.env("SNOWPLOW_MICRO_URL"); @@ -10,7 +10,7 @@ export const describeWithSnowplowEE = HAS_SNOWPLOW && isEE ? describe : describe.skip; export const enableTracking = () => { - cy.request("PUT", "/api/setting/anon-tracking-enabled", { value: true }); + updateSetting("anon-tracking-enabled", true); }; export const resetSnowplow = () => { diff --git a/e2e/support/helpers/index.js b/e2e/support/helpers/index.ts similarity index 100% rename from e2e/support/helpers/index.js rename to e2e/support/helpers/index.ts diff --git a/e2e/test/scenarios/admin-2/settings.cy.spec.js b/e2e/test/scenarios/admin-2/settings.cy.spec.js index b2285149b9ba6..ed57e240bf4b5 100644 --- a/e2e/test/scenarios/admin-2/settings.cy.spec.js +++ b/e2e/test/scenarios/admin-2/settings.cy.spec.js @@ -28,6 +28,7 @@ import { setupSMTP, tableHeaderClick, undoToast, + updateSetting, visitQuestion, visitQuestionAdhoc, } from "e2e/support/helpers"; @@ -794,9 +795,7 @@ describe("scenarios > admin > license and billing", () => { describe("scenarios > admin > localization", () => { function setFirstWeekDayTo(day) { - cy.request("PUT", "/api/setting/start-of-week", { - value: day.toLowerCase(), - }); + updateSetting("start-of-week", day.toLowerCase()); } beforeEach(() => { diff --git a/e2e/test/scenarios/admin-2/sso/ldap.cy.spec.js b/e2e/test/scenarios/admin-2/sso/ldap.cy.spec.js index d2fe58d4f1755..30ac40c260162 100644 --- a/e2e/test/scenarios/admin-2/sso/ldap.cy.spec.js +++ b/e2e/test/scenarios/admin-2/sso/ldap.cy.spec.js @@ -6,6 +6,7 @@ import { setTokenFeatures, setupLdap, typeAndBlurUsingLabel, + updateSetting, } from "e2e/support/helpers"; import { @@ -188,7 +189,7 @@ describeEE( it("should show the login form when ldap is enabled but password login isn't (metabase#25661)", () => { setupLdap(); - cy.request("PUT", "/api/setting/enable-password-login", { value: false }); + updateSetting("enable-password-login", false); cy.signOut(); cy.visit("/auth/login"); diff --git a/e2e/test/scenarios/admin-2/whitelabel.cy.spec.js b/e2e/test/scenarios/admin-2/whitelabel.cy.spec.js index 9c79fd898bafc..85a874a896adf 100644 --- a/e2e/test/scenarios/admin-2/whitelabel.cy.spec.js +++ b/e2e/test/scenarios/admin-2/whitelabel.cy.spec.js @@ -9,6 +9,7 @@ import { restore, setTokenFeatures, undoToast, + updateSetting, visitDashboard, visitQuestion, } from "e2e/support/helpers"; @@ -72,9 +73,10 @@ describeEE("formatting > whitelabel", () => { cy.log("Add a logo"); cy.readFile("e2e/support/assets/logo.jpeg", "base64").then( logo_data => { - cy.request("PUT", "/api/setting/application-logo-url", { - value: `data:image/jpeg;base64,${logo_data}`, - }); + updateSetting( + "application-logo-url", + `data:image/jpeg;base64,${logo_data}`, + ); }, ); }); @@ -101,9 +103,7 @@ describeEE("formatting > whitelabel", () => { it("should work for people that set favicon URL before we change the input to file input", () => { const faviconUrl = "https://cdn.ecosia.org/assets/images/ico/favicon.ico"; - cy.request("PUT", "/api/setting/application-favicon-url", { - value: faviconUrl, - }); + updateSetting("application-favicon-url", faviconUrl); checkFavicon(faviconUrl); cy.signInAsNormalUser(); cy.visit("/"); @@ -730,9 +730,7 @@ function changeLoadingMessage(message) { } function setApplicationFontTo(font) { - cy.request("PUT", "/api/setting/application-font", { - value: font, - }); + updateSetting("application-font", font); } const openSettingsMenu = () => appBar().icon("gear").click(); diff --git a/e2e/test/scenarios/binning/binning-reproductions.cy.spec.js b/e2e/test/scenarios/binning/binning-reproductions.cy.spec.js index fe880333dae64..56ead25977729 100644 --- a/e2e/test/scenarios/binning/binning-reproductions.cy.spec.js +++ b/e2e/test/scenarios/binning/binning-reproductions.cy.spec.js @@ -48,7 +48,7 @@ describe("binning related reproductions", () => { // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText(/CREATED_AT/i).realHover(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText("by day").click({ force: true }); + cy.findByText("by month").click({ force: true }); // Implicit assertion - it fails if there is more than one instance of the string, which is exactly what we need for this repro // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage diff --git a/e2e/test/scenarios/collections/cleanup.cy.spec.js b/e2e/test/scenarios/collections/cleanup.cy.spec.js index 9e5cc65a0de6c..b92cfdce420a9 100644 --- a/e2e/test/scenarios/collections/cleanup.cy.spec.js +++ b/e2e/test/scenarios/collections/cleanup.cy.spec.js @@ -44,7 +44,7 @@ describe("scenarios > collections > clean up", () => { cy.log("should not show in custom analytics collections"); visitCollection("root"); navigationSidebar().within(() => { - cy.findByText("Metabase analytics").click(); + cy.findByText("Usage analytics").click(); cy.findByText("Custom reports").click(); }); collectionMenu().click(); diff --git a/e2e/test/scenarios/collections/instance-analytics.cy.spec.js b/e2e/test/scenarios/collections/instance-analytics.cy.spec.js index a41b9f4a77149..27a1d04bd362e 100644 --- a/e2e/test/scenarios/collections/instance-analytics.cy.spec.js +++ b/e2e/test/scenarios/collections/instance-analytics.cy.spec.js @@ -18,7 +18,7 @@ import { visitQuestion, } from "e2e/support/helpers"; -const ANALYTICS_COLLECTION_NAME = "Metabase analytics"; +const ANALYTICS_COLLECTION_NAME = "Usage analytics"; const CUSTOM_REPORTS_COLLECTION_NAME = "Custom reports"; const PEOPLE_MODEL_NAME = "People"; const METRICS_DASHBOARD_NAME = "Metabase metrics"; diff --git a/e2e/test/scenarios/collections/permissions.cy.spec.js b/e2e/test/scenarios/collections/permissions.cy.spec.js index 42eadbb6a8ccd..42282b71c6150 100644 --- a/e2e/test/scenarios/collections/permissions.cy.spec.js +++ b/e2e/test/scenarios/collections/permissions.cy.spec.js @@ -417,9 +417,9 @@ describe("collection permissions", () => { ); cy.findByTestId("permission-table"); - sidebar().findByText("Metabase analytics").click(); + sidebar().findByText("Usage analytics").click(); cy.findByTestId("permissions-editor").findByText( - "Permissions for Metabase analytics", + "Permissions for Usage analytics", ); cy.findByTestId("permission-table"); }); diff --git a/e2e/test/scenarios/custom-column/custom-column.cy.spec.js b/e2e/test/scenarios/custom-column/custom-column.cy.spec.js index 85207891ff577..6efec2cecd056 100644 --- a/e2e/test/scenarios/custom-column/custom-column.cy.spec.js +++ b/e2e/test/scenarios/custom-column/custom-column.cy.spec.js @@ -146,7 +146,7 @@ describe("scenarios > question > custom column", () => { .click(); getNotebookStep("summarize") - .findByText("Product Date: Day") + .findByText("Product Date: Month") .should("be.visible"); }); diff --git a/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js b/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js index 87a6401d13077..5b19e0856d9a8 100644 --- a/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js +++ b/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js @@ -28,6 +28,7 @@ import { saveDashboard, setTokenFeatures, updateDashboardCards, + updateSetting, visitDashboard, visitEmbeddedPage, visitIframe, @@ -1807,9 +1808,7 @@ describe("scenarios > dashboard > dashboard cards > click behavior", () => { }); it("allows opening custom URL destination that is not a Metabase instance URL using link (metabase#33379)", () => { - cy.request("PUT", "/api/setting/site-url", { - value: "https://localhost:4000/subpath", - }); + updateSetting("site-url", "https://localhost:4000/subpath"); const dashboardDetails = { enable_embedding: true, }; diff --git a/e2e/test/scenarios/dashboard-cards/dashboard-card-reproductions.cy.spec.js b/e2e/test/scenarios/dashboard-cards/dashboard-card-reproductions.cy.spec.js index e7c3680cf3f90..59b27a553a8d0 100644 --- a/e2e/test/scenarios/dashboard-cards/dashboard-card-reproductions.cy.spec.js +++ b/e2e/test/scenarios/dashboard-cards/dashboard-card-reproductions.cy.spec.js @@ -21,6 +21,7 @@ import { saveDashboard, showDashboardCardActions, sidebar, + updateSetting, visitDashboard, } from "e2e/support/helpers"; import { createMockParameter } from "metabase-types/api/mocks"; @@ -869,11 +870,9 @@ describe("issues 27020 and 27105: static-viz fails to render for certain date fo // This is currently the default setting, anyway. // But we want to explicitly set it in case something changes in the future, // because it is a crucial step for this reproduction. - cy.request("PUT", "/api/setting/custom-formatting", { - value: { - "type/Temporal": { - date_style: "MMMM D, YYYY", - }, + updateSetting("custom-formatting", { + "type/Temporal": { + date_style: "MMMM D, YYYY", }, }); diff --git a/e2e/test/scenarios/dashboard-filters/dashboard-filter-defaults.cy.spec.ts b/e2e/test/scenarios/dashboard-filters/dashboard-filter-defaults.cy.spec.ts index d537772fad3a0..6e4a087566079 100644 --- a/e2e/test/scenarios/dashboard-filters/dashboard-filter-defaults.cy.spec.ts +++ b/e2e/test/scenarios/dashboard-filters/dashboard-filter-defaults.cy.spec.ts @@ -49,7 +49,9 @@ describe("scenarios > dashboard > filters > reset", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); + }); + it("should reset a filters value when editing the default", () => { createQuestionAndDashboard({ questionDetails: QUESTION, dashboardDetails: DASHBOARD, @@ -69,14 +71,21 @@ describe("scenarios > dashboard > filters > reset", () => { target: ["dimension", ["field", PRODUCTS.TITLE, null]], }, ], + }).then(() => { + visitDashboard(dashboard_id, { + params: { + filter_one: "", + filter_two: "Bar", + }, + }); }); - - visitDashboard(dashboard_id); }); - }); - it("should reset a filters value when editing the default", () => { cy.log("Default dashboard filter"); + + filterWidget().contains("Filter One").should("be.visible"); + filterWidget().contains("Bar").should("be.visible"); + cy.location("search").should("eq", "?filter_one=&filter_two=Bar"); clearFilterWidget(1); @@ -106,24 +115,66 @@ describe("scenarios > dashboard > filters > reset", () => { filterWidget().contains("Filter One").should("be.visible"); filterWidget().contains("Foo").should("be.visible"); + }); + it("should reset a filters value when editing the default, and leave other filters alone", () => { + createQuestionAndDashboard({ + questionDetails: QUESTION, + dashboardDetails: DASHBOARD, + }).then(({ body: dashboardCard }) => { + const { card_id, dashboard_id } = dashboardCard; + + cy.editDashboardCard(dashboardCard, { + parameter_mappings: [ + { + parameter_id: FILTER_ONE.id, + card_id, + target: ["dimension", ["field", PRODUCTS.CATEGORY, null]], + }, + { + parameter_id: FILTER_TWO.id, + card_id, + target: ["dimension", ["field", PRODUCTS.TITLE, null]], + }, + ], + }).then(() => { + visitDashboard(dashboard_id, { + params: { + filter_one: "", + filter_two: "Bar", + }, + }); + }); + }); + + cy.log("Default dashboard filter"); + + filterWidget().contains("Filter One").should("be.visible"); + filterWidget().contains("Bar").should("be.visible"); + + cy.location("search").should("eq", "?filter_one=&filter_two=Bar"); + + cy.log( + "Finally, when we remove dashboard filter's default value, the url should reflect that by removing the placeholder", + ); editDashboard(); openFilterOptions("Filter One"); sidebar().within(() => { cy.findByLabelText("Input box").click(); - setDefaultFilterValue("Quu"); + setDefaultFilterValue("Foo"); }); popover().button("Add filter").click(); - cy.location("search").should("eq", "?filter_one=Quu&filter_two=Foo"); + cy.location("search").should("eq", "?filter_one=Foo&filter_two=Bar"); saveDashboard(); - cy.location("search").should("eq", "?filter_one=Quu&filter_two=Foo"); - filterWidget().contains("Quu").should("be.visible"); + cy.location("search").should("eq", "?filter_one=Foo&filter_two=Bar"); + + filterWidget().contains("Filter One").should("be.visible"); filterWidget().contains("Foo").should("be.visible"); }); }); diff --git a/e2e/test/scenarios/dashboard-filters/temporal-unit-parameters.cy.spec.js b/e2e/test/scenarios/dashboard-filters/temporal-unit-parameters.cy.spec.js index 2d126c889cb0f..402960790cce3 100644 --- a/e2e/test/scenarios/dashboard-filters/temporal-unit-parameters.cy.spec.js +++ b/e2e/test/scenarios/dashboard-filters/temporal-unit-parameters.cy.spec.js @@ -24,6 +24,7 @@ import { undoToast, undoToastList, updateDashboardCards, + updateSetting, visitDashboard, visitEmbeddedPage, } from "e2e/support/helpers"; @@ -998,7 +999,7 @@ describe("scenarios > dashboard > temporal unit parameters", () => { describe("embedding", () => { beforeEach(() => { cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); }); it("should be able to use temporal unit parameters in a public dashboard", () => { diff --git a/e2e/test/scenarios/dashboard/tabs.cy.spec.js b/e2e/test/scenarios/dashboard/tabs.cy.spec.js index 8ae991d740b50..f347df34d2a24 100644 --- a/e2e/test/scenarios/dashboard/tabs.cy.spec.js +++ b/e2e/test/scenarios/dashboard/tabs.cy.spec.js @@ -46,6 +46,7 @@ import { sidebar, undo, updateDashboardCards, + updateSetting, visitCollection, visitDashboard, visitDashboardAndCreateTab, @@ -510,7 +511,7 @@ describe("scenarios > dashboard > tabs", () => { }); // Go to public dashboard - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); cy.request( "POST", `/api/dashboard/${ORDERS_DASHBOARD_ID}/public_link`, diff --git a/e2e/test/scenarios/dashboard/text-cards.cy.spec.js b/e2e/test/scenarios/dashboard/text-cards.cy.spec.js index 09df1831895d3..04632394dda92 100644 --- a/e2e/test/scenarios/dashboard/text-cards.cy.spec.js +++ b/e2e/test/scenarios/dashboard/text-cards.cy.spec.js @@ -18,6 +18,7 @@ import { saveDashboard, selectDashboardFilter, setFilter, + updateSetting, visitDashboard, } from "e2e/support/helpers"; import { createMockParameter } from "metabase-types/api/mocks"; @@ -342,7 +343,7 @@ describe("scenarios > dashboard > parameters in text and heading cards", () => { cy.request("GET", "/api/user/current").then(({ body: { id: USER_ID } }) => { cy.request("PUT", `/api/user/${USER_ID}`, { locale: "en" }); }); - cy.request("PUT", "/api/setting/site-locale", { value: "fr" }); + updateSetting("site-locale", "fr"); cy.reload(); editDashboard(); @@ -382,7 +383,7 @@ describe("scenarios > dashboard > parameters in text and heading cards", () => { cy.request("GET", "/api/user/current").then(({ body: { id: USER_ID } }) => { cy.request("PUT", `/api/user/${USER_ID}`, { locale: "en" }); }); - cy.request("PUT", "/api/setting/site-locale", { value: "fr" }); + updateSetting("site-locale", "fr"); // Create dashboard with a single date parameter, and a single question cy.createQuestionAndDashboard({ diff --git a/e2e/test/scenarios/embedding-sdk/metabase-sdk-styles-tests.cy.spec.ts b/e2e/test/scenarios/embedding-sdk/metabase-sdk-styles-tests.cy.spec.ts index f3b05af7126be..c76f6ecda869a 100644 --- a/e2e/test/scenarios/embedding-sdk/metabase-sdk-styles-tests.cy.spec.ts +++ b/e2e/test/scenarios/embedding-sdk/metabase-sdk-styles-tests.cy.spec.ts @@ -3,6 +3,7 @@ import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data"; import { restore, setTokenFeatures, + updateSetting, visitFullAppEmbeddingUrl, } from "e2e/support/helpers"; import { @@ -142,9 +143,7 @@ describeSDK("scenarios > embedding-sdk > static-dashboard", () => { it("should fallback to the font from the instance if no fontFamily is set on the theme", () => { cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/application-font", { - value: "Roboto Mono", - }); + updateSetting("application-font", "Roboto Mono"); cy.signOut(); visitFullAppEmbeddingUrl({ @@ -166,5 +165,48 @@ describeSDK("scenarios > embedding-sdk > static-dashboard", () => { '"Roboto Mono", sans-serif', ); }); + + it("should work with 'Custom' fontFamily, using the font files linked in the instance", () => { + cy.signInAsAdmin(); + + const fontUrl = + Cypress.config().baseUrl + + "/app/fonts/Open_Sans/OpenSans-Regular.woff2"; + // setting `application-font-files` will make getFont return "Custom" + updateSetting("application-font-files", [ + { + src: fontUrl, + fontWeight: 400, + fontFormat: "woff2", + }, + ]); + + cy.signOut(); + + cy.intercept("GET", fontUrl).as("fontFile"); + + visitFullAppEmbeddingUrl({ + url: EMBEDDING_SDK_STORY_HOST, + qs: { + id: STORIES.NO_STYLES_SUCCESS, + viewMode: "story", + }, + onBeforeLoad: (window: any) => { + window.JWT_SHARED_SECRET = JWT_SHARED_SECRET; + window.METABASE_INSTANCE_URL = Cypress.config().baseUrl; + window.QUESTION_ID = ORDERS_QUESTION_ID; + }, + }); + + // this test only tests if the file is loaded, not really if it is rendered + // we'll probably need visual regression tests for that + cy.wait("@fontFile"); + + cy.findByText("Product ID").should( + "have.css", + "font-family", + "Custom, sans-serif", + ); + }); }); }); diff --git a/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js b/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js index 677962253bb90..26f23a7bd5d59 100644 --- a/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js +++ b/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js @@ -10,6 +10,7 @@ import { restore, sharingMenu, sharingMenuButton, + updateSetting, visitDashboard, visitIframe, visitQuestion, @@ -310,10 +311,8 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => { }); function resetEmbedding() { - cy.request("PUT", "/api/setting/enable-embedding", { value: false }); - cy.request("PUT", "/api/setting/embedding-secret-key", { - value: null, - }); + updateSetting("enable-embedding", false); + updateSetting("embedding-secret-key", null); } function getTokenValue() { diff --git a/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js b/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js index 9d14348d3e5d9..3c37c277ff8fb 100644 --- a/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js +++ b/e2e/test/scenarios/embedding/interactive-embedding.cy.spec.js @@ -532,7 +532,7 @@ describeEE("scenarios > embedding > full app", () => { cy.get("@postMessage").invoke("resetHistory"); cy.findByTestId("app-bar").findByText("Our analytics").click(); - cy.findByRole("heading", { name: "Metabase analytics" }).should( + cy.findByRole("heading", { name: "Usage analytics" }).should( "be.visible", ); cy.get("@postMessage").should("have.been.calledWith", { diff --git a/e2e/test/scenarios/filters-reproductions/dashboard-filters-with-question-revert.cy.spec.js b/e2e/test/scenarios/filters-reproductions/dashboard-filters-with-question-revert.cy.spec.js index d1606a97af1bb..a8206e1f9030e 100644 --- a/e2e/test/scenarios/filters-reproductions/dashboard-filters-with-question-revert.cy.spec.js +++ b/e2e/test/scenarios/filters-reproductions/dashboard-filters-with-question-revert.cy.spec.js @@ -13,6 +13,7 @@ import { saveDashboard, setFilter, snapshot, + updateSetting, visitDashboard, visitEmbeddedPage, visitQuestion, @@ -221,9 +222,7 @@ describe("issue 35954", () => { }); // Discard the legalese modal so we don't need to do an extra click in the UI - cy.request("PUT", "/api/setting/show-static-embed-terms", { - value: false, - }); + updateSetting("show-static-embed-terms", false); visitDashboard(id); openSharingMenu("Embed"); diff --git a/e2e/test/scenarios/models/reproductions.cy.spec.ts b/e2e/test/scenarios/models/reproductions.cy.spec.ts index d4f30b7635dde..ac217311ab4fb 100644 --- a/e2e/test/scenarios/models/reproductions.cy.spec.ts +++ b/e2e/test/scenarios/models/reproductions.cy.spec.ts @@ -914,7 +914,7 @@ describeEE("issue 43088", () => { it("should be able to create ad-hoc questions based on instance analytics models (metabase#43088)", () => { cy.visit("/"); - navigationSidebar().findByText("Metabase analytics").click(); + navigationSidebar().findByText("Usage analytics").click(); getPinnedSection().findByText("People").scrollIntoView().click(); cy.wait("@dataset"); summarize(); diff --git a/e2e/test/scenarios/native-filters/sql-filters-source.cy.spec.js b/e2e/test/scenarios/native-filters/sql-filters-source.cy.spec.js index d3d87e7cab1db..2131f1e3e9f72 100644 --- a/e2e/test/scenarios/native-filters/sql-filters-source.cy.spec.js +++ b/e2e/test/scenarios/native-filters/sql-filters-source.cy.spec.js @@ -16,6 +16,7 @@ import { setFilterQuestionSource, setSearchBoxFilterType, setTokenFeatures, + updateSetting, visitEmbeddedPage, visitPublicQuestion, visitQuestion, @@ -48,7 +49,7 @@ describe("scenarios > filters > sql filters > values source", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); cy.intercept("POST", "/api/dataset").as("dataset"); cy.intercept("GET", "/api/session/properties").as("sessionProperties"); cy.intercept("PUT", "/api/card/*").as("updateQuestion"); @@ -616,7 +617,7 @@ describe("scenarios > filters > sql filters > values source > number parameter", beforeEach(() => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); cy.intercept("POST", "/api/dataset").as("dataset"); cy.intercept("GET", "/api/session/properties").as("sessionProperties"); cy.intercept("PUT", "/api/card/*").as("updateQuestion"); diff --git a/e2e/test/scenarios/native/native-database-source.cy.spec.js b/e2e/test/scenarios/native/native-database-source.cy.spec.js index 79ce6b64a036e..5a1af0f75612d 100644 --- a/e2e/test/scenarios/native/native-database-source.cy.spec.js +++ b/e2e/test/scenarios/native/native-database-source.cy.spec.js @@ -6,6 +6,7 @@ import { popover, restore, setTokenFeatures, + updateSetting, } from "e2e/support/helpers"; const PG_DB_ID = 2; @@ -107,9 +108,7 @@ describe( }); it("should not update the setting when the same database is selected again", () => { - cy.request("PUT", "/api/setting/last-used-native-database-id", { - value: SAMPLE_DB_ID, - }); + updateSetting("last-used-native-database-id", SAMPLE_DB_ID); startNativeQuestion(); cy.findByTestId("selected-database") diff --git a/e2e/test/scenarios/native/native-reproductions.cy.spec.js b/e2e/test/scenarios/native/native-reproductions.cy.spec.js index f89ec1ba6555e..d94aecc305a57 100644 --- a/e2e/test/scenarios/native/native-reproductions.cy.spec.js +++ b/e2e/test/scenarios/native/native-reproductions.cy.spec.js @@ -13,6 +13,7 @@ import { runNativeQuery, sidebar, startNewNativeModel, + updateSetting, visitQuestion, visitQuestionAdhoc, } from "e2e/support/helpers"; @@ -358,9 +359,7 @@ describe("issue 20625", { tags: "@quarantine" }, () => { beforeEach(() => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/native-query-autocomplete-match-style", { - value: "prefix", - }); + updateSetting("native-query-autocomplete-match-style", "prefix"); cy.signInAsNormalUser(); cy.intercept("GET", "/api/database/*/autocomplete_suggestions**").as( "autocomplete", diff --git a/e2e/test/scenarios/native/native.cy.spec.js b/e2e/test/scenarios/native/native.cy.spec.js index eba547cc53d0b..09360b37e78a0 100644 --- a/e2e/test/scenarios/native/native.cy.spec.js +++ b/e2e/test/scenarios/native/native.cy.spec.js @@ -14,6 +14,7 @@ import { restore, rightSidebar, summarize, + updateSetting, visitCollection, visitQuestionAdhoc, } from "e2e/support/helpers"; @@ -297,7 +298,7 @@ describe("scenarios > question > native", () => { beforeEach(() => { cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/is-metabot-enabled", { value: true }); + updateSetting("is-metabot-enabled", true); cy.intercept( "POST", "/api/metabot/database/**/query", diff --git a/e2e/test/scenarios/navigation/navbar.cy.spec.js b/e2e/test/scenarios/navigation/navbar.cy.spec.js index 017779151b8b1..5f61690c9f50b 100644 --- a/e2e/test/scenarios/navigation/navbar.cy.spec.js +++ b/e2e/test/scenarios/navigation/navbar.cy.spec.js @@ -6,6 +6,7 @@ import { popover, restore, setTokenFeatures, + updateSetting, visitDashboard, } from "e2e/support/helpers"; @@ -71,10 +72,8 @@ describe("scenarios > navigation > navbar", () => { it("should be open when visiting home with a custom home page configured", () => { cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/custom-homepage", { value: true }); - cy.request("PUT", "/api/setting/custom-homepage-dashboard", { - value: ORDERS_DASHBOARD_ID, - }); + updateSetting("custom-homepage", true); + updateSetting("custom-homepage-dashboard", ORDERS_DASHBOARD_ID); cy.visit("/"); cy.reload(); cy.url().should("contain", "question"); @@ -83,10 +82,8 @@ describe("scenarios > navigation > navbar", () => { it("should preserve state when clicking the mb logo and a custom home page is configured", () => { cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/custom-homepage", { value: true }); - cy.request("PUT", "/api/setting/custom-homepage-dashboard", { - value: ORDERS_DASHBOARD_ID, - }); + updateSetting("custom-homepage", true); + updateSetting("custom-homepage-dashboard", ORDERS_DASHBOARD_ID); visitDashboard(ORDERS_DASHBOARD_ID); cy.findByTestId("main-logo-link").click(); navigationSidebar().should("not.be.visible"); @@ -101,18 +98,14 @@ describe("scenarios > navigation > navbar", () => { }); it("should be open when logging in with a landing page configured", () => { - cy.request("PUT", "/api/setting/landing-page", { - value: "/question/76", - }); + updateSetting("landing-page", "/question/76"); cy.visit("/"); cy.url().should("contain", "question"); navigationSidebar().should("be.visible"); }); it("should preserve state when clicking the mb logo and landing page is configured", () => { - cy.request("PUT", "/api/setting/landing-page", { - value: "/question/76", - }); + updateSetting("landing-page", "/question/76"); visitDashboard(ORDERS_DASHBOARD_ID); cy.findByTestId("main-logo-link").click(); navigationSidebar().should("not.be.visible"); diff --git a/e2e/test/scenarios/onboarding/command-palette.cy.spec.js b/e2e/test/scenarios/onboarding/command-palette.cy.spec.js index 9fe2763654019..335a6dcefea9c 100644 --- a/e2e/test/scenarios/onboarding/command-palette.cy.spec.js +++ b/e2e/test/scenarios/onboarding/command-palette.cy.spec.js @@ -49,6 +49,7 @@ describe("command palette", () => { cy.findByText("New dashboard"); cy.findByText("New collection"); cy.findByText("New model"); + cy.findByText("New metric").should("not.exist"); cy.log("Should show recent items"); cy.findByRole("option", { name: "Orders in a dashboard" }).should( @@ -94,6 +95,9 @@ describe("command palette", () => { cy.findByRole("option", { name: "REVIEWS" }).should("exist"); cy.findByRole("option", { name: "PRODUCTS" }).should("exist"); commandPaletteInput().clear(); + + commandPaletteInput().clear().type("New met"); + cy.findByText("New metric").should("exist"); }); cy.log("We can close the command palette using escape"); @@ -208,4 +212,16 @@ describe("command palette", () => { cy.visit("/"); commandPaletteButton().should("not.contain.text", "search"); }); + + it("Should have a new metric item", () => { + cy.visit("/"); + cy.findByRole("button", { name: /Search/ }).click(); + + commandPalette().within(() => { + commandPaletteInput().should("exist").type("Me"); + cy.findByText("New metric").should("be.visible").click(); + + cy.location("pathname").should("eq", "/metric/query"); + }); + }); }); diff --git a/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts b/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts index 3f0be43433c35..008b853d894a0 100644 --- a/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts +++ b/e2e/test/scenarios/onboarding/home/browse.cy.spec.ts @@ -15,6 +15,11 @@ import { const { PRODUCTS_ID } = SAMPLE_DATABASE; +const filterButton = () => + cy + .findByTestId("browse-models-header") + .findByRole("button", { name: /Filters/i }); + describeWithSnowplow("scenarios > browse", () => { beforeEach(() => { resetSnowplow(); @@ -100,7 +105,7 @@ describeWithSnowplow("scenarios > browse", () => { it("on an open-source instance, the Browse models page has no controls for setting filters", () => { cy.visit("/"); navigationSidebar().findByLabelText("Browse models").click(); - cy.findByRole("button", { name: /filter icon/i }).should("not.exist"); + filterButton().should("not.exist"); cy.findByRole("switch", { name: /Show verified models only/ }).should( "not.exist", ); @@ -119,8 +124,7 @@ describeWithSnowplowEE("scenarios > browse (EE)", () => { ); cy.intercept("POST", "/api/moderation-review").as("updateVerification"); }); - const openFilterPopover = () => - cy.findByRole("button", { name: /filter icon/i }).click(); + const openFilterPopover = () => filterButton().click(); const toggle = () => cy.findByRole("switch", { name: /Show verified models only/ }); @@ -132,10 +136,6 @@ describeWithSnowplowEE("scenarios > browse (EE)", () => { const recentModel2 = () => recentsGrid().findByText("Model 2"); const model1Row = () => modelsTable().findByRole("row", { name: /Model 1/i }); const model2Row = () => modelsTable().findByRole("row", { name: /Model 2/i }); - const filterButton = () => - cy - .findByTestId("browse-models-header") - .findByRole("button", { name: /filter icon/i }); const setVerification = (linkSelector: RegExp | string) => { cy.findByLabelText("Move, trash, and more...").click(); diff --git a/e2e/test/scenarios/onboarding/home/homepage.cy.spec.js b/e2e/test/scenarios/onboarding/home/homepage.cy.spec.js index eabb54a580f19..20d7f99536cb5 100644 --- a/e2e/test/scenarios/onboarding/home/homepage.cy.spec.js +++ b/e2e/test/scenarios/onboarding/home/homepage.cy.spec.js @@ -24,6 +24,7 @@ import { restore, setTokenFeatures, undoToast, + updateSetting, visitDashboard, visitQuestion, } from "e2e/support/helpers"; @@ -384,10 +385,8 @@ describe("scenarios > home > custom homepage", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/custom-homepage", { value: true }); - cy.request("PUT", "/api/setting/custom-homepage-dashboard", { - value: ORDERS_DASHBOARD_ID, - }); + updateSetting("custom-homepage", true); + updateSetting("custom-homepage-dashboard", ORDERS_DASHBOARD_ID); }); it("should not flash the homescreen before redirecting (#37089)", () => { diff --git a/e2e/test/scenarios/onboarding/metabot.cy.spec.js b/e2e/test/scenarios/onboarding/metabot.cy.spec.js index 8b441eed52236..aa19acb52c51c 100644 --- a/e2e/test/scenarios/onboarding/metabot.cy.spec.js +++ b/e2e/test/scenarios/onboarding/metabot.cy.spec.js @@ -11,6 +11,7 @@ import { resetSnowplow, restore, sidebar, + updateSetting, visitModel, } from "e2e/support/helpers"; @@ -149,7 +150,7 @@ describeWithSnowplow.skip("scenarios > metabot", () => { }); const enableMetabot = () => { - cy.request("PUT", "/api/setting/is-metabot-enabled", { value: true }); + updateSetting("is-metabot-enabled", true); }; const verifyTableVisibility = () => { diff --git a/e2e/test/scenarios/question-reproductions/reproductions-3.cy.spec.js b/e2e/test/scenarios/question-reproductions/reproductions-3.cy.spec.js index af59d9f3cae3d..bc78adc57e27c 100644 --- a/e2e/test/scenarios/question-reproductions/reproductions-3.cy.spec.js +++ b/e2e/test/scenarios/question-reproductions/reproductions-3.cy.spec.js @@ -1250,10 +1250,11 @@ describe("issue 43294", () => { createQuestion(questionDetails, { visitQuestion: true }); queryBuilderFooter().findByLabelText("Switch to data").click(); - cy.log("compare action"); - cy.button("Add column").click(); - popover().findByText("Compare to the past").click(); - popover().button("Done").click(); + // TODO: reenable this test when we reenable the "Compare to the past" components. + // cy.log("compare action"); + // cy.button("Add column").click(); + // popover().findByText("Compare to the past").click(); + // popover().button("Done").click(); cy.log("extract action"); cy.button("Add column").click(); diff --git a/e2e/test/scenarios/question/column-compare.cy.spec.ts b/e2e/test/scenarios/question/column-compare.cy.spec.ts index a57c55840b08d..28f326da43e6c 100644 --- a/e2e/test/scenarios/question/column-compare.cy.spec.ts +++ b/e2e/test/scenarios/question/column-compare.cy.spec.ts @@ -196,1597 +196,1615 @@ const CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE = [ "count", ]; -describeWithSnowplow("scenarios > question > column compare", () => { - beforeEach(() => { - restore(); - resetSnowplow(); - cy.signInAsAdmin(); - }); - - afterEach(() => { - expectNoBadSnowplowEvents(); - }); - - describe("no aggregations", () => { - it("does not show column compare shortcut", () => { - createQuestion( - { query: QUERY_NO_AGGREGATION }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - cy.log("chill mode - summarize sidebar"); - cy.button("Summarize").click(); - rightSidebar().button("Count").icon("close").click(); - rightSidebar().button("Add aggregation").click(); - verifyNoColumnCompareShortcut(); - - cy.log("chill mode - column drill"); - tableHeaderClick("Title"); - verifyNoColumnCompareShortcut(); - - cy.log("chill mode - plus button"); - cy.button("Add column").click(); - verifyNoColumnCompareShortcut(); - - cy.log("notebook editor"); - openNotebook(); - cy.button("Summarize").click(); - verifyNoColumnCompareShortcut(); - }); - }); - - describe("no temporal columns", () => { +// TODO: reenable test when we reenable the "Compare to the past" components. +describe.skip("scenarios > question", () => { + describeWithSnowplow("column compare", () => { beforeEach(() => { - cy.request("PUT", `/api/field/${PRODUCTS.CREATED_AT}`, { - base_type: "type/Text", - }); - }); - - it("no breakout", () => { - createQuestion( - { query: QUERY_NO_AGGREGATION }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - cy.log("chill mode - summarize sidebar"); - cy.button("Summarize").click(); - rightSidebar().button("Count").icon("close").click(); - rightSidebar().button("Add aggregation").click(); - verifyNoColumnCompareShortcut(); - - cy.log("chill mode - column drill"); - tableHeaderClick("Title"); - verifyNoColumnCompareShortcut(); - - cy.log("chill mode - plus button"); - cy.button("Add column").click(); - verifyNoColumnCompareShortcut(); - - cy.log("notebook editor"); - openNotebook(); - cy.button("Summarize").click(); - verifyNoColumnCompareShortcut(); + restore(); + resetSnowplow(); + cy.signInAsAdmin(); }); - it("one breakout", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NON_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - cy.log("chill mode - summarize sidebar"); - cy.button("Summarize").click(); - rightSidebar().button("Count").icon("close").click(); - rightSidebar().button("Add aggregation").click(); - verifyNoColumnCompareShortcut(); - - cy.log("chill mode - column drill"); - tableHeaderClick("Category"); - verifyNoColumnCompareShortcut(); - - cy.log("chill mode - plus button"); - cy.button("Add column").click(); - verifyNoColumnCompareShortcut(); - - cy.log("notebook editor"); - openNotebook(); - cy.button("Summarize").click(); - verifyNoColumnCompareShortcut(); + afterEach(() => { + expectNoBadSnowplowEvents(); }); - }); - - describe("offset", () => { - it("should be possible to change the temporal bucket through a preset", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - openNotebook(); - getNotebookStep("summarize") - .findAllByTestId("aggregate-step") - .last() - .icon("add") - .click(); - - popover().within(() => { - cy.findByText("Basic Metrics").click(); - cy.findByText("Compare to the past").click(); - - cy.findByText("Previous year").click(); - cy.findByText("Done").click(); - }); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Year", - }); - - verifyAggregations([ - { - name: "Count (previous year)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (% vs previous year)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - }); - - it("should be possible to change the temporal bucket with a custom offset", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - openNotebook(); - getNotebookStep("summarize") - .findAllByTestId("aggregate-step") - .last() - .icon("add") - .click(); - popover().within(() => { - cy.findByText("Basic Metrics").click(); - cy.findByText("Compare to the past").click(); - - cy.findByText("Custom...").click(); - - cy.findByLabelText("Offset").clear().type("2"); - cy.findByLabelText("Unit").click(); - }); - - popover().last().findByText("Weeks").click(); - - popover().within(() => { - cy.findByText("Done").click(); - }); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Week", - }); - - verifyAggregations([ - { - name: "Count (2 weeks ago)", - expression: "Offset(Count, -2)", - }, - { - name: "Count (% vs 2 weeks ago)", - expression: "Count / Offset(Count, -2) - 1", - }, - ]); - }); - - describe("single aggregation", () => { - it("no breakout", () => { + describe("no aggregations", () => { + it("does not show column compare shortcut", () => { createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, + { query: QUERY_NO_AGGREGATION }, { visitQuestion: true, wrapId: true, idAlias: "questionId" }, ); - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - }); - - it("breakout on binned datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Created At: Month"); + cy.log("chill mode - summarize sidebar"); + cy.button("Summarize").click(); + rightSidebar().button("Count").icon("close").click(); + rightSidebar().button("Add aggregation").click(); verifyNoColumnCompareShortcut(); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); - }); - - it("breakout on non-binned datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NON_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Created At: Day"); + cy.log("chill mode - column drill"); + tableHeaderClick("Title"); verifyNoColumnCompareShortcut(); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous period)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous period)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous period)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyColumns([ - "Count (previous period)", - "Count (vs previous period)", - "Count (% vs previous period)", - ]); - }); - - it("breakout on non-datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NON_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Category"); + cy.log("chill mode - plus button"); + cy.button("Add column").click(); verifyNoColumnCompareShortcut(); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - + cy.log("notebook editor"); openNotebook(); - cy.button("Summarize").click(); verifyNoColumnCompareShortcut(); - cy.realPress("Escape"); - - cy.button("Show Visualization").click(); - queryBuilderMain().findByText("42").should("be.visible"); - - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); - }); - - it("breakout on temporal column which is an expression", () => { - createQuestion( - { query: QUERY_TEMPORAL_EXPRESSION_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Created At plus one month: Month"); - verifyNoColumnCompareShortcut(); - - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At plus one month", - bucket: "Month", - }); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); - }); - - it("multiple breakouts", () => { - createQuestion( - { query: QUERY_MULTIPLE_BREAKOUTS }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - breakout({ column: "Category" }).should("exist"); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); - }); - - it("multiple temporal breakouts", () => { - createQuestion( - { query: QUERY_MULTIPLE_TEMPORAL_BREAKOUTS }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - breakout({ column: "Category" }).should("exist"); - breakout({ column: "Created At" }).should("exist"); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); - }); - - it("one breakout on non-default datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_OTHER_DATETIME }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Count"); - verifyNoColumnCompareShortcut(); - - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyBreakoutExistsAndIsFirst({ - column: "User → Created At", - bucket: "Month", - }); - breakout({ column: "Created At", bucket: "Month" }).should("not.exist"); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); }); }); - describe("multiple aggregations", () => { - it("no breakout", () => { - createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_NO_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step1Title: "Compare one of these to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - }); - - it("breakout on binned datetime column", () => { - createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step1Title: "Compare one of these to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Created At: Month"); - verifyNoColumnCompareShortcut(); - - verifyColumnDrillText(_.omit(info, "step1Title")); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); + describe("no temporal columns", () => { + beforeEach(() => { + cy.request("PUT", `/api/field/${PRODUCTS.CREATED_AT}`, { + base_type: "type/Text", }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); }); - it("breakout on non-binned datetime column", () => { - createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_NON_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - itemName: "Compare to the past", - step1Title: "Compare one of these to the past", - step2Title: "Compare “Count” to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Created At: Day"); - verifyNoColumnCompareShortcut(); - - verifyColumnDrillText(_.omit(info, "step1Title")); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous period)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous period)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous period)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyColumns([ - "Count (previous period)", - "Count (vs previous period)", - "Count (% vs previous period)", - ]); - }); - - it("breakout on non-datetime column", () => { + it("no breakout", () => { createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_NON_DATETIME_BREAKOUT }, + { query: QUERY_NO_AGGREGATION }, { visitQuestion: true, wrapId: true, idAlias: "questionId" }, ); - const info = { - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - step1Title: "Compare one of these to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "ago", - }; - - verifySummarizeText(info); - - tableHeaderClick("Category"); + cy.log("chill mode - summarize sidebar"); + cy.button("Summarize").click(); + rightSidebar().button("Count").icon("close").click(); + rightSidebar().button("Add aggregation").click(); verifyNoColumnCompareShortcut(); - verifyColumnDrillText(_.omit(info, "step1Title")); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); - }); - - verifyAggregations([ - { - name: "Count (previous month)", - expression: "Offset(Count, -1)", - }, - { - name: "Count (vs previous month)", - expression: "Count - Offset(Count, -1)", - }, - { - name: "Count (% vs previous month)", - expression: "Count / Offset(Count, -1) - 1", - }, - ]); - - verifyColumns([ - "Count (previous month)", - "Count (vs previous month)", - "Count (% vs previous month)", - ]); - }); - }); - }); - - describe("moving average", () => { - it("should be possible to change the temporal bucket with a custom offset", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + cy.log("chill mode - column drill"); + tableHeaderClick("Title"); + verifyNoColumnCompareShortcut(); - openNotebook(); - getNotebookStep("summarize") - .findAllByTestId("aggregate-step") - .last() - .icon("add") - .click(); + cy.log("chill mode - plus button"); + cy.button("Add column").click(); + verifyNoColumnCompareShortcut(); - popover().within(() => { - cy.findByText("Basic Metrics").click(); - cy.findByText("Compare to the past").click(); + cy.log("notebook editor"); + openNotebook(); + cy.button("Summarize").click(); + verifyNoColumnCompareShortcut(); + }); - cy.findByText("Moving average").click(); + it("one breakout", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NON_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); - cy.findByLabelText("Offset").clear().type("3"); - cy.findByLabelText("Unit").click(); - }); + cy.log("chill mode - summarize sidebar"); + cy.button("Summarize").click(); + rightSidebar().button("Count").icon("close").click(); + rightSidebar().button("Add aggregation").click(); + verifyNoColumnCompareShortcut(); - popover().last().findByText("Week").click(); + cy.log("chill mode - column drill"); + tableHeaderClick("Category"); + verifyNoColumnCompareShortcut(); - popover().within(() => { - cy.findByText("Done").click(); - }); + cy.log("chill mode - plus button"); + cy.button("Add column").click(); + verifyNoColumnCompareShortcut(); - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Week", + cy.log("notebook editor"); + openNotebook(); + cy.button("Summarize").click(); + verifyNoColumnCompareShortcut(); }); - - verifyAggregations([ - { - name: "Count (3-week moving average)", - expression: - "(Offset(Count, -1) + Offset(Count, -2) + Offset(Count, -3)) / 3", - }, - { - name: "Count (% vs 3-week moving average)", - expression: - "Count / ((Offset(Count, -1) + Offset(Count, -2) + Offset(Count, -3)) / 3)", - }, - ]); }); - describe("single aggregation", () => { - it("no breakout", () => { + describe("offset", () => { + it("should be possible to change the temporal bucket through a preset", () => { createQuestion( { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, { visitQuestion: true, wrapId: true, idAlias: "questionId" }, ); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; - - verifySummarizeText(info); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); + openNotebook(); + getNotebookStep("summarize") + .findAllByTestId("aggregate-step") + .last() + .icon("add") + .click(); + + popover().within(() => { + cy.findByText("Basic Metrics").click(); + cy.findByText("Compare to the past").click(); + + cy.findByText("Previous year").click(); + cy.findByText("Done").click(); }); verifyBreakoutExistsAndIsFirst({ column: "Created At", - bucket: "Month", + bucket: "Year", }); verifyAggregations([ { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + name: "Count (previous year)", + expression: "Offset(Count, -1)", }, { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + name: "Count (% vs previous year)", + expression: "Count / Offset(Count, -1) - 1", }, ]); - - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); }); - it("breakout on binned datetime column", () => { + it("should be possible to change the temporal bucket with a custom offset", () => { createQuestion( - { query: QUERY_SINGLE_AGGREGATION_BINNED_DATETIME_BREAKOUT }, + { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, { visitQuestion: true, wrapId: true, idAlias: "questionId" }, ); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; + openNotebook(); + getNotebookStep("summarize") + .findAllByTestId("aggregate-step") + .last() + .icon("add") + .click(); - verifySummarizeText(info); + popover().within(() => { + cy.findByText("Basic Metrics").click(); + cy.findByText("Compare to the past").click(); - tableHeaderClick("Created At: Month"); - verifyNoColumnCompareShortcut(); + cy.findByText("Custom...").click(); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); + cy.findByLabelText("Offset").clear().type("2"); + cy.findByLabelText("Unit").click(); + }); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + popover().last().findByText("Weeks").click(); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); + popover().within(() => { + cy.findByText("Done").click(); + }); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Week", }); verifyAggregations([ { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + name: "Count (2 weeks ago)", + expression: "Offset(Count, -2)", }, { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + name: "Count (% vs 2 weeks ago)", + expression: "Count / Offset(Count, -2) - 1", }, ]); - - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); }); - it("breakout on non-binned datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NON_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; - - verifySummarizeText(info); + describe("single aggregation", () => { + it("no breakout", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - tableHeaderClick("Created At: Day"); - verifyNoColumnCompareShortcut(); + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + }); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + it("breakout on binned datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Month"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", }); + + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); }); - verifyAggregations([ - { - name: "Count (2-period moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-period moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-period moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("breakout on non-binned datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NON_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Day"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyBreakoutExistsAndIsFirst({ - column: "Created At", + verifyAggregations([ + { + name: "Count (previous period)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous period)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous period)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyColumns([ + "Count (previous period)", + "Count (vs previous period)", + "Count (% vs previous period)", + ]); }); - verifyColumns([ - "Count (2-period moving average)", - "Count (vs 2-period moving average)", - "Count (% vs 2-period moving average)", - ]); - }); - - it("breakout on non-datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_NON_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + it("breakout on non-datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NON_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; - verifySummarizeText(info); + verifySummarizeText(info); - tableHeaderClick("Category"); - verifyNoColumnCompareShortcut(); + tableHeaderClick("Category"); + verifyNoColumnCompareShortcut(); - verifyColumnDrillText(info); - verifyPlusButtonText(info); + verifyColumnDrillText(info); + verifyPlusButtonText(info); - openNotebook(); + openNotebook(); - cy.button("Summarize").click(); - verifyNoColumnCompareShortcut(); - cy.realPress("Escape"); + cy.button("Summarize").click(); + verifyNoColumnCompareShortcut(); + cy.realPress("Escape"); - cy.button("Show Visualization").click(); - queryBuilderMain().findByText("42").should("be.visible"); + cy.button("Show Visualization").click(); + queryBuilderMain().findByText("42").should("be.visible"); - verifyNotebookText(info); + verifyNotebookText(info); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); }); - }); - verifyAggregations([ - { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); }); - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); - }); - - it("multiple breakouts", () => { - createQuestion( - { query: QUERY_MULTIPLE_BREAKOUTS }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + it("breakout on temporal column which is an expression", () => { + createQuestion( + { query: QUERY_TEMPORAL_EXPRESSION_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At plus one month: Month"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; - - verifySummarizeText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At plus one month", + bucket: "Month", }); + + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); }); - verifyAggregations([ - { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("multiple breakouts", () => { + createQuestion( + { query: QUERY_MULTIPLE_BREAKOUTS }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", - }); - breakout({ column: "Category" }).should("exist"); + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); + breakout({ column: "Category" }).should("exist"); - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); - }); + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); + }); - it("multiple temporal breakouts", () => { - createQuestion( - { query: QUERY_MULTIPLE_TEMPORAL_BREAKOUTS }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + it("multiple temporal breakouts", () => { + createQuestion( + { query: QUERY_MULTIPLE_TEMPORAL_BREAKOUTS }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; - - verifySummarizeText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", }); + breakout({ column: "Category" }).should("exist"); + breakout({ column: "Created At" }).should("exist"); + + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); }); - verifyAggregations([ - { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("one breakout on non-default datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_OTHER_DATETIME }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Count"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyBreakoutExistsAndIsFirst({ - column: "Created At", - bucket: "Month", + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "User → Created At", + bucket: "Month", + }); + breakout({ column: "Created At", bucket: "Month" }).should( + "not.exist", + ); + + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); }); - breakout({ column: "Category" }).should("exist"); - - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); }); - it("one breakout on non-default datetime column", () => { - createQuestion( - { query: QUERY_SINGLE_AGGREGATION_OTHER_DATETIME }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); - - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; - - verifySummarizeText(info); + describe("multiple aggregations", () => { + it("no breakout", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_NO_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step1Title: "Compare one of these to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - tableHeaderClick("Count"); - verifyNoColumnCompareShortcut(); + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + }); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); + it("breakout on binned datetime column", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step1Title: "Compare one of these to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Month"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(_.omit(info, "step1Title")); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); + }); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + it("breakout on non-binned datetime column", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_NON_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step1Title: "Compare one of these to the past", + step2Title: "Compare “Count” to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Day"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(_.omit(info, "step1Title")); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); }); + + verifyAggregations([ + { + name: "Count (previous period)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous period)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous period)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyColumns([ + "Count (previous period)", + "Count (vs previous period)", + "Count (% vs previous period)", + ]); }); - verifyAggregations([ - { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("breakout on non-datetime column", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_NON_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + step1Title: "Compare one of these to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "ago", + }; + + verifySummarizeText(info); + + tableHeaderClick("Category"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(_.omit(info, "step1Title")); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyBreakoutExistsAndIsFirst({ - column: "User → Created At", - bucket: "Month", + verifyAggregations([ + { + name: "Count (previous month)", + expression: "Offset(Count, -1)", + }, + { + name: "Count (vs previous month)", + expression: "Count - Offset(Count, -1)", + }, + { + name: "Count (% vs previous month)", + expression: "Count / Offset(Count, -1) - 1", + }, + ]); + + verifyColumns([ + "Count (previous month)", + "Count (vs previous month)", + "Count (% vs previous month)", + ]); }); - breakout({ column: "Created At", bucket: "Month" }).should("not.exist"); - - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); }); }); - describe("multiple aggregations", () => { - it("no breakout", () => { + describe("moving average", () => { + it("should be possible to change the temporal bucket with a custom offset", () => { createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_NO_BREAKOUT }, + { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, { visitQuestion: true, wrapId: true, idAlias: "questionId" }, ); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step1Title: "Compare one of these to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; - - verifySummarizeText(info); - verifyColumnDrillText(info); - verifyPlusButtonText(info); - verifyNotebookText(info); - - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); - - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, - }); + openNotebook(); + getNotebookStep("summarize") + .findAllByTestId("aggregate-step") + .last() + .icon("add") + .click(); + + popover().within(() => { + cy.findByText("Basic Metrics").click(); + cy.findByText("Compare to the past").click(); + + cy.findByText("Moving average").click(); + + cy.findByLabelText("Offset").clear().type("3"); + cy.findByLabelText("Unit").click(); + }); + + popover().last().findByText("Week").click(); + + popover().within(() => { + cy.findByText("Done").click(); }); verifyBreakoutExistsAndIsFirst({ column: "Created At", - bucket: "Month", + bucket: "Week", }); + verifyAggregations([ { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + name: "Count (3-week moving average)", + expression: + "(Offset(Count, -1) + Offset(Count, -2) + Offset(Count, -3)) / 3", }, { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + name: "Count (% vs 3-week moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2) + Offset(Count, -3)) / 3)", }, ]); - - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); }); - it("breakout on binned datetime column", () => { - createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + describe("single aggregation", () => { + it("no breakout", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NO_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step1Title: "Compare one of these to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); - verifySummarizeText(info); + it("breakout on binned datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Month"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - tableHeaderClick("Created At: Month"); - verifyNoColumnCompareShortcut(); + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); - verifyColumnDrillText(_.omit(info, "step1Title")); - verifyPlusButtonText(info); - verifyNotebookText(info); + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + it("breakout on non-binned datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NON_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Day"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + verifyAggregations([ + { + name: "Count (2-period moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-period moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-period moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", }); + + verifyColumns([ + "Count (2-period moving average)", + "Count (vs 2-period moving average)", + "Count (% vs 2-period moving average)", + ]); }); - verifyAggregations([ - { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("breakout on non-datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_NON_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); - }); + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; - it("breakout on non-binned datetime column", () => { - createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_NON_BINNED_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + verifySummarizeText(info); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step1Title: "Compare one of these to the past", - step2Title: "Compare “Count” to the past", - offsetHelp: "moving average", - }; + tableHeaderClick("Category"); + verifyNoColumnCompareShortcut(); - verifySummarizeText(info); + verifyColumnDrillText(info); + verifyPlusButtonText(info); - tableHeaderClick("Created At: Day"); - verifyNoColumnCompareShortcut(); + openNotebook(); + + cy.button("Summarize").click(); + verifyNoColumnCompareShortcut(); + cy.realPress("Escape"); + + cy.button("Show Visualization").click(); + queryBuilderMain().findByText("42").should("be.visible"); + + verifyNotebookText(info); - verifyColumnDrillText(_.omit(info, "step1Title")); - verifyPlusButtonText(info); - verifyNotebookText(info); + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", }); + + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); }); - verifyAggregations([ - { - name: "Count (2-period moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-period moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-period moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("multiple breakouts", () => { + createQuestion( + { query: QUERY_MULTIPLE_BREAKOUTS }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyColumns([ - "Count (2-period moving average)", - "Count (vs 2-period moving average)", - "Count (% vs 2-period moving average)", - ]); - }); + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); + breakout({ column: "Category" }).should("exist"); - it("breakout on non-datetime column", () => { - createQuestion( - { query: QUERY_MULTIPLE_AGGREGATIONS_NON_DATETIME_BREAKOUT }, - { visitQuestion: true, wrapId: true, idAlias: "questionId" }, - ); + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); + + it("multiple temporal breakouts", () => { + createQuestion( + { query: QUERY_MULTIPLE_TEMPORAL_BREAKOUTS }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - const info = { - type: "moving-average" as const, - itemName: "Compare to the past", - step2Title: "Compare “Count” to the past", - step1Title: "Compare one of these to the past", - presets: ["Previous month", "Previous year"], - offsetHelp: "moving average", - }; + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", + }); + breakout({ column: "Category" }).should("exist"); - verifySummarizeText(info); + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); - tableHeaderClick("Category"); - verifyNoColumnCompareShortcut(); + it("one breakout on non-default datetime column", () => { + createQuestion( + { query: QUERY_SINGLE_AGGREGATION_OTHER_DATETIME }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + + tableHeaderClick("Count"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyColumnDrillText(_.omit(info, "step1Title")); - verifyPlusButtonText(info); - verifyNotebookText(info); + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyBreakoutExistsAndIsFirst({ + column: "User → Created At", + bucket: "Month", + }); + breakout({ column: "Created At", bucket: "Month" }).should( + "not.exist", + ); + + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); + }); - toggleColumnPickerItems(["Value difference"]); - popover().button("Done").click(); + describe("multiple aggregations", () => { + it("no breakout", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_NO_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step1Title: "Compare one of these to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + verifyColumnDrillText(info); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - cy.get("@questionId").then(questionId => { - expectGoodSnowplowEvent({ - event: "column_compare_via_shortcut", - custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, - database_id: SAMPLE_DB_ID, - question_id: questionId, + verifyBreakoutExistsAndIsFirst({ + column: "Created At", + bucket: "Month", }); + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); }); - verifyAggregations([ - { - name: "Count (2-month moving average)", - expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (vs 2-month moving average)", - expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", - }, - { - name: "Count (% vs 2-month moving average)", - expression: "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", - }, - ]); + it("breakout on binned datetime column", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step1Title: "Compare one of these to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Month"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(_.omit(info, "step1Title")); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); - verifyColumns([ - "Count (2-month moving average)", - "Count (vs 2-month moving average)", - "Count (% vs 2-month moving average)", - ]); + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); + + it("breakout on non-binned datetime column", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_NON_BINNED_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step1Title: "Compare one of these to the past", + step2Title: "Compare “Count” to the past", + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + + tableHeaderClick("Created At: Day"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(_.omit(info, "step1Title")); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); + + verifyAggregations([ + { + name: "Count (2-period moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-period moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-period moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyColumns([ + "Count (2-period moving average)", + "Count (vs 2-period moving average)", + "Count (% vs 2-period moving average)", + ]); + }); + + it("breakout on non-datetime column", () => { + createQuestion( + { query: QUERY_MULTIPLE_AGGREGATIONS_NON_DATETIME_BREAKOUT }, + { visitQuestion: true, wrapId: true, idAlias: "questionId" }, + ); + + const info = { + type: "moving-average" as const, + itemName: "Compare to the past", + step2Title: "Compare “Count” to the past", + step1Title: "Compare one of these to the past", + presets: ["Previous month", "Previous year"], + offsetHelp: "moving average", + }; + + verifySummarizeText(info); + + tableHeaderClick("Category"); + verifyNoColumnCompareShortcut(); + + verifyColumnDrillText(_.omit(info, "step1Title")); + verifyPlusButtonText(info); + verifyNotebookText(info); + + toggleColumnPickerItems(["Value difference"]); + popover().button("Done").click(); + + cy.get("@questionId").then(questionId => { + expectGoodSnowplowEvent({ + event: "column_compare_via_shortcut", + custom_expressions_used: CUSTOM_EXPRESSIONS_USED_MOVING_AVERAGE, + database_id: SAMPLE_DB_ID, + question_id: questionId, + }); + }); + + verifyAggregations([ + { + name: "Count (2-month moving average)", + expression: "(Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (vs 2-month moving average)", + expression: "Count - (Offset(Count, -1) + Offset(Count, -2)) / 2", + }, + { + name: "Count (% vs 2-month moving average)", + expression: + "Count / ((Offset(Count, -1) + Offset(Count, -2)) / 2)", + }, + ]); + + verifyColumns([ + "Count (2-month moving average)", + "Count (vs 2-month moving average)", + "Count (% vs 2-month moving average)", + ]); + }); }); }); }); diff --git a/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts b/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts index 38055af3ec53a..39d0596929b8b 100644 --- a/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts +++ b/e2e/test/scenarios/question/multiple-column-breakouts.cy.spec.ts @@ -272,6 +272,19 @@ function getNestedQuestionDetails(cardId: number) { }; } +// This is used in several places for the same query. +function assertTableDataForFilteredTemporalBreakouts() { + assertTableData({ + columns: ["Created At: Year", "Created At: Month", "Count"], + firstRows: [ + ["2023", "March 2023", "256"], + ["2023", "April 2023", "238"], + ["2023", "May 2023", "271"], + ], + }); + assertQueryBuilderRowCount(3); +} + describe("scenarios > question > multiple column breakouts", () => { beforeEach(() => { restore(); @@ -776,16 +789,16 @@ describe("scenarios > question > multiple column breakouts", () => { }); assertTableData({ columns: [ - "Created At", - "Created At", + "Created At: Year", + "Created At: Month", "Count", "Expression1", "Expression2", ], firstRows: [ [ - "January 1, 2022, 12:00 AM", - "April 1, 2022, 12:00 AM", + "2022", + "April 2022", "1", "January 1, 2023, 12:00 AM", "May 1, 2022, 12:00 AM", @@ -955,15 +968,7 @@ describe("scenarios > question > multiple column breakouts", () => { column2MinValue: "March 1, 2023", column2MaxValue: "May 31, 2023", }); - assertTableData({ - columns: ["Created At", "Created At", "Count"], - firstRows: [ - ["January 1, 2023, 12:00 AM", "March 1, 2023, 12:00 AM", "256"], - ["January 1, 2023, 12:00 AM", "April 1, 2023, 12:00 AM", "238"], - ["January 1, 2023, 12:00 AM", "May 1, 2023, 12:00 AM", "271"], - ], - }); - assertQueryBuilderRowCount(3); + assertTableDataForFilteredTemporalBreakouts(); cy.log("'num-bins' breakouts"); testNumericPostAggregationFilter({ @@ -1285,15 +1290,7 @@ describe("scenarios > question > multiple column breakouts", () => { column2MinValue: "March 1, 2023", column2MaxValue: "May 31, 2023", }); - assertTableData({ - columns: ["Created At", "Created At", "Count"], - firstRows: [ - ["January 1, 2023, 12:00 AM", "March 1, 2023, 12:00 AM", "256"], - ["January 1, 2023, 12:00 AM", "April 1, 2023, 12:00 AM", "238"], - ["January 1, 2023, 12:00 AM", "May 1, 2023, 12:00 AM", "271"], - ], - }); - assertQueryBuilderRowCount(3); + assertTableDataForFilteredTemporalBreakouts(); cy.log("'num-bins' breakouts"); testNumericPostAggregationFilter({ @@ -1393,8 +1390,8 @@ describe("scenarios > question > multiple column breakouts", () => { questionDetails: multiStageQuestionWith2TemporalBreakoutsDetails, queryColumn1Name: "Created At: Year", queryColumn2Name: "Created At: Month", - tableColumn1Name: "Created At", - tableColumn2Name: "Created At", + tableColumn1Name: "Created At: Year", + tableColumn2Name: "Created At: Month", }); cy.log("'num-bins' breakouts"); @@ -1564,15 +1561,7 @@ describe("scenarios > question > multiple column breakouts", () => { column2MinValue: "March 1, 2023", column2MaxValue: "May 31, 2023", }); - assertTableData({ - columns: ["Created At", "Created At", "Count"], - firstRows: [ - ["January 1, 2023, 12:00 AM", "March 1, 2023, 12:00 AM", "256"], - ["January 1, 2023, 12:00 AM", "April 1, 2023, 12:00 AM", "238"], - ["January 1, 2023, 12:00 AM", "May 1, 2023, 12:00 AM", "271"], - ], - }); - assertQueryBuilderRowCount(3); + assertTableDataForFilteredTemporalBreakouts(); cy.log("'num-bins' breakouts"); testNumericPostAggregationFilter({ @@ -1784,8 +1773,10 @@ describe("scenarios > question > multiple column breakouts", () => { visitQuestion: true, }); }); + const columnNameYear = columnName + ": Year"; + const columnNameMonth = columnName + ": Month"; assertTableData({ - columns: [columnName, columnName, "Count"], + columns: [columnNameYear, columnNameMonth, "Count"], }); cy.findByTestId("viz-settings-button").click(); @@ -1794,7 +1785,7 @@ describe("scenarios > question > multiple column breakouts", () => { .click(); toggleColumn(columnName, 0, false); cy.wait("@dataset"); - assertTableData({ columns: [columnName, "Count"] }); + assertTableData({ columns: [columnNameMonth, "Count"] }); toggleColumn(columnName, 1, false); cy.wait("@dataset"); @@ -1802,11 +1793,11 @@ describe("scenarios > question > multiple column breakouts", () => { toggleColumn(columnName, 0, true); cy.wait("@dataset"); - assertTableData({ columns: ["Count", columnName] }); + assertTableData({ columns: ["Count", columnNameYear] }); toggleColumn(columnName, 1, true); assertTableData({ - columns: ["Count", columnName, columnName], + columns: ["Count", columnNameYear, columnNameMonth], }); } diff --git a/e2e/test/scenarios/question/offset.cy.spec.ts b/e2e/test/scenarios/question/offset.cy.spec.ts index 53f333c46ab01..f6c1631735a58 100644 --- a/e2e/test/scenarios/question/offset.cy.spec.ts +++ b/e2e/test/scenarios/question/offset.cy.spec.ts @@ -1224,8 +1224,7 @@ describe("scenarios > question > offset", () => { ]); }); - // unskip once https://github.com/metabase/metabase/issues/47854 is fixed - it.skip("should work with metrics (metabase#47854)", () => { + it("should work with metrics (metabase#47854)", () => { const metricName = "Count of orders"; const ORDERS_SCALAR_METRIC: StructuredQuestionDetails = { name: metricName, @@ -1234,6 +1233,13 @@ describe("scenarios > question > offset", () => { query: { "source-table": ORDERS_ID, aggregation: [["count"]], + breakout: [ + [ + "field", + ORDERS.CREATED_AT, + { "base-type": "type/DateTime", "temporal-unit": "month" }, + ], + ], }, display: "scalar", }; diff --git a/e2e/test/scenarios/search/search-typeahead.cy.spec.js b/e2e/test/scenarios/search/search-typeahead.cy.spec.js index 5bfc22309bf1f..510b3cc9e1e09 100644 --- a/e2e/test/scenarios/search/search-typeahead.cy.spec.js +++ b/e2e/test/scenarios/search/search-typeahead.cy.spec.js @@ -4,6 +4,7 @@ import { commandPaletteButton, commandPaletteInput, restore, + updateSetting, visitFullAppEmbeddingUrl, } from "e2e/support/helpers"; @@ -39,9 +40,7 @@ describe("command palette", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/search-typeahead-enabled", { - value: false, - }); + updateSetting("search-typeahead-enabled", false); cy.visit("/"); }); diff --git a/e2e/test/scenarios/search/search.cy.spec.js b/e2e/test/scenarios/search/search.cy.spec.js index 3f72dce972476..579da6f733f6c 100644 --- a/e2e/test/scenarios/search/search.cy.spec.js +++ b/e2e/test/scenarios/search/search.cy.spec.js @@ -10,6 +10,7 @@ import { isScrollableHorizontally, main, restore, + updateSetting, visitFullAppEmbeddingUrl, } from "e2e/support/helpers"; @@ -193,10 +194,8 @@ describe("scenarios > search", () => { }); it("should not dismiss when the homepage redirects to a dashboard (metabase#34226)", () => { - cy.request("PUT", "/api/setting/custom-homepage", { value: true }); - cy.request("PUT", "/api/setting/custom-homepage-dashboard", { - value: ORDERS_DASHBOARD_ID, - }); + updateSetting("custom-homepage", true); + updateSetting("custom-homepage-dashboard", ORDERS_DASHBOARD_ID); cy.intercept( { url: `/api/dashboard/${ORDERS_DASHBOARD_ID}`, diff --git a/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js b/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js index 5cffe5dbb0b1e..d4628231b590c 100644 --- a/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js +++ b/e2e/test/scenarios/sharing/alert/email-alert.cy.spec.js @@ -7,6 +7,7 @@ import { popover, restore, setupSMTP, + updateSetting, visitQuestion, } from "e2e/support/helpers"; @@ -44,9 +45,7 @@ describe("scenarios > alert > email_alert", { tags: "@external" }, () => { }); it("should respect email alerts toggled off (metabase#12349)", () => { - cy.request("PUT", "/api/setting/report-timezone", { - value: "America/New_York", - }); + updateSetting("report-timezone", "America/New_York"); openAlertForQuestion(ORDERS_QUESTION_ID); diff --git a/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js b/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js index bfcc23b37d2e7..6b3e4ec17cb76 100644 --- a/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js +++ b/e2e/test/scenarios/sharing/public-dashboard.cy.spec.js @@ -12,6 +12,7 @@ import { popover, restore, setTokenFeatures, + updateSetting, visitDashboard, visitPublicDashboard, } from "e2e/support/helpers"; @@ -81,7 +82,7 @@ const USERS = { }; const prepareDashboard = () => { - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); cy.intercept("/api/dashboard/*/public_link").as("publicLink"); @@ -288,9 +289,7 @@ describeEE("scenarios [EE] > public > dashboard", () => { }); it("should set the window title to `{dashboard name} · {application name}`", () => { - cy.request("PUT", "/api/setting/application-name", { - value: "Custom Application Name", - }); + updateSetting("application-name", "Custom Application Name"); cy.get("@dashboardId").then(id => { visitPublicDashboard(id); diff --git a/e2e/test/scenarios/sharing/public-question.cy.spec.js b/e2e/test/scenarios/sharing/public-question.cy.spec.js index 4e17f39a359d2..ac50adeab0379 100644 --- a/e2e/test/scenarios/sharing/public-question.cy.spec.js +++ b/e2e/test/scenarios/sharing/public-question.cy.spec.js @@ -12,6 +12,7 @@ import { openSharingMenu, restore, saveQuestion, + updateSetting, visitQuestion, } from "e2e/support/helpers"; @@ -61,7 +62,7 @@ describe("scenarios > public > question", () => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); }); it("adds filters to url as get params and renders the results correctly (metabase#7120, metabase#17033, metabase#21993)", () => { diff --git a/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js b/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js index 91b1d5823af9e..ee6299725f253 100644 --- a/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js +++ b/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js @@ -18,6 +18,7 @@ import { setTokenFeatures, sharingMenu, startNewQuestion, + updateSetting, visitDashboard, visitQuestion, visualize, @@ -36,7 +37,7 @@ import { describe("when embedding is disabled", () => { beforeEach(() => { - cy.request("PUT", "/api/setting/enable-embedding", { value: false }); + updateSetting("enable-embedding", false); }); describe("when user is admin", () => { @@ -70,10 +71,8 @@ import { describe("when embedding is enabled", () => { describe("when public sharing is enabled", () => { beforeEach(() => { - cy.request("PUT", "/api/setting/enable-public-sharing", { - value: true, - }); - cy.request("PUT", "/api/setting/enable-embedding", { value: true }); + updateSetting("enable-public-sharing", true); + updateSetting("enable-embedding", true); }); describe("when user is admin", () => { @@ -138,10 +137,8 @@ import { describe("when public sharing is disabled", () => { beforeEach(() => { - cy.request("PUT", "/api/setting/enable-public-sharing", { - value: false, - }); - cy.request("PUT", "/api/setting/enable-embedding", { value: true }); + updateSetting("enable-public-sharing", false); + updateSetting("enable-embedding", true); }); describe("when user is admin", () => { @@ -250,7 +247,7 @@ describe("#39152 sharing an unsaved question", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); - cy.request("PUT", "/api/setting/enable-public-sharing", { value: true }); + updateSetting("enable-public-sharing", true); }); it("should ask the user to save the question before creating a public link", () => { diff --git a/e2e/test/scenarios/sharing/public-sharing.cy.spec.js b/e2e/test/scenarios/sharing/public-sharing.cy.spec.js index 2be35995aeead..19592aef894c4 100644 --- a/e2e/test/scenarios/sharing/public-sharing.cy.spec.js +++ b/e2e/test/scenarios/sharing/public-sharing.cy.spec.js @@ -14,6 +14,7 @@ import { setTokenFeatures, setupSMTP, sidebar, + updateSetting, visitDashboard, visitDashboardAndCreateTab, visitQuestion, @@ -294,9 +295,7 @@ describeEE( } function setAllowedDomains() { - cy.request("PUT", "/api/setting/subscription-allowed-domains", { - value: allowedDomain, - }); + updateSetting("subscription-allowed-domains", allowedDomain); } beforeEach(() => { diff --git a/e2e/test/scenarios/sharing/subscriptions.cy.spec.js b/e2e/test/scenarios/sharing/subscriptions.cy.spec.js index 8c965adcc481c..77ff3f2d5539e 100644 --- a/e2e/test/scenarios/sharing/subscriptions.cy.spec.js +++ b/e2e/test/scenarios/sharing/subscriptions.cy.spec.js @@ -26,6 +26,7 @@ import { setupSubscriptionWithRecipients, sharingMenu, sidebar, + updateSetting, viewEmailPage, visitDashboard, } from "e2e/support/helpers"; @@ -759,9 +760,7 @@ function openSlackCreationForm() { } function openRecipientsWithUserVisibilitySetting(setting) { - cy.request("PUT", "/api/setting/user-visibility", { - value: setting, - }); + updateSetting("user-visibility", setting); cy.signInAsNormalUser(); openDashboardSubscriptions(); diff --git a/e2e/test/scenarios/visualizations-tabular/drillthroughs/column_extract_drill.cy.spec.js b/e2e/test/scenarios/visualizations-tabular/drillthroughs/column_extract_drill.cy.spec.js index 9617ad422e7b5..d3c157b59e034 100644 --- a/e2e/test/scenarios/visualizations-tabular/drillthroughs/column_extract_drill.cy.spec.js +++ b/e2e/test/scenarios/visualizations-tabular/drillthroughs/column_extract_drill.cy.spec.js @@ -204,7 +204,7 @@ describeWithSnowplow("extract action", () => { it("should add an expression based on an aggregation column", () => { cy.createQuestion(DATE_QUESTION, { visitQuestion: true }); extractColumnAndCheck({ - column: "Min of Created At: Default", + column: "Min of Created At", option: "Year", value: "2,022", extraction: "Extract day, month…", diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj index deb146fe8fa8e..8f94870b413fa 100644 --- a/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj +++ b/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj @@ -185,8 +185,15 @@ (is (=? (mt/query checkins {:type :query :query {:source-query {:source-table $$checkins - :fields [$id !default.$date $user_id $venue_id] + :fields [$id $date $user_id $venue_id] :filter [:and + ;; This still gets :default bucketing! + ;; auto-bucket-datetimes puts :day bucketing + ;; on both parts of this filter, since it's + ;; matching a YYYY-mm-dd string. Then + ;; optimize-temporal-filters sees that the + ;; :type/Date column already has :day + ;; granularity, and switches both to :default [:> !default.date [:absolute-datetime #t "2014-01-01" :default]] diff --git a/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx b/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx index 2cc306f7c1bfb..51077535c42ae 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx @@ -1,3 +1,4 @@ +import { css } from "@emotion/react"; import styled from "@emotion/styled"; import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled"; @@ -13,18 +14,6 @@ export const PublicComponentStylesWrapper = styled.div` all: initial; text-decoration: none; - // # Basic css reset - // We can't apply a global css reset as it would leak into the host app - // but we can't also apply our entire css reset scoped to this container, - // as it would be of higher specificity than some of our styles. - // We'll have to hand pick the css resets that we neeed - - button { - border: 0; - background-color: transparent; - } - // end of RESET - font-style: normal; width: 100%; @@ -48,3 +37,19 @@ export const PublicComponentStylesWrapper = styled.div` display: inline; } `; +/** + * We can't apply a global css reset as it would leak into the host app but we + * can't also apply our entire css reset scoped to this container, as it would + * be of higher specificity than some of our styles. + * + * The reason why this works is two things combined: + * - `*:where(button)` doesn't increase specificity, so the resulting specificity is (0,1,0) + * - this global css is loaded in the provider, before our other styles + * - -> our other code with specificity (0,1,0) will override this as they're loaded after + */ +export const SCOPED_CSS_RESET = css` + ${PublicComponentStylesWrapper} *:where(button) { + border: 0; + background-color: transparent; + } +`; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.stories.tsx b/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.stories.tsx index d9f31fad6ee1a..3a72563e6b400 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.stories.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.stories.tsx @@ -1,15 +1,18 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Box } from "metabase/ui"; -import { SdkUsageProblemBanner } from "./SdkUsageProblemBanner"; +import { + SdkUsageProblemBanner, + type SdkUsageProblemBannerProps, +} from "./SdkUsageProblemBanner"; export default { title: "EmbeddingSDK/SdkUsageProblemBanner", component: SdkUsageProblemBanner, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ( @@ -20,14 +23,18 @@ const Template: ComponentStory = args => { const MESSAGE = "The embedding SDK is using API keys. This is intended for evaluation purposes and works only on localhost. To use on other sites, implement SSO."; -export const Warning = Template.bind({}); +export const Warning = { + render: Template, -Warning.args = { - problem: { severity: "warning", message: MESSAGE }, + args: { + problem: { severity: "warning", message: MESSAGE }, + }, }; -export const Error = Template.bind({}); +export const Error = { + render: Template, -Error.args = { - problem: { severity: "error", message: MESSAGE }, + args: { + problem: { severity: "error", message: MESSAGE }, + }, }; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.tsx b/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.tsx index 6a4137627e10b..17a8df46e4b68 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/SdkUsageProblem/SdkUsageProblemBanner.tsx @@ -20,7 +20,7 @@ import { import S from "./SdkUsageProblemBanner.module.css"; -interface Props { +export interface SdkUsageProblemBannerProps { problem: SdkUsageProblem | null; } @@ -30,7 +30,9 @@ const unthemedBrand = originalColors["brand"]; const unthemedTextDark = originalColors["text-dark"]; const unthemedTextMedium = originalColors["text-medium"]; -export const SdkUsageProblemBanner = ({ problem }: Props) => { +export const SdkUsageProblemBanner = ({ + problem, +}: SdkUsageProblemBannerProps) => { const theme = useMantineTheme(); const [expanded, setExpanded] = useState(false); diff --git a/enterprise/frontend/src/embedding-sdk/components/public/CreateDashboardModal/CreateDashboardModal.stories.tsx b/enterprise/frontend/src/embedding-sdk/components/public/CreateDashboardModal/CreateDashboardModal.stories.tsx index c34718cbe6a31..06ba7b5b442dc 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/CreateDashboardModal/CreateDashboardModal.stories.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/CreateDashboardModal/CreateDashboardModal.stories.tsx @@ -1,5 +1,5 @@ import { action } from "@storybook/addon-actions"; -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { type JSXElementConstructor, useState } from "react"; import { EditableDashboard } from "embedding-sdk/components/public"; @@ -19,16 +19,18 @@ export default { decorators: [CommonSdkStoryWrapper], }; -const Template: ComponentStory = () => ( +const Template: StoryFn = () => ( ); -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; -const HookTemplate: ComponentStory< +const HookTemplate: StoryFn< JSXElementConstructor> > = () => { const [dashboard, setDashboard] = useState(null); @@ -57,9 +59,11 @@ const HookTemplate: ComponentStory< ); }; -export const useCreateDashboardApiHook = HookTemplate.bind({}); +export const useCreateDashboardApiHook = { + render: HookTemplate, +}; -const FullWorkflowExampleTemplate: ComponentStory< +const FullWorkflowExampleTemplate: StoryFn< JSXElementConstructor> > = () => { const [dashboard, setDashboard] = useState(null); @@ -73,4 +77,6 @@ const FullWorkflowExampleTemplate: ComponentStory< ); }; -export const FullWorkflowExample = FullWorkflowExampleTemplate.bind({}); +export const FullWorkflowExample = { + render: FullWorkflowExampleTemplate, +}; diff --git a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/EditableDashboard.stories.tsx b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/EditableDashboard.stories.tsx index 61877c1707edc..37d41d9436389 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/EditableDashboard.stories.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/EditableDashboard.stories.tsx @@ -1,8 +1,11 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { CommonSdkStoryWrapper } from "embedding-sdk/test/CommonSdkStoryWrapper"; -import { EditableDashboard } from "./EditableDashboard"; +import { + EditableDashboard, + type EditableDashboardProps, +} from "./EditableDashboard"; const DASHBOARD_ID = (window as any).DASHBOARD_ID || 1; @@ -15,11 +18,14 @@ export default { decorators: [CommonSdkStoryWrapper], }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - dashboardId: DASHBOARD_ID, +export const Default = { + render: Template, + + args: { + dashboardId: DASHBOARD_ID, + }, }; diff --git a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/InteractiveDashboard.stories.tsx b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/InteractiveDashboard.stories.tsx index 020a3033c138c..13889910356f4 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/InteractiveDashboard.stories.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveDashboard/InteractiveDashboard.stories.tsx @@ -1,8 +1,11 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { CommonSdkStoryWrapper } from "embedding-sdk/test/CommonSdkStoryWrapper"; -import { InteractiveDashboard } from "./InteractiveDashboard"; +import { + InteractiveDashboard, + type InteractiveDashboardProps, +} from "./InteractiveDashboard"; const DASHBOARD_ID = (window as any).DASHBOARD_ID || 1; @@ -15,11 +18,14 @@ export default { decorators: [CommonSdkStoryWrapper], }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - dashboardId: DASHBOARD_ID, +export const Default = { + render: Template, + + args: { + dashboardId: DASHBOARD_ID, + }, }; diff --git a/enterprise/frontend/src/embedding-sdk/components/public/MetabaseProvider.tsx b/enterprise/frontend/src/embedding-sdk/components/public/MetabaseProvider.tsx index c920e95614ded..87f100b15a930 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/MetabaseProvider.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/MetabaseProvider.tsx @@ -1,3 +1,4 @@ +import { Global } from "@emotion/react"; import type { Action, Store } from "@reduxjs/toolkit"; import { type JSX, type ReactNode, memo, useEffect } from "react"; import { Provider } from "react-redux"; @@ -24,10 +25,14 @@ import type { MetabaseTheme } from "embedding-sdk/types/theme"; import { setOptions } from "metabase/redux/embed"; import { EmotionCacheProvider } from "metabase/styled-components/components/EmotionCacheProvider"; -import { PublicComponentStylesWrapper } from "../private/PublicComponentStylesWrapper"; +import { + PublicComponentStylesWrapper, + SCOPED_CSS_RESET, +} from "../private/PublicComponentStylesWrapper"; import { SdkFontsGlobalStyles } from "../private/SdkGlobalFontsStyles"; import "metabase/css/index.module.css"; import { SdkUsageProblemDisplay } from "../private/SdkUsageProblem"; + import "metabase/css/vendor.css"; export interface MetabaseProviderProps { @@ -83,6 +88,7 @@ export const MetabaseProviderInternal = ({ return ( +
diff --git a/enterprise/frontend/src/embedding-sdk/components/public/StaticDashboard/StaticDashboard.stories.tsx b/enterprise/frontend/src/embedding-sdk/components/public/StaticDashboard/StaticDashboard.stories.tsx index 2ef2c7f5be445..b3cb34bdb712a 100644 --- a/enterprise/frontend/src/embedding-sdk/components/public/StaticDashboard/StaticDashboard.stories.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/public/StaticDashboard/StaticDashboard.stories.tsx @@ -1,8 +1,10 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { StaticDashboard } from "embedding-sdk"; import { CommonSdkStoryWrapper } from "embedding-sdk/test/CommonSdkStoryWrapper"; +import type { StaticDashboardProps } from "./StaticDashboard"; + const DASHBOARD_ID = (window as any).DASHBOARD_ID || "1"; export default { @@ -14,11 +16,14 @@ export default { decorators: [CommonSdkStoryWrapper], }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - dashboardId: DASHBOARD_ID, +export const Default = { + render: Template, + + args: { + dashboardId: DASHBOARD_ID, + }, }; diff --git a/enterprise/frontend/src/embedding-sdk/tests/styling-sdk-tests.stories.tsx b/enterprise/frontend/src/embedding-sdk/tests/styling-sdk-tests.stories.tsx index ef9b445fcd761..163ce7954ea24 100644 --- a/enterprise/frontend/src/embedding-sdk/tests/styling-sdk-tests.stories.tsx +++ b/enterprise/frontend/src/embedding-sdk/tests/styling-sdk-tests.stories.tsx @@ -11,7 +11,7 @@ export default { const configThatWillError: SDKConfig = { apiKey: "TEST", - metabaseInstanceUrl: "http://localhost", + metabaseInstanceUrl: "http://fake-host:1234", }; /** diff --git a/enterprise/frontend/src/metabase-enterprise/browse/components/BrowseModels.unit.spec.tsx b/enterprise/frontend/src/metabase-enterprise/browse/models/BrowseModels.unit.spec.tsx similarity index 98% rename from enterprise/frontend/src/metabase-enterprise/browse/components/BrowseModels.unit.spec.tsx rename to enterprise/frontend/src/metabase-enterprise/browse/models/BrowseModels.unit.spec.tsx index 3dc80a5392d18..f615978b30fb8 100644 --- a/enterprise/frontend/src/metabase-enterprise/browse/components/BrowseModels.unit.spec.tsx +++ b/enterprise/frontend/src/metabase-enterprise/browse/models/BrowseModels.unit.spec.tsx @@ -6,11 +6,11 @@ import { } from "__support__/server-mocks"; import { mockSettings } from "__support__/settings"; import { renderWithProviders, screen, within } from "__support__/ui"; -import { BrowseModels } from "metabase/browse/components/BrowseModels"; +import { BrowseModels } from "metabase/browse"; import { createMockModelResult, createMockRecentModel, -} from "metabase/browse/test-utils"; +} from "metabase/browse/models/test-utils"; import type { RecentCollectionItem } from "metabase-types/api"; import { createMockCollection, diff --git a/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.tsx b/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.tsx index 860e6e7777a95..8e51b9b2a93bf 100644 --- a/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.tsx +++ b/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.tsx @@ -34,8 +34,7 @@ export function CollectionInstanceAnalyticsIcon({ ); diff --git a/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.unit.spec.tsx b/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.unit.spec.tsx index 69a386d00b5e1..be7c091059527 100644 --- a/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.unit.spec.tsx +++ b/enterprise/frontend/src/metabase-enterprise/collections/components/CollectionInstanceAnalyticsIcon.unit.spec.tsx @@ -59,7 +59,7 @@ describe("CollectionInstanceAnalyticsIcon", () => { expect(queryOfficialIcon()).toBeInTheDocument(); await userEvent.hover(queryOfficialIcon()); expect(screen.getByRole("tooltip")).toHaveTextContent( - `This is a read-only Metabase Analytics ${entity}`, + `This is a read-only Usage Analytics ${entity}`, ); }); }); diff --git a/enterprise/frontend/src/metabase-enterprise/collections/utils.unit.spec.tsx b/enterprise/frontend/src/metabase-enterprise/collections/utils.unit.spec.tsx index 5f3b35e7ee8c7..0570b2f2cca5e 100644 --- a/enterprise/frontend/src/metabase-enterprise/collections/utils.unit.spec.tsx +++ b/enterprise/frontend/src/metabase-enterprise/collections/utils.unit.spec.tsx @@ -1,7 +1,7 @@ import { setupEnterprisePlugins } from "__support__/enterprise"; import { mockSettings } from "__support__/settings"; import { renderWithProviders } from "__support__/ui"; -import { createMockModelResult } from "metabase/browse/test-utils"; +import { createMockModelResult } from "metabase/browse/models/test-utils"; import { createMockCollection, createMockTokenFeatures, diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx deleted file mode 100644 index 88e51c28cb81d..0000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.styled.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import styled from "@emotion/styled"; - -import { Text } from "metabase/ui"; - -export const ModelFilterControlSwitchLabel = styled(Text)` - text-align: right; - font-weight: bold; - line-height: 1rem; - padding: 0 0.75rem; -`; diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx deleted file mode 100644 index 1acb1250272fd..0000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/ModelFilterControls.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useCallback } from "react"; -import { t } from "ttag"; -import _ from "underscore"; - -import type { - ActualModelFilters, - ModelFilterControlsProps, -} from "metabase/browse/utils"; -import { useUserSetting } from "metabase/common/hooks"; -import { Button, Icon, Paper, Popover, Switch, Text } from "metabase/ui"; - -export const ModelFilterControls = ({ - actualModelFilters, - setActualModelFilters, -}: ModelFilterControlsProps) => { - const [__, setVerifiedFilterStatus] = useUserSetting( - "browse-filter-only-verified-models", - { shouldRefresh: false }, - ); - const setVerifiedFilterStatusDebounced = _.debounce( - setVerifiedFilterStatus, - 200, - ); - - const handleModelFilterChange = useCallback( - (modelFilterName: string, active: boolean) => { - // For now, only one filter is supported - setVerifiedFilterStatusDebounced(active); - setActualModelFilters((prev: ActualModelFilters) => { - return { ...prev, [modelFilterName]: active }; - }); - }, - [setActualModelFilters, setVerifiedFilterStatusDebounced], - ); - - // There's only one filter for now - const filters = [actualModelFilters.onlyShowVerifiedModels]; - - const areAnyFiltersActive = filters.some(filter => filter); - - return ( - - - - - - {t`Show verified models only`} - } - role="switch" - checked={actualModelFilters.onlyShowVerifiedModels} - onChange={e => { - handleModelFilterChange("onlyShowVerifiedModels", e.target.checked); - }} - labelPosition="left" - /> - - - ); -}; - -const Dot = () => { - return ( - - ); -}; diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts b/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts index a5c206fcb98b9..1e318383b25ed 100644 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts +++ b/enterprise/frontend/src/metabase-enterprise/content_verification/index.ts @@ -1,19 +1,18 @@ import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins"; import { hasPremiumFeature } from "metabase-enterprise/settings"; -import { ModelFilterControls } from "./ModelFilterControls"; import { VerifiedFilter } from "./VerifiedFilter"; import { MetricFilterControls, getDefaultMetricFilters } from "./metrics"; -import { availableModelFilters, useModelFilterSettings } from "./utils"; +import { ModelFilterControls, getDefaultModelFilters } from "./models"; if (hasPremiumFeature("content_verification")) { Object.assign(PLUGIN_CONTENT_VERIFICATION, { + contentVerificationEnabled: true, VerifiedFilter, + ModelFilterControls, - availableModelFilters, - useModelFilterSettings, + getDefaultModelFilters, - contentVerificationEnabled: true, getDefaultMetricFilters, MetricFilterControls, }); diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/metrics.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/metrics.tsx index 7a78ad6f1511b..2522465ddb1fb 100644 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/metrics.tsx +++ b/enterprise/frontend/src/metabase-enterprise/content_verification/metrics.tsx @@ -4,7 +4,7 @@ import { t } from "ttag"; import type { MetricFilterControlsProps, MetricFilterSettings, -} from "metabase/browse/utils"; +} from "metabase/browse/metrics"; import { useUserSetting } from "metabase/common/hooks"; import { getSetting } from "metabase/selectors/settings"; import { Button, Icon, Paper, Popover, Switch, Text } from "metabase/ui"; diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/models.tsx b/enterprise/frontend/src/metabase-enterprise/content_verification/models.tsx new file mode 100644 index 0000000000000..7d9f473960d0b --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/content_verification/models.tsx @@ -0,0 +1,85 @@ +import { type ChangeEvent, useCallback } from "react"; +import { t } from "ttag"; + +import type { + ModelFilterControlsProps, + ModelFilterSettings, +} from "metabase/browse/models"; +import { useUserSetting } from "metabase/common/hooks"; +import { getSetting } from "metabase/selectors/settings"; +import { Button, Icon, Paper, Popover, Switch, Text } from "metabase/ui"; +import type { State } from "metabase-types/store"; + +const USER_SETTING_KEY = "browse-filter-only-verified-models"; + +export function getDefaultModelFilters(state: State): ModelFilterSettings { + return { + verified: getSetting(state, USER_SETTING_KEY) ?? false, + }; +} + +// This component is similar to the MetricFilterControls component from ./MetricFilterControls.tsx +// merging them might be a good idea in the future. +export const ModelFilterControls = ({ + modelFilters, + setModelFilters, +}: ModelFilterControlsProps) => { + const areAnyFiltersActive = Object.values(modelFilters).some(Boolean); + + const [_, setUserSetting] = useUserSetting(USER_SETTING_KEY); + + const handleVerifiedFilterChange = useCallback( + function (evt: ChangeEvent) { + setModelFilters({ ...modelFilters, verified: evt.target.checked }); + setUserSetting(evt.target.checked); + }, + [modelFilters, setModelFilters, setUserSetting], + ); + + return ( + + + + + + {t`Show verified models only`} + } + role="switch" + checked={Boolean(modelFilters.verified)} + onChange={handleVerifiedFilterChange} + labelPosition="left" + /> + + + ); +}; + +const Dot = () => { + return ( + + ); +}; diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts b/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts deleted file mode 100644 index 5e7f5968c0349..0000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Dispatch, SetStateAction } from "react"; -import { useEffect, useMemo, useState } from "react"; - -import type { - ActualModelFilters, - AvailableModelFilters, -} from "metabase/browse/utils"; -import { useUserSetting } from "metabase/common/hooks"; - -export const availableModelFilters: AvailableModelFilters = { - onlyShowVerifiedModels: { - predicate: model => model.moderated_status === "verified", - activeByDefault: true, - }, -}; - -export const useModelFilterSettings = (): [ - ActualModelFilters, - Dispatch>, -] => { - const [initialVerifiedFilterStatus] = useUserSetting( - "browse-filter-only-verified-models", - { shouldRefresh: false }, - ); - const initialModelFilters = useMemo( - () => ({ - onlyShowVerifiedModels: initialVerifiedFilterStatus ?? false, - }), - [initialVerifiedFilterStatus], - ); - - const [actualModelFilters, setActualModelFilters] = - useState(initialModelFilters); - - useEffect(() => { - setActualModelFilters(initialModelFilters); - }, [initialModelFilters, setActualModelFilters]); - - return [actualModelFilters, setActualModelFilters]; -}; diff --git a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts b/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts deleted file mode 100644 index 0a65bb1485c62..0000000000000 --- a/enterprise/frontend/src/metabase-enterprise/content_verification/utils.unit.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createMockModelResult } from "metabase/browse/test-utils"; - -import { availableModelFilters } from "./utils"; - -describe("Utilities related to content verification", () => { - it("include a constant that defines a filter for only showing verified models", () => { - const models = [ - createMockModelResult({ - name: "A verified model", - moderated_status: "verified", - }), - createMockModelResult({ - name: "An unverified model", - moderated_status: null, - }), - ]; - const filteredModels = models.filter( - availableModelFilters.onlyShowVerifiedModels.predicate, - ); - expect(filteredModels.length).toBe(1); - expect(filteredModels[0].name).toBe("A verified model"); - }); -}); diff --git a/frontend/src/metabase-lib/v1/expressions/index.ts b/frontend/src/metabase-lib/v1/expressions/index.ts index f2842901a2425..a17b354b92c22 100644 --- a/frontend/src/metabase-lib/v1/expressions/index.ts +++ b/frontend/src/metabase-lib/v1/expressions/index.ts @@ -127,31 +127,60 @@ export function formatSegmentName( */ export function parseDimension( name: string, - { - query, - stageIndex, - expressionIndex, - }: { + options: { query: Lib.Query; stageIndex: number; - source: string; expressionIndex: number | undefined; + startRule: string; }, ) { - const columns = Lib.expressionableColumns(query, stageIndex, expressionIndex); - - return columns.find(column => { - const displayInfo = Lib.displayInfo(query, stageIndex, column); - + return getAvailableDimensions(options).find(({ info }) => { return EDITOR_FK_SYMBOLS.symbols.some(separator => { const displayName = getDisplayNameWithSeparator( - displayInfo.longDisplayName, + info.longDisplayName, separator, ); return displayName === name; }); + })?.dimension; +} + +function getAvailableDimensions({ + query, + stageIndex, + expressionIndex, + startRule, +}: { + query: Lib.Query; + stageIndex: number; + expressionIndex: number | undefined; + startRule: string; +}) { + const results = Lib.expressionableColumns( + query, + stageIndex, + expressionIndex, + ).map(dimension => { + return { + dimension, + info: Lib.displayInfo(query, stageIndex, dimension), + }; }); + + if (startRule === "aggregation") { + return [ + ...results, + ...Lib.availableMetrics(query, stageIndex).map(dimension => { + return { + dimension, + info: Lib.displayInfo(query, stageIndex, dimension), + }; + }), + ]; + } + + return results; } export function formatLegacyDimensionName( diff --git a/frontend/src/metabase/admin/app/components/DeprecationNotice/DeprecationNotice.stories.tsx b/frontend/src/metabase/admin/app/components/DeprecationNotice/DeprecationNotice.stories.tsx index ab72dd7918226..a0d425024ead1 100644 --- a/frontend/src/metabase/admin/app/components/DeprecationNotice/DeprecationNotice.stories.tsx +++ b/frontend/src/metabase/admin/app/components/DeprecationNotice/DeprecationNotice.stories.tsx @@ -1,6 +1,8 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryObj } from "@storybook/react"; -import DeprecationNotice from "./DeprecationNotice"; +import DeprecationNotice, { + type DeprecationNoticeProps, +} from "./DeprecationNotice"; export default { title: "Admin/App/DeprecationNotice", @@ -10,12 +12,14 @@ export default { }, }; -export const Default: ComponentStory = args => { - return ; -}; +export const Default: StoryObj = { + render: args => { + return ; + }, -Default.args = { - hasSlackBot: true, - hasDeprecatedDatabase: true, - isEnabled: true, + args: { + hasSlackBot: true, + hasDeprecatedDatabase: true, + isEnabled: true, + }, }; diff --git a/frontend/src/metabase/admin/settings/slack/components/SlackSetup/SlackSetup.stories.tsx b/frontend/src/metabase/admin/settings/slack/components/SlackSetup/SlackSetup.stories.tsx index a8cc5e93444a5..49c6d3428177e 100644 --- a/frontend/src/metabase/admin/settings/slack/components/SlackSetup/SlackSetup.stories.tsx +++ b/frontend/src/metabase/admin/settings/slack/components/SlackSetup/SlackSetup.stories.tsx @@ -1,6 +1,6 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryObj } from "@storybook/react"; -import SlackSetup from "./SlackSetup"; +import SlackSetup, { type SlackSetupProps } from "./SlackSetup"; export default { title: "Admin/Settings/Slack/SlackSetup", @@ -11,13 +11,15 @@ export default { }, }; -export const Default: ComponentStory = args => { - return ; -}; +export const Default: StoryObj = { + render: args => { + return ; + }, -Default.args = { - Form: () =>
, - manifest: "app: token", - isBot: false, - isValid: true, + args: { + Form: () =>
, + manifest: "app: token", + isBot: false, + isValid: true, + }, }; diff --git a/frontend/src/metabase/admin/settings/slack/components/SlackStatus/SlackStatus.stories.tsx b/frontend/src/metabase/admin/settings/slack/components/SlackStatus/SlackStatus.stories.tsx index c2f0b23cd4c5f..a30fa6e14a553 100644 --- a/frontend/src/metabase/admin/settings/slack/components/SlackStatus/SlackStatus.stories.tsx +++ b/frontend/src/metabase/admin/settings/slack/components/SlackStatus/SlackStatus.stories.tsx @@ -1,6 +1,6 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryObj } from "@storybook/react"; -import SlackStatus from "./SlackStatus"; +import SlackStatus, { type SlackStatusProps } from "./SlackStatus"; export default { title: "Admin/Settings/Slack/SlackStatus", @@ -11,11 +11,13 @@ export default { }, }; -export const Default: ComponentStory = args => { - return ; -}; +export const Default: StoryObj = { + render: args => { + return ; + }, -Default.args = { - Form: () =>
, - isValid: true, + args: { + Form: () =>
, + isValid: true, + }, }; diff --git a/frontend/src/metabase/admin/upsells/components/UpsellCard.mdx b/frontend/src/metabase/admin/upsells/components/UpsellCard.mdx new file mode 100644 index 0000000000000..947adc51a199d --- /dev/null +++ b/frontend/src/metabase/admin/upsells/components/UpsellCard.mdx @@ -0,0 +1,20 @@ +import { Canvas, Story, Meta } from "@storybook/blocks"; +import { ReduxProvider } from "__support__/storybook"; +import { _UpsellCard } from "./UpsellCard"; +import * as UpsellCardStories from "./UpsellCard.stories"; + + + +# Upsell Card + +- Use as a small, visible upsell, with or without an image + +## Examples + + + + + + + + diff --git a/frontend/src/metabase/admin/upsells/components/UpsellCard.stories.mdx b/frontend/src/metabase/admin/upsells/components/UpsellCard.stories.tsx similarity index 50% rename from frontend/src/metabase/admin/upsells/components/UpsellCard.stories.mdx rename to frontend/src/metabase/admin/upsells/components/UpsellCard.stories.tsx index 12d7f22cf7b52..56b8f6e6520da 100644 --- a/frontend/src/metabase/admin/upsells/components/UpsellCard.stories.mdx +++ b/frontend/src/metabase/admin/upsells/components/UpsellCard.stories.tsx @@ -1,8 +1,9 @@ -import { Canvas, Story, Meta } from "@storybook/addon-docs"; import { ReduxProvider } from "__support__/storybook"; -import { _UpsellCard } from "./UpsellCard"; +import { Flex } from "metabase/ui"; -export const args = { +import { type UpsellCardProps, _UpsellCard } from "./UpsellCard"; + +const args = { title: "Ice Cream", buttonText: "Get Some", buttonLink: "https://www.metabase.com", @@ -12,7 +13,7 @@ export const args = { children: "You wouldn't believe how great this stuff is.", }; -export const argTypes = { +const argTypes = { children: { control: { type: "text" }, }, @@ -31,25 +32,9 @@ export const argTypes = { source: { control: { type: "text" }, }, - children: { - control: { type: "text" }, - } }; - - -# Upsell Card - -- Use as a small, visible upsell, with or without an image - -## Examples - -export const DefaultTemplate = (args) => ( +const DefaultTemplate = (args: UpsellCardProps) => ( <_UpsellCard {...args} /> @@ -57,15 +42,20 @@ export const DefaultTemplate = (args) => ( ); -export const WithImage = DefaultTemplate.bind({}); - -export const WithoutImage = DefaultTemplate.bind({}); -WithoutImage.args = { ...args, illustrationSrc: null}; +export default { + title: "Upsells/Card", + component: _UpsellCard, + args, + argTypes, +}; - - {WithImage} - +export const WithImage = { + render: DefaultTemplate, + name: "With Image", +}; - - {WithoutImage} - +export const WithoutImage = { + render: DefaultTemplate, + name: "Without Image", + args: { ...args, illustrationSrc: null }, +}; diff --git a/frontend/src/metabase/admin/upsells/components/UpsellCard.tsx b/frontend/src/metabase/admin/upsells/components/UpsellCard.tsx index 4717eb93cb15a..4e97369f9d9e7 100644 --- a/frontend/src/metabase/admin/upsells/components/UpsellCard.tsx +++ b/frontend/src/metabase/admin/upsells/components/UpsellCard.tsx @@ -31,7 +31,7 @@ interface FixedWidthVariant { type Variants = FullWidthVariant | FixedWidthVariant; -type UpsellCardProps = OwnProps & Variants; +export type UpsellCardProps = OwnProps & Variants; export const _UpsellCard: React.FC = ({ title, diff --git a/frontend/src/metabase/admin/upsells/components/UpsellPill.mdx b/frontend/src/metabase/admin/upsells/components/UpsellPill.mdx new file mode 100644 index 0000000000000..fdaa3b8f814a1 --- /dev/null +++ b/frontend/src/metabase/admin/upsells/components/UpsellPill.mdx @@ -0,0 +1,19 @@ +import { Box } from "metabase/ui"; +import { Canvas, Story, Meta } from "@storybook/blocks"; +import { ReduxProvider } from "__support__/storybook"; +import { _UpsellPill } from "./UpsellPill"; +import * as UpsellPillStories from "./UpsellPill.stories"; + + + +# Upsell Pill + +## Examples + + + + + + + + diff --git a/frontend/src/metabase/admin/upsells/components/UpsellPill.stories.mdx b/frontend/src/metabase/admin/upsells/components/UpsellPill.stories.tsx similarity index 53% rename from frontend/src/metabase/admin/upsells/components/UpsellPill.stories.mdx rename to frontend/src/metabase/admin/upsells/components/UpsellPill.stories.tsx index 79d4cb9e888f5..efde7919e2c6f 100644 --- a/frontend/src/metabase/admin/upsells/components/UpsellPill.stories.mdx +++ b/frontend/src/metabase/admin/upsells/components/UpsellPill.stories.tsx @@ -1,16 +1,18 @@ -import { Box } from "metabase/ui"; -import { Canvas, Story, Meta } from "@storybook/addon-docs"; +import type { ComponentProps } from "react"; + import { ReduxProvider } from "__support__/storybook"; +import { Box } from "metabase/ui"; + import { _UpsellPill } from "./UpsellPill"; -export const args = { +const args = { children: "Metabase Enterprise is so great", link: "https://www.metabase.com", campaign: "enterprise", source: "enterprise-page-footer", }; -export const argTypes = { +const argTypes = { children: { control: { type: "text" }, }, @@ -25,19 +27,9 @@ export const argTypes = { }, }; - +type UpsellPillProps = ComponentProps; -# Upsell Pill - - -## Examples - -export const DefaultTemplate = (args) => ( +const DefaultTemplate = (args: UpsellPillProps) => ( <_UpsellPill {...args} /> @@ -45,26 +37,27 @@ export const DefaultTemplate = (args) => ( ); -export const Default = DefaultTemplate.bind({}); - - -export const NarrowTemplate = (args) => ( +const NarrowTemplate = (args: UpsellPillProps) => ( - + <_UpsellPill {...args} /> ); -export const Narrow = NarrowTemplate.bind({}); - - - {Default} - - - - - {Narrow} - +export default { + title: "Upsells/Pill", + component: _UpsellPill, + args, + argTypes, +}; +export const Default = { + render: DefaultTemplate, + name: "Default", +}; +export const Multiline = { + render: NarrowTemplate, + name: "Multiline", +}; diff --git a/frontend/src/metabase/browse/components/BrowseModels.tsx b/frontend/src/metabase/browse/components/BrowseModels.tsx deleted file mode 100644 index 94e62e4c21eec..0000000000000 --- a/frontend/src/metabase/browse/components/BrowseModels.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useMemo } from "react"; -import { t } from "ttag"; - -import NoResults from "assets/img/no_results.svg"; -import { useListRecentsQuery } from "metabase/api"; -import { useFetchModels } from "metabase/common/hooks/use-fetch-models"; -import { DelayedLoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper/DelayedLoadingAndErrorWrapper"; -import { - PLUGIN_COLLECTIONS, - PLUGIN_CONTENT_VERIFICATION, -} from "metabase/plugins"; -import { Box, Flex, Group, Icon, Stack, Title } from "metabase/ui"; - -import type { ModelResult } from "../types"; -import { isRecentModel } from "../types"; -import { filterModels } from "../utils"; - -import { - BrowseContainer, - BrowseHeader, - BrowseMain, - BrowseSection, - CenteredEmptyState, -} from "./BrowseContainer.styled"; -import { ModelExplanationBanner } from "./ModelExplanationBanner"; -import { ModelsTable } from "./ModelsTable"; -import { RecentModels } from "./RecentModels"; -import { getMaxRecentModelCount } from "./utils"; - -const { availableModelFilters, useModelFilterSettings, ModelFilterControls } = - PLUGIN_CONTENT_VERIFICATION; - -export const BrowseModels = () => { - /** Mapping of filter names to true if the filter is active or false if it is inactive */ - const [actualModelFilters, setActualModelFilters] = useModelFilterSettings(); - - const modelsResult = useFetchModels({ model_ancestors: true }); - - const { models, doVerifiedModelsExist } = useMemo(() => { - const unfilteredModels = - (modelsResult.data?.data as ModelResult[] | undefined) ?? []; - const doVerifiedModelsExist = unfilteredModels.some( - model => model.moderated_status === "verified", - ); - const models = - PLUGIN_COLLECTIONS.filterOutItemsFromInstanceAnalytics(unfilteredModels); - return { models, doVerifiedModelsExist }; - }, [modelsResult]); - - const { filteredModels } = useMemo(() => { - const filteredModels = filterModels( - models, - // If no models are verified, don't filter them - doVerifiedModelsExist ? actualModelFilters : {}, - availableModelFilters, - ); - return { filteredModels }; - }, [actualModelFilters, models, doVerifiedModelsExist]); - - const recentModelsResult = useListRecentsQuery(undefined, { - refetchOnMountOrArgChange: true, - }); - - const filteredRecentModels = useMemo( - () => - filterModels( - recentModelsResult.data?.filter(isRecentModel), - // If no models are verified, don't filter them - doVerifiedModelsExist ? actualModelFilters : {}, - availableModelFilters, - ), - [recentModelsResult.data, actualModelFilters, doVerifiedModelsExist], - ); - - const recentModels = useMemo(() => { - const cap = getMaxRecentModelCount(models.length); - return filteredRecentModels.slice(0, cap); - }, [filteredRecentModels, models.length]); - - const isEmpty = - !recentModelsResult.isLoading && - !modelsResult.isLoading && - !filteredModels.length; - - return ( - - - - - - <Group spacing="sm"> - <Icon - size={24} - color="var(--mb-color-icon-primary)" - name="model" - /> - {t`Models`} - </Group> - - {doVerifiedModelsExist && ( - - )} - - - - - - - {isEmpty ? ( - {t`No models here yet`}} - message={ - {t`Models help curate data to make it easier to find answers to questions all in one place.`} - } - illustrationElement={ - - - - } - /> - ) : ( - <> - - } - > - - - } - > - - - - )} - - - - - ); -}; diff --git a/frontend/src/metabase/browse/containers/TableBrowser/TableBrowser.jsx b/frontend/src/metabase/browse/containers/TableBrowser/TableBrowser.jsx index f762df7e2a110..37ea6879b89cb 100644 --- a/frontend/src/metabase/browse/containers/TableBrowser/TableBrowser.jsx +++ b/frontend/src/metabase/browse/containers/TableBrowser/TableBrowser.jsx @@ -9,8 +9,8 @@ import { getSetting } from "metabase/selectors/settings"; import { SAVED_QUESTIONS_VIRTUAL_DB_ID } from "metabase-lib/v1/metadata/utils/saved-questions"; import * as ML_Urls from "metabase-lib/v1/urls"; -import TableBrowser from "../../components/TableBrowser"; import { RELOAD_INTERVAL } from "../../constants"; +import TableBrowser from "../../tables/TableBrowser"; const getDatabaseId = (props, { includeVirtual } = {}) => { const { params } = props; diff --git a/frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx b/frontend/src/metabase/browse/databases/BrowseDatabases.styled.tsx similarity index 86% rename from frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx rename to frontend/src/metabase/browse/databases/BrowseDatabases.styled.tsx index 7ed6b06559958..87a3679a3471e 100644 --- a/frontend/src/metabase/browse/components/BrowseDatabases.styled.tsx +++ b/frontend/src/metabase/browse/databases/BrowseDatabases.styled.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router"; import Card from "metabase/components/Card"; -import { BrowseGrid } from "./BrowseContainer.styled"; +import { BrowseGrid } from "../components/BrowseContainer.styled"; export const DatabaseGrid = styled(BrowseGrid)``; diff --git a/frontend/src/metabase/browse/components/BrowseDatabases.tsx b/frontend/src/metabase/browse/databases/BrowseDatabases.tsx similarity index 94% rename from frontend/src/metabase/browse/components/BrowseDatabases.tsx rename to frontend/src/metabase/browse/databases/BrowseDatabases.tsx index 1690c1cb420cd..93d61a1bd22bb 100644 --- a/frontend/src/metabase/browse/components/BrowseDatabases.tsx +++ b/frontend/src/metabase/browse/databases/BrowseDatabases.tsx @@ -13,8 +13,9 @@ import { BrowseMain, BrowseSection, CenteredEmptyState, -} from "./BrowseContainer.styled"; -import { BrowseDataHeader } from "./BrowseDataHeader"; +} from "../components/BrowseContainer.styled"; +import { BrowseDataHeader } from "../components/BrowseDataHeader"; + import { DatabaseCard, DatabaseCardLink, diff --git a/frontend/src/metabase/browse/components/BrowseDatabases.unit.spec.tsx b/frontend/src/metabase/browse/databases/BrowseDatabases.unit.spec.tsx similarity index 100% rename from frontend/src/metabase/browse/components/BrowseDatabases.unit.spec.tsx rename to frontend/src/metabase/browse/databases/BrowseDatabases.unit.spec.tsx diff --git a/frontend/src/metabase/browse/databases/index.tsx b/frontend/src/metabase/browse/databases/index.tsx new file mode 100644 index 0000000000000..7529e040d0299 --- /dev/null +++ b/frontend/src/metabase/browse/databases/index.tsx @@ -0,0 +1 @@ +export { BrowseDatabases } from "./BrowseDatabases"; diff --git a/frontend/src/metabase/browse/index.tsx b/frontend/src/metabase/browse/index.tsx new file mode 100644 index 0000000000000..a9e26c2d9a22f --- /dev/null +++ b/frontend/src/metabase/browse/index.tsx @@ -0,0 +1,5 @@ +export { BrowseMetrics } from "./metrics"; +export { BrowseModels } from "./models"; +export { BrowseDatabases } from "./databases"; +export { BrowseTables } from "./tables"; +export { BrowseSchemas } from "./schemas"; diff --git a/frontend/src/metabase/browse/components/BrowseMetrics.tsx b/frontend/src/metabase/browse/metrics/BrowseMetrics.tsx similarity index 97% rename from frontend/src/metabase/browse/components/BrowseMetrics.tsx rename to frontend/src/metabase/browse/metrics/BrowseMetrics.tsx index 4a8c48f0421aa..66264f2a43919 100644 --- a/frontend/src/metabase/browse/components/BrowseMetrics.tsx +++ b/frontend/src/metabase/browse/metrics/BrowseMetrics.tsx @@ -10,16 +10,15 @@ import { useSelector } from "metabase/lib/redux"; import { PLUGIN_CONTENT_VERIFICATION } from "metabase/plugins"; import { Box, Flex, Group, Icon, Stack, Text, Title } from "metabase/ui"; -import type { MetricResult } from "../types"; -import type { MetricFilterSettings } from "../utils"; - import { BrowseContainer, BrowseHeader, BrowseMain, BrowseSection, -} from "./BrowseContainer.styled"; +} from "../components/BrowseContainer.styled"; + import { MetricsTable } from "./MetricsTable"; +import type { MetricFilterSettings, MetricResult } from "./types"; const { contentVerificationEnabled, diff --git a/frontend/src/metabase/browse/components/BrowseMetrics.unit.spec.tsx b/frontend/src/metabase/browse/metrics/BrowseMetrics.unit.spec.tsx similarity index 98% rename from frontend/src/metabase/browse/components/BrowseMetrics.unit.spec.tsx rename to frontend/src/metabase/browse/metrics/BrowseMetrics.unit.spec.tsx index b5335e1e03d1c..d7a5c4d28f1e5 100644 --- a/frontend/src/metabase/browse/components/BrowseMetrics.unit.spec.tsx +++ b/frontend/src/metabase/browse/metrics/BrowseMetrics.unit.spec.tsx @@ -14,10 +14,9 @@ import { } from "metabase-types/api/mocks"; import { createMockSetupState } from "metabase-types/store/mocks"; -import { createMockMetricResult, createMockRecentMetric } from "../test-utils"; -import type { MetricResult, RecentMetric } from "../types"; - import { BrowseMetrics } from "./BrowseMetrics"; +import { createMockMetricResult, createMockRecentMetric } from "./test-utils"; +import type { MetricResult, RecentMetric } from "./types"; type SetupOpts = { metricCount?: number; diff --git a/frontend/src/metabase/browse/components/MetricsTable.tsx b/frontend/src/metabase/browse/metrics/MetricsTable.tsx similarity index 98% rename from frontend/src/metabase/browse/components/MetricsTable.tsx rename to frontend/src/metabase/browse/metrics/MetricsTable.tsx index 7721eadb578fc..07971547644a2 100644 --- a/frontend/src/metabase/browse/components/MetricsTable.tsx +++ b/frontend/src/metabase/browse/metrics/MetricsTable.tsx @@ -42,8 +42,6 @@ import { import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat"; import { SortDirection, type SortingOptions } from "metabase-types/api/sorting"; -import type { MetricResult } from "../types"; - import { Cell, CollectionLink, @@ -53,11 +51,13 @@ import { Value, ValueTableCell, ValueWrapper, -} from "./BrowseTable.styled"; +} from "../components/BrowseTable.styled"; + +import type { MetricResult } from "./types"; import { getDatasetValueForMetric, getMetricDescription, - sortModelOrMetric, + sortMetrics, } from "./utils"; type MetricsTableProps = { @@ -109,7 +109,7 @@ export function MetricsTable({ ); const locale = useLocale(); - const sortedMetrics = sortModelOrMetric(metrics, sortingOptions, locale); + const sortedMetrics = sortMetrics(metrics, sortingOptions, locale); const handleSortingOptionsChange = skeleton ? undefined : setSortingOptions; diff --git a/frontend/src/metabase/browse/metrics/index.tsx b/frontend/src/metabase/browse/metrics/index.tsx new file mode 100644 index 0000000000000..9eefac1ac955f --- /dev/null +++ b/frontend/src/metabase/browse/metrics/index.tsx @@ -0,0 +1,6 @@ +export { BrowseMetrics } from "./BrowseMetrics"; +export type { + MetricFilterControlsProps, + MetricFilterSettings, + RecentMetric, +} from "./types"; diff --git a/frontend/src/metabase/browse/metrics/test-utils.ts b/frontend/src/metabase/browse/metrics/test-utils.ts new file mode 100644 index 0000000000000..a420b268588a0 --- /dev/null +++ b/frontend/src/metabase/browse/metrics/test-utils.ts @@ -0,0 +1,19 @@ +import { + createMockRecentCollectionItem, + createMockSearchResult, +} from "metabase-types/api/mocks"; + +import type { MetricResult, RecentMetric } from "./types"; + +export const createMockMetricResult = ( + metric: Partial = {}, +): MetricResult => + createMockSearchResult({ ...metric, model: "metric" }) as MetricResult; + +export const createMockRecentMetric = ( + metric: Partial, +): RecentMetric => + createMockRecentCollectionItem({ + ...metric, + model: "metric", + }) as RecentMetric; diff --git a/frontend/src/metabase/browse/metrics/types.tsx b/frontend/src/metabase/browse/metrics/types.tsx new file mode 100644 index 0000000000000..f3ba40104fcac --- /dev/null +++ b/frontend/src/metabase/browse/metrics/types.tsx @@ -0,0 +1,19 @@ +import type { RecentCollectionItem, SearchResult } from "metabase-types/api"; + +/** + * Metric retrieved through the search endpoint + */ +export type MetricResult = SearchResult; + +export interface RecentMetric extends RecentCollectionItem { + model: "metric"; +} + +export type MetricFilterSettings = { + verified?: boolean; +}; + +export type MetricFilterControlsProps = { + metricFilters: MetricFilterSettings; + setMetricFilters: (settings: MetricFilterSettings) => void; +}; diff --git a/frontend/src/metabase/browse/components/utils.tsx b/frontend/src/metabase/browse/metrics/utils.tsx similarity index 68% rename from frontend/src/metabase/browse/components/utils.tsx rename to frontend/src/metabase/browse/metrics/utils.tsx index c12fb60054eca..919e223535fdb 100644 --- a/frontend/src/metabase/browse/components/utils.tsx +++ b/frontend/src/metabase/browse/metrics/utils.tsx @@ -3,24 +3,10 @@ import { t } from "ttag"; import { getCollectionPathAsString } from "metabase/collections/utils"; import { formatValue } from "metabase/lib/formatting"; import { isDate } from "metabase-lib/v1/types/utils/isa"; -import type { Dataset, SearchResult } from "metabase-types/api"; +import type { Dataset } from "metabase-types/api"; import { SortDirection, type SortingOptions } from "metabase-types/api/sorting"; -import type { MetricResult, ModelResult } from "../types"; - -export type ModelOrMetricResult = ModelResult | MetricResult; - -export const isModel = (item: SearchResult) => item.model === "dataset"; - -export const getModelDescription = (item: ModelResult) => { - if (item.collection && !item.description?.trim()) { - return t`A model`; - } else { - return item.description; - } -}; - -export const isMetric = (item: SearchResult) => item.model === "metric"; +import type { MetricResult } from "./types"; export const getMetricDescription = (item: MetricResult) => { if (item.collection && !item.description?.trim()) { @@ -31,30 +17,30 @@ export const getMetricDescription = (item: MetricResult) => { }; const getValueForSorting = ( - model: ModelResult | MetricResult, - sort_column: keyof ModelResult, + metric: MetricResult, + sort_column: keyof MetricResult, ): string => { if (sort_column === "collection") { - return getCollectionPathAsString(model.collection) ?? ""; + return getCollectionPathAsString(metric.collection) ?? ""; } else { - return model[sort_column] ?? ""; + return metric[sort_column] ?? ""; } }; export const isValidSortColumn = ( sort_column: string, -): sort_column is keyof ModelResult => { +): sort_column is keyof MetricResult => { return ["name", "collection", "description"].includes(sort_column); }; export const getSecondarySortColumn = ( sort_column: string, -): keyof ModelResult => { +): keyof MetricResult => { return sort_column === "name" ? "collection" : "name"; }; -export function sortModelOrMetric( - modelsOrMetrics: T[], +export function sortMetrics( + metrics: MetricResult[], sortingOptions: SortingOptions, localeCode: string = "en", ) { @@ -62,21 +48,21 @@ export function sortModelOrMetric( if (!isValidSortColumn(sort_column)) { console.error("Invalid sort column", sort_column); - return modelsOrMetrics; + return metrics; } const compare = (a: string, b: string) => a.localeCompare(b, localeCode, { sensitivity: "base" }); - return [...modelsOrMetrics].sort((modelOrMetricA, modelOrMetricB) => { - const a = getValueForSorting(modelOrMetricA, sort_column); - const b = getValueForSorting(modelOrMetricB, sort_column); + return [...metrics].sort((metricA, metricB) => { + const a = getValueForSorting(metricA, sort_column); + const b = getValueForSorting(metricB, sort_column); let result = compare(a, b); if (result === 0) { const sort_column2 = getSecondarySortColumn(sort_column); - const a2 = getValueForSorting(modelOrMetricA, sort_column2); - const b2 = getValueForSorting(modelOrMetricB, sort_column2); + const a2 = getValueForSorting(metricA, sort_column2); + const b2 = getValueForSorting(metricB, sort_column2); result = compare(a2, b2); } @@ -84,22 +70,6 @@ export function sortModelOrMetric( }); } -/** Find the maximum number of recently viewed models to show. - * This is roughly proportional to the number of models the user - * has permission to see */ -export const getMaxRecentModelCount = ( - /** How many models the user has permission to see */ - modelCount: number, -) => { - if (modelCount > 20) { - return 8; - } - if (modelCount > 9) { - return 4; - } - return 0; -}; - export function isDatasetScalar(dataset: Dataset) { if (dataset.error) { return false; diff --git a/frontend/src/metabase/browse/components/utils.unit.spec.tsx b/frontend/src/metabase/browse/metrics/utils.unit.spec.tsx similarity index 72% rename from frontend/src/metabase/browse/components/utils.unit.spec.tsx rename to frontend/src/metabase/browse/metrics/utils.unit.spec.tsx index 1c22219bead8e..325dd859a520f 100644 --- a/frontend/src/metabase/browse/components/utils.unit.spec.tsx +++ b/frontend/src/metabase/browse/metrics/utils.unit.spec.tsx @@ -6,20 +6,18 @@ import { } from "metabase-types/api/mocks"; import { SortDirection } from "metabase-types/api/sorting"; -import { createMockModelResult } from "../test-utils"; -import type { ModelResult } from "../types"; - +import { createMockMetricResult } from "./test-utils"; +import type { MetricResult } from "./types"; import { getDatasetValueForMetric, - getMaxRecentModelCount, isDatasetScalar, - sortModelOrMetric, + sortMetrics, } from "./utils"; -describe("sortModels", () => { +describe("sortMetrics", () => { let id = 0; - const modelMap: Record = { - "model named A, with collection path X / Y / Z": createMockModelResult({ + const metricMap: Record = { + "model named A, with collection path X / Y / Z": createMockMetricResult({ id: id++, name: "A", collection: createMockCollection({ @@ -30,12 +28,12 @@ describe("sortModels", () => { ], }), }), - "model named C, with collection path Y": createMockModelResult({ + "model named C, with collection path Y": createMockMetricResult({ id: id++, name: "C", collection: createMockCollection({ name: "Y" }), }), - "model named B, with collection path D / E / F": createMockModelResult({ + "model named B, with collection path D / E / F": createMockMetricResult({ id: id++, name: "B", collection: createMockCollection({ @@ -47,14 +45,14 @@ describe("sortModels", () => { }), }), }; - const mockSearchResults = Object.values(modelMap); + const mockSearchResults = Object.values(metricMap); it("can sort by name in ascending order", () => { const sortingOptions = { sort_column: "name", sort_direction: SortDirection.Asc, } as const; - const sorted = sortModelOrMetric(mockSearchResults, sortingOptions); + const sorted = sortMetrics(mockSearchResults, sortingOptions); expect(sorted?.map(model => model.name)).toEqual(["A", "B", "C"]); }); @@ -63,7 +61,7 @@ describe("sortModels", () => { sort_column: "name", sort_direction: SortDirection.Desc, } as const; - const sorted = sortModelOrMetric(mockSearchResults, sortingOptions); + const sorted = sortMetrics(mockSearchResults, sortingOptions); expect(sorted?.map(model => model.name)).toEqual(["C", "B", "A"]); }); @@ -72,7 +70,7 @@ describe("sortModels", () => { sort_column: "collection", sort_direction: SortDirection.Asc, } as const; - const sorted = sortModelOrMetric(mockSearchResults, sortingOptions); + const sorted = sortMetrics(mockSearchResults, sortingOptions); expect(sorted?.map(model => model.name)).toEqual(["B", "A", "C"]); }); @@ -81,17 +79,19 @@ describe("sortModels", () => { sort_column: "collection", sort_direction: SortDirection.Desc, } as const; - const sorted = sortModelOrMetric(mockSearchResults, sortingOptions); + const sorted = sortMetrics(mockSearchResults, sortingOptions); expect(sorted?.map(model => model.name)).toEqual(["C", "A", "B"]); }); describe("secondary sort", () => { - modelMap["model named C, with collection path Z"] = createMockModelResult({ - name: "C", - collection: createMockCollection({ name: "Z" }), - }); - modelMap["model named Bz, with collection path D / E / F"] = - createMockModelResult({ + metricMap["model named C, with collection path Z"] = createMockMetricResult( + { + name: "C", + collection: createMockCollection({ name: "Z" }), + }, + ); + metricMap["model named Bz, with collection path D / E / F"] = + createMockMetricResult({ name: "Bz", collection: createMockCollection({ name: "F", @@ -101,20 +101,20 @@ describe("sortModels", () => { ], }), }); - const mockSearchResults = Object.values(modelMap); + const mockSearchResults = Object.values(metricMap); it("can sort by collection path, ascending, and then does a secondary sort by name", () => { const sortingOptions = { sort_column: "collection", sort_direction: SortDirection.Asc, } as const; - const sorted = sortModelOrMetric(mockSearchResults, sortingOptions); + const sorted = sortMetrics(mockSearchResults, sortingOptions); expect(sorted).toEqual([ - modelMap["model named B, with collection path D / E / F"], - modelMap["model named Bz, with collection path D / E / F"], - modelMap["model named A, with collection path X / Y / Z"], - modelMap["model named C, with collection path Y"], - modelMap["model named C, with collection path Z"], + metricMap["model named B, with collection path D / E / F"], + metricMap["model named Bz, with collection path D / E / F"], + metricMap["model named A, with collection path X / Y / Z"], + metricMap["model named C, with collection path Y"], + metricMap["model named C, with collection path Z"], ]); }); @@ -123,13 +123,13 @@ describe("sortModels", () => { sort_column: "collection", sort_direction: SortDirection.Desc, } as const; - const sorted = sortModelOrMetric(mockSearchResults, sortingOptions); + const sorted = sortMetrics(mockSearchResults, sortingOptions); expect(sorted).toEqual([ - modelMap["model named C, with collection path Z"], - modelMap["model named C, with collection path Y"], - modelMap["model named A, with collection path X / Y / Z"], - modelMap["model named Bz, with collection path D / E / F"], - modelMap["model named B, with collection path D / E / F"], + metricMap["model named C, with collection path Z"], + metricMap["model named C, with collection path Y"], + metricMap["model named A, with collection path X / Y / Z"], + metricMap["model named Bz, with collection path D / E / F"], + metricMap["model named B, with collection path D / E / F"], ]); }); @@ -139,7 +139,7 @@ describe("sortModels", () => { sort_direction: SortDirection.Asc, } as const; - const addUmlauts = (model: ModelResult): ModelResult => ({ + const addUmlauts = (model: MetricResult): MetricResult => ({ ...model, name: model.name.replace(/^B$/g, "Bä"), collection: { @@ -153,63 +153,45 @@ describe("sortModels", () => { }, }); - const swedishModelMap = { + const swedishmetricMap = { "model named A, with collection path Ä / Y / Z": addUmlauts( - modelMap["model named A, with collection path X / Y / Z"], + metricMap["model named A, with collection path X / Y / Z"], ), "model named Bä, with collection path D / E / F": addUmlauts( - modelMap["model named B, with collection path D / E / F"], + metricMap["model named B, with collection path D / E / F"], ), "model named Bz, with collection path D / E / F": addUmlauts( - modelMap["model named Bz, with collection path D / E / F"], + metricMap["model named Bz, with collection path D / E / F"], ), "model named C, with collection path Y": addUmlauts( - modelMap["model named C, with collection path Y"], + metricMap["model named C, with collection path Y"], ), "model named C, with collection path Z": addUmlauts( - modelMap["model named C, with collection path Z"], + metricMap["model named C, with collection path Z"], ), }; - const swedishResults = Object.values(swedishModelMap); + const swedishResults = Object.values(swedishmetricMap); // When sorting in Swedish, z comes before ä const swedishLocaleCode = "sv"; - const sorted = sortModelOrMetric( + const sorted = sortMetrics( swedishResults, sortingOptions, swedishLocaleCode, ); expect("ä".localeCompare("z", "sv", { sensitivity: "base" })).toEqual(1); expect(sorted).toEqual([ - swedishModelMap["model named Bz, with collection path D / E / F"], // Model Bz sorts before Bä - swedishModelMap["model named Bä, with collection path D / E / F"], - swedishModelMap["model named C, with collection path Y"], - swedishModelMap["model named C, with collection path Z"], // Collection Z sorts before Ä - swedishModelMap["model named A, with collection path Ä / Y / Z"], + swedishmetricMap["model named Bz, with collection path D / E / F"], // Model Bz sorts before Bä + swedishmetricMap["model named Bä, with collection path D / E / F"], + swedishmetricMap["model named C, with collection path Y"], + swedishmetricMap["model named C, with collection path Z"], // Collection Z sorts before Ä + swedishmetricMap["model named A, with collection path Ä / Y / Z"], ]); }); }); }); -describe("getMaxRecentModelCount", () => { - it("returns 8 for modelCount greater than 20", () => { - expect(getMaxRecentModelCount(21)).toBe(8); - expect(getMaxRecentModelCount(100)).toBe(8); - }); - - it("returns 4 for modelCount greater than 9 and less than or equal to 20", () => { - expect(getMaxRecentModelCount(10)).toBe(4); - expect(getMaxRecentModelCount(20)).toBe(4); - }); - - it("returns 0 for modelCount of 9 or less", () => { - expect(getMaxRecentModelCount(0)).toBe(0); - expect(getMaxRecentModelCount(5)).toBe(0); - expect(getMaxRecentModelCount(9)).toBe(0); - }); -}); - describe("isDatasetScalar", () => { it("should return true for a dataset with a single column and a single row", () => { const dataset = createMockDataset({ diff --git a/frontend/src/metabase/browse/components/BrowseModels.styled.tsx b/frontend/src/metabase/browse/models/BrowseModels.styled.tsx similarity index 98% rename from frontend/src/metabase/browse/components/BrowseModels.styled.tsx rename to frontend/src/metabase/browse/models/BrowseModels.styled.tsx index dda35826be063..720e82f301fca 100644 --- a/frontend/src/metabase/browse/components/BrowseModels.styled.tsx +++ b/frontend/src/metabase/browse/models/BrowseModels.styled.tsx @@ -7,7 +7,7 @@ import { Ellipsified } from "metabase/core/components/Ellipsified"; import Link from "metabase/core/components/Link"; import { Box, type ButtonProps, Collapse, Icon } from "metabase/ui"; -import { BrowseGrid } from "./BrowseContainer.styled"; +import { BrowseGrid } from "../components/BrowseContainer.styled"; export const ModelCardLink = styled(Link)` margin: 0.5rem 0; diff --git a/frontend/src/metabase/browse/models/BrowseModels.tsx b/frontend/src/metabase/browse/models/BrowseModels.tsx new file mode 100644 index 0000000000000..afc510f1e4e6d --- /dev/null +++ b/frontend/src/metabase/browse/models/BrowseModels.tsx @@ -0,0 +1,209 @@ +import { useState } from "react"; +import { t } from "ttag"; + +import NoResults from "assets/img/no_results.svg"; +import { skipToken, useListRecentsQuery } from "metabase/api"; +import { useFetchModels } from "metabase/common/hooks/use-fetch-models"; +import { DelayedLoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper/DelayedLoadingAndErrorWrapper"; +import { useSelector } from "metabase/lib/redux"; +import { + PLUGIN_COLLECTIONS, + PLUGIN_CONTENT_VERIFICATION, +} from "metabase/plugins"; +import { Box, Flex, Group, Icon, Stack, Title } from "metabase/ui"; + +import { + BrowseContainer, + BrowseHeader, + BrowseMain, + BrowseSection, + CenteredEmptyState, +} from "../components/BrowseContainer.styled"; + +import { ModelExplanationBanner } from "./ModelExplanationBanner"; +import { ModelsTable } from "./ModelsTable"; +import { RecentModels } from "./RecentModels"; +import type { ModelFilterSettings, ModelResult } from "./types"; +import { getMaxRecentModelCount, isRecentModel } from "./utils"; + +const { + contentVerificationEnabled, + ModelFilterControls, + getDefaultModelFilters, +} = PLUGIN_CONTENT_VERIFICATION; + +export const BrowseModels = () => { + const [modelFilters, setModelFilters] = useModelFilterSettings(); + const { isLoading, error, models, recentModels, hasVerifiedModels } = + useFilteredModels(modelFilters); + + const isEmpty = !isLoading && models.length === 0; + + return ( + + + + + + <Group spacing="sm"> + <Icon + size={24} + color="var(--mb-color-icon-primary)" + name="model" + /> + {t`Models`} + </Group> + + {hasVerifiedModels && ( + + )} + + + + + + + {isEmpty ? ( + {t`No models here yet`}} + message={ + {t`Models help curate data to make it easier to find answers to questions all in one place.`} + } + illustrationElement={ + + + + } + /> + ) : ( + <> + + } + > + + + } + > + + + + )} + + + + + ); +}; + +function useModelFilterSettings() { + const defaultModelFilters = useSelector(getDefaultModelFilters); + return useState(defaultModelFilters); +} + +function useHasVerifiedModels() { + const result = useFetchModels( + contentVerificationEnabled + ? { + filter_items_in_personal_collection: "exclude", + model_ancestors: false, + limit: 0, + verified: true, + } + : skipToken, + ); + + if (!contentVerificationEnabled) { + return { + isLoading: false, + error: null, + result: false, + }; + } + + const total = result.data?.total ?? 0; + + return { + isLoading: result.isLoading, + error: result.error, + result: total > 0, + }; +} + +function useFilteredModels(modelFilters: ModelFilterSettings) { + const hasVerifiedModels = useHasVerifiedModels(); + + const filters = cleanModelFilters(modelFilters, hasVerifiedModels.result); + + const modelsResult = useFetchModels( + hasVerifiedModels.isLoading || hasVerifiedModels.error + ? skipToken + : { + filter_items_in_personal_collection: "exclude", + model_ancestors: false, + ...filters, + }, + ); + + const models = modelsResult.data?.data as ModelResult[] | undefined; + + const recentsCap = getMaxRecentModelCount(models?.length ?? 0); + + const recentModelsResult = useListRecentsQuery(undefined, { + refetchOnMountOrArgChange: true, + skip: recentsCap === 0, + }); + + const isLoading = + hasVerifiedModels.isLoading || + modelsResult.isLoading || + recentModelsResult.isLoading; + + const error = + hasVerifiedModels.error || modelsResult.error || recentModelsResult.error; + + return { + isLoading, + error, + hasVerifiedModels: hasVerifiedModels.result, + models: PLUGIN_COLLECTIONS.filterOutItemsFromInstanceAnalytics( + models ?? [], + ), + + recentModels: (recentModelsResult.data ?? []) + .filter(isRecentModel) + .filter( + model => !filters.verified || model.moderated_status === "verified", + ) + .slice(0, recentsCap), + }; +} + +function cleanModelFilters( + modelFilters: ModelFilterSettings, + hasVerifiedModels: boolean, +) { + const filters = { ...modelFilters }; + if (!hasVerifiedModels || !filters.verified) { + // we cannot pass false or undefined to the backend + // delete the key instead + delete filters.verified; + } + return filters; +} diff --git a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx b/frontend/src/metabase/browse/models/BrowseModels.unit.spec.tsx similarity index 99% rename from frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx rename to frontend/src/metabase/browse/models/BrowseModels.unit.spec.tsx index e05f459ff2e51..d5d8969d015ed 100644 --- a/frontend/src/metabase/browse/components/BrowseModels.unit.spec.tsx +++ b/frontend/src/metabase/browse/models/BrowseModels.unit.spec.tsx @@ -10,9 +10,8 @@ import { } from "metabase-types/api/mocks"; import { createMockSetupState } from "metabase-types/store/mocks"; -import { createMockModelResult, createMockRecentModel } from "../test-utils"; - import { BrowseModels } from "./BrowseModels"; +import { createMockModelResult, createMockRecentModel } from "./test-utils"; const defaultRootCollection = createMockCollection({ id: "root", diff --git a/frontend/src/metabase/browse/components/ModelExplanationBanner.tsx b/frontend/src/metabase/browse/models/ModelExplanationBanner.tsx similarity index 100% rename from frontend/src/metabase/browse/components/ModelExplanationBanner.tsx rename to frontend/src/metabase/browse/models/ModelExplanationBanner.tsx diff --git a/frontend/src/metabase/browse/components/ModelsTable.tsx b/frontend/src/metabase/browse/models/ModelsTable.tsx similarity index 96% rename from frontend/src/metabase/browse/components/ModelsTable.tsx rename to frontend/src/metabase/browse/models/ModelsTable.tsx index 815dd86197952..8b4376eb5c7f2 100644 --- a/frontend/src/metabase/browse/components/ModelsTable.tsx +++ b/frontend/src/metabase/browse/models/ModelsTable.tsx @@ -24,18 +24,17 @@ import { FixedSizeIcon, Flex, Icon, Skeleton } from "metabase/ui"; import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat"; import { SortDirection, type SortingOptions } from "metabase-types/api/sorting"; -import { trackModelClick } from "../analytics"; -import type { ModelResult } from "../types"; -import { getIcon } from "../utils"; - import { Cell, CollectionLink, CollectionTableCell, NameColumn, TableRow, -} from "./BrowseTable.styled"; -import { getModelDescription, sortModelOrMetric } from "./utils"; +} from "../components/BrowseTable.styled"; + +import { trackModelClick } from "./analytics"; +import type { ModelResult } from "./types"; +import { getIcon, getModelDescription, sortModels } from "./utils"; export interface ModelsTableProps { models?: ModelResult[]; @@ -69,7 +68,7 @@ export const ModelsTable = ({ ); const locale = useLocale(); - const sortedModels = sortModelOrMetric(models, sortingOptions, locale); + const sortedModels = sortModels(models, sortingOptions, locale); /** The name column has an explicitly set width. The remaining columns divide the remaining width. This is the percentage allocated to the collection column */ const collectionWidth = 38.5; diff --git a/frontend/src/metabase/browse/components/RecentModels.styled.tsx b/frontend/src/metabase/browse/models/RecentModels.styled.tsx similarity index 100% rename from frontend/src/metabase/browse/components/RecentModels.styled.tsx rename to frontend/src/metabase/browse/models/RecentModels.styled.tsx diff --git a/frontend/src/metabase/browse/components/RecentModels.tsx b/frontend/src/metabase/browse/models/RecentModels.tsx similarity index 96% rename from frontend/src/metabase/browse/components/RecentModels.tsx rename to frontend/src/metabase/browse/models/RecentModels.tsx index 66b750040c38d..c4aa34dd730b3 100644 --- a/frontend/src/metabase/browse/components/RecentModels.tsx +++ b/frontend/src/metabase/browse/models/RecentModels.tsx @@ -5,9 +5,8 @@ import { Box, Text } from "metabase/ui"; import { Repeat } from "metabase/ui/components/feedback/Skeleton/Repeat"; import type { RecentCollectionItem } from "metabase-types/api"; -import { trackModelClick } from "../analytics"; - import { RecentModelsGrid } from "./RecentModels.styled"; +import { trackModelClick } from "./analytics"; export function RecentModels({ models = [], diff --git a/frontend/src/metabase/browse/models/analytics.ts b/frontend/src/metabase/browse/models/analytics.ts new file mode 100644 index 0000000000000..f176dabfda3f9 --- /dev/null +++ b/frontend/src/metabase/browse/models/analytics.ts @@ -0,0 +1,8 @@ +import { trackSchemaEvent } from "metabase/lib/analytics"; +import type { CardId } from "metabase-types/api"; + +export const trackModelClick = (modelId: CardId) => + trackSchemaEvent("browse_data", { + event: "browse_data_model_clicked", + model_id: modelId, + }); diff --git a/frontend/src/metabase/browse/models/index.tsx b/frontend/src/metabase/browse/models/index.tsx new file mode 100644 index 0000000000000..5ac8edc8ec027 --- /dev/null +++ b/frontend/src/metabase/browse/models/index.tsx @@ -0,0 +1,7 @@ +export { BrowseModels } from "./BrowseModels"; +export type { + ModelFilterSettings, + ModelFilterControlsProps, + ModelResult, + RecentModel, +} from "./types"; diff --git a/frontend/src/metabase/browse/test-utils.ts b/frontend/src/metabase/browse/models/test-utils.ts similarity index 53% rename from frontend/src/metabase/browse/test-utils.ts rename to frontend/src/metabase/browse/models/test-utils.ts index 38ee0947246e2..c3b0e8c7635df 100644 --- a/frontend/src/metabase/browse/test-utils.ts +++ b/frontend/src/metabase/browse/models/test-utils.ts @@ -4,12 +4,7 @@ import { createMockSearchResult, } from "metabase-types/api/mocks"; -import type { - MetricResult, - ModelResult, - RecentMetric, - RecentModel, -} from "./types"; +import type { ModelResult, RecentModel } from "./types"; export const createMockModelResult = ( model: Partial = {}, @@ -20,16 +15,3 @@ export const createMockRecentModel = ( model: Partial, ): RecentModel => createMockRecentCollectionItem({ ...model, model: "dataset" }) as RecentModel; - -export const createMockMetricResult = ( - metric: Partial = {}, -): MetricResult => - createMockSearchResult({ ...metric, model: "metric" }) as MetricResult; - -export const createMockRecentMetric = ( - metric: Partial, -): RecentMetric => - createMockRecentCollectionItem({ - ...metric, - model: "metric", - }) as RecentMetric; diff --git a/frontend/src/metabase/browse/models/types.tsx b/frontend/src/metabase/browse/models/types.tsx new file mode 100644 index 0000000000000..ac0b4db850aaf --- /dev/null +++ b/frontend/src/metabase/browse/models/types.tsx @@ -0,0 +1,22 @@ +import type { RecentCollectionItem, SearchResult } from "metabase-types/api"; + +/** + * Model retrieved through the search endpoint + */ +export type ModelResult = SearchResult; + +/** + * Model retrieved through the recent views endpoint + */ +export interface RecentModel extends RecentCollectionItem { + model: "dataset"; +} + +export type ModelFilterSettings = { + verified?: boolean; +}; + +export type ModelFilterControlsProps = { + modelFilters: ModelFilterSettings; + setModelFilters: (settings: ModelFilterSettings) => void; +}; diff --git a/frontend/src/metabase/browse/models/utils.ts b/frontend/src/metabase/browse/models/utils.ts new file mode 100644 index 0000000000000..e35852bc23e26 --- /dev/null +++ b/frontend/src/metabase/browse/models/utils.ts @@ -0,0 +1,98 @@ +import { t } from "ttag"; +import _ from "underscore"; + +import { getCollectionPathAsString } from "metabase/collections/utils"; +import { entityForObject } from "metabase/lib/schema"; +import type { IconName } from "metabase/ui"; +import type { RecentItem, SearchResult } from "metabase-types/api"; +import { SortDirection, type SortingOptions } from "metabase-types/api/sorting"; + +import type { ModelResult, RecentModel } from "./types"; + +export const isModel = (item: SearchResult) => item.model === "dataset"; + +export const isRecentModel = (item: RecentItem): item is RecentModel => + item.model === "dataset"; + +export const getModelDescription = (item: ModelResult) => { + if (item.collection && !item.description?.trim()) { + return t`A model`; + } else { + return item.description; + } +}; + +const getValueForSorting = ( + model: ModelResult, + sort_column: keyof ModelResult, +): string => { + if (sort_column === "collection") { + return getCollectionPathAsString(model.collection) ?? ""; + } else { + return model[sort_column] ?? ""; + } +}; + +export const isValidSortColumn = ( + sort_column: string, +): sort_column is keyof ModelResult => { + return ["name", "collection", "description"].includes(sort_column); +}; + +export const getSecondarySortColumn = ( + sort_column: string, +): keyof ModelResult => { + return sort_column === "name" ? "collection" : "name"; +}; + +export function sortModels( + models: ModelResult[], + sortingOptions: SortingOptions, + localeCode: string = "en", +) { + const { sort_column, sort_direction } = sortingOptions; + + if (!isValidSortColumn(sort_column)) { + console.error("Invalid sort column", sort_column); + return models; + } + + const compare = (a: string, b: string) => + a.localeCompare(b, localeCode, { sensitivity: "base" }); + + return [...models].sort((modelA, modelB) => { + const a = getValueForSorting(modelA, sort_column); + const b = getValueForSorting(modelB, sort_column); + + let result = compare(a, b); + if (result === 0) { + const sort_column2 = getSecondarySortColumn(sort_column); + const a2 = getValueForSorting(modelA, sort_column2); + const b2 = getValueForSorting(modelB, sort_column2); + result = compare(a2, b2); + } + + return sort_direction === SortDirection.Asc ? result : -result; + }); +} + +/** Find the maximum number of recently viewed models to show. + * This is roughly proportional to the number of models the user + * has permission to see */ +export const getMaxRecentModelCount = ( + /** How many models the user has permission to see */ + modelCount: number, +) => { + if (modelCount > 20) { + return 8; + } + if (modelCount > 9) { + return 4; + } + return 0; +}; + +export const getIcon = (item: unknown): { name: IconName; color: string } => { + const entity = entityForObject(item); + return entity?.objectSelectors?.getIcon?.(item) || { name: "folder" }; +}; diff --git a/frontend/src/metabase/browse/models/utils.unit.spec.tsx b/frontend/src/metabase/browse/models/utils.unit.spec.tsx new file mode 100644 index 0000000000000..a33bd9014f674 --- /dev/null +++ b/frontend/src/metabase/browse/models/utils.unit.spec.tsx @@ -0,0 +1,200 @@ +import { createMockCollection } from "metabase-types/api/mocks"; +import { SortDirection } from "metabase-types/api/sorting"; + +import { createMockModelResult } from "./test-utils"; +import type { ModelResult } from "./types"; +import { getMaxRecentModelCount, sortModels } from "./utils"; + +describe("sortModels", () => { + let id = 0; + const modelMap: Record = { + "model named A, with collection path X / Y / Z": createMockModelResult({ + id: id++, + name: "A", + collection: createMockCollection({ + name: "Z", + effective_ancestors: [ + createMockCollection({ name: "X" }), + createMockCollection({ name: "Y" }), + ], + }), + }), + "model named C, with collection path Y": createMockModelResult({ + id: id++, + name: "C", + collection: createMockCollection({ name: "Y" }), + }), + "model named B, with collection path D / E / F": createMockModelResult({ + id: id++, + name: "B", + collection: createMockCollection({ + name: "F", + effective_ancestors: [ + createMockCollection({ name: "D" }), + createMockCollection({ name: "E" }), + ], + }), + }), + }; + const mockSearchResults = Object.values(modelMap); + + it("can sort by name in ascending order", () => { + const sortingOptions = { + sort_column: "name", + sort_direction: SortDirection.Asc, + } as const; + const sorted = sortModels(mockSearchResults, sortingOptions); + expect(sorted?.map(model => model.name)).toEqual(["A", "B", "C"]); + }); + + it("can sort by name in descending order", () => { + const sortingOptions = { + sort_column: "name", + sort_direction: SortDirection.Desc, + } as const; + const sorted = sortModels(mockSearchResults, sortingOptions); + expect(sorted?.map(model => model.name)).toEqual(["C", "B", "A"]); + }); + + it("can sort by collection path in ascending order", () => { + const sortingOptions = { + sort_column: "collection", + sort_direction: SortDirection.Asc, + } as const; + const sorted = sortModels(mockSearchResults, sortingOptions); + expect(sorted?.map(model => model.name)).toEqual(["B", "A", "C"]); + }); + + it("can sort by collection path in descending order", () => { + const sortingOptions = { + sort_column: "collection", + sort_direction: SortDirection.Desc, + } as const; + const sorted = sortModels(mockSearchResults, sortingOptions); + expect(sorted?.map(model => model.name)).toEqual(["C", "A", "B"]); + }); + + describe("secondary sort", () => { + modelMap["model named C, with collection path Z"] = createMockModelResult({ + name: "C", + collection: createMockCollection({ name: "Z" }), + }); + modelMap["model named Bz, with collection path D / E / F"] = + createMockModelResult({ + name: "Bz", + collection: createMockCollection({ + name: "F", + effective_ancestors: [ + createMockCollection({ name: "D" }), + createMockCollection({ name: "E" }), + ], + }), + }); + const mockSearchResults = Object.values(modelMap); + + it("can sort by collection path, ascending, and then does a secondary sort by name", () => { + const sortingOptions = { + sort_column: "collection", + sort_direction: SortDirection.Asc, + } as const; + const sorted = sortModels(mockSearchResults, sortingOptions); + expect(sorted).toEqual([ + modelMap["model named B, with collection path D / E / F"], + modelMap["model named Bz, with collection path D / E / F"], + modelMap["model named A, with collection path X / Y / Z"], + modelMap["model named C, with collection path Y"], + modelMap["model named C, with collection path Z"], + ]); + }); + + it("can sort by collection path, descending, and then does a secondary sort by name", () => { + const sortingOptions = { + sort_column: "collection", + sort_direction: SortDirection.Desc, + } as const; + const sorted = sortModels(mockSearchResults, sortingOptions); + expect(sorted).toEqual([ + modelMap["model named C, with collection path Z"], + modelMap["model named C, with collection path Y"], + modelMap["model named A, with collection path X / Y / Z"], + modelMap["model named Bz, with collection path D / E / F"], + modelMap["model named B, with collection path D / E / F"], + ]); + }); + + it("can sort by collection path, ascending, and then does a secondary sort by name - with a localized sort order", () => { + const sortingOptions = { + sort_column: "collection", + sort_direction: SortDirection.Asc, + } as const; + + const addUmlauts = (model: ModelResult): ModelResult => ({ + ...model, + name: model.name.replace(/^B$/g, "Bä"), + collection: { + ...model.collection, + effective_ancestors: model.collection?.effective_ancestors?.map( + ancestor => ({ + ...ancestor, + name: ancestor.name.replace("X", "Ä"), + }), + ), + }, + }); + + const swedishModelMap = { + "model named A, with collection path Ä / Y / Z": addUmlauts( + modelMap["model named A, with collection path X / Y / Z"], + ), + "model named Bä, with collection path D / E / F": addUmlauts( + modelMap["model named B, with collection path D / E / F"], + ), + "model named Bz, with collection path D / E / F": addUmlauts( + modelMap["model named Bz, with collection path D / E / F"], + ), + "model named C, with collection path Y": addUmlauts( + modelMap["model named C, with collection path Y"], + ), + "model named C, with collection path Z": addUmlauts( + modelMap["model named C, with collection path Z"], + ), + }; + + const swedishResults = Object.values(swedishModelMap); + + // When sorting in Swedish, z comes before ä + const swedishLocaleCode = "sv"; + const sorted = sortModels( + swedishResults, + sortingOptions, + swedishLocaleCode, + ); + expect("ä".localeCompare("z", "sv", { sensitivity: "base" })).toEqual(1); + expect(sorted).toEqual([ + swedishModelMap["model named Bz, with collection path D / E / F"], // Model Bz sorts before Bä + swedishModelMap["model named Bä, with collection path D / E / F"], + swedishModelMap["model named C, with collection path Y"], + swedishModelMap["model named C, with collection path Z"], // Collection Z sorts before Ä + swedishModelMap["model named A, with collection path Ä / Y / Z"], + ]); + }); + }); +}); + +describe("getMaxRecentModelCount", () => { + it("returns 8 for modelCount greater than 20", () => { + expect(getMaxRecentModelCount(21)).toBe(8); + expect(getMaxRecentModelCount(100)).toBe(8); + }); + + it("returns 4 for modelCount greater than 9 and less than or equal to 20", () => { + expect(getMaxRecentModelCount(10)).toBe(4); + expect(getMaxRecentModelCount(20)).toBe(4); + }); + + it("returns 0 for modelCount of 9 or less", () => { + expect(getMaxRecentModelCount(0)).toBe(0); + expect(getMaxRecentModelCount(5)).toBe(0); + expect(getMaxRecentModelCount(9)).toBe(0); + }); +}); diff --git a/frontend/src/metabase/browse/components/BrowseSchemas.styled.tsx b/frontend/src/metabase/browse/schemas/BrowseSchemas.styled.tsx similarity index 100% rename from frontend/src/metabase/browse/components/BrowseSchemas.styled.tsx rename to frontend/src/metabase/browse/schemas/BrowseSchemas.styled.tsx diff --git a/frontend/src/metabase/browse/components/BrowseSchemas.tsx b/frontend/src/metabase/browse/schemas/BrowseSchemas.tsx similarity index 90% rename from frontend/src/metabase/browse/components/BrowseSchemas.tsx rename to frontend/src/metabase/browse/schemas/BrowseSchemas.tsx index b59c8cc706c87..1c9922ffec704 100644 --- a/frontend/src/metabase/browse/components/BrowseSchemas.tsx +++ b/frontend/src/metabase/browse/schemas/BrowseSchemas.tsx @@ -16,12 +16,13 @@ import { BrowseContainer, BrowseMain, BrowseSection, -} from "./BrowseContainer.styled"; -import { BrowseDataHeader } from "./BrowseDataHeader"; -import { BrowseHeaderContent } from "./BrowseHeader.styled"; +} from "../components/BrowseContainer.styled"; +import { BrowseDataHeader } from "../components/BrowseDataHeader"; +import { BrowseHeaderContent } from "../components/BrowseHeader.styled"; + import { SchemaGridItem, SchemaLink } from "./BrowseSchemas.styled"; -const BrowseSchemas = ({ +const BrowseSchemasContainer = ({ schemas, params, }: { @@ -90,9 +91,8 @@ const BrowseSchemas = ({ ); }; -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default Schema.loadList({ +export const BrowseSchemas = Schema.loadList({ query: (state: any, { params: { slug } }: { params: { slug: string } }) => ({ dbId: Urls.extractEntityId(slug), }), -})(BrowseSchemas); +})(BrowseSchemasContainer); diff --git a/frontend/src/metabase/browse/schemas/index.tsx b/frontend/src/metabase/browse/schemas/index.tsx new file mode 100644 index 0000000000000..36acba3f3707b --- /dev/null +++ b/frontend/src/metabase/browse/schemas/index.tsx @@ -0,0 +1 @@ +export { BrowseSchemas } from "./BrowseSchemas"; diff --git a/frontend/src/metabase/browse/components/BrowseTables.tsx b/frontend/src/metabase/browse/tables/BrowseTables.tsx similarity index 81% rename from frontend/src/metabase/browse/components/BrowseTables.tsx rename to frontend/src/metabase/browse/tables/BrowseTables.tsx index 9007610cfe966..409fe3104f074 100644 --- a/frontend/src/metabase/browse/components/BrowseTables.tsx +++ b/frontend/src/metabase/browse/tables/BrowseTables.tsx @@ -1,11 +1,10 @@ -import TableBrowser from "../containers/TableBrowser"; - import { BrowseContainer, BrowseMain, BrowseSection, -} from "./BrowseContainer.styled"; -import { BrowseDataHeader } from "./BrowseDataHeader"; +} from "../components/BrowseContainer.styled"; +import { BrowseDataHeader } from "../components/BrowseDataHeader"; +import TableBrowser from "../containers/TableBrowser"; export const BrowseTables = ({ params: { dbId, schemaName }, diff --git a/frontend/src/metabase/browse/components/TableBrowser/TableBrowser.jsx b/frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.jsx similarity index 97% rename from frontend/src/metabase/browse/components/TableBrowser/TableBrowser.jsx rename to frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.jsx index 468c9515960df..60358f0368585 100644 --- a/frontend/src/metabase/browse/components/TableBrowser/TableBrowser.jsx +++ b/frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.jsx @@ -14,8 +14,8 @@ import { isVirtualCardId, } from "metabase-lib/v1/metadata/utils/saved-questions"; -import { trackTableClick } from "../../analytics"; -import { BrowseHeaderContent } from "../BrowseHeader.styled"; +import { BrowseHeaderContent } from "../../components/BrowseHeader.styled"; +import { trackTableClick } from "../analytics"; import { TableActionLink, diff --git a/frontend/src/metabase/browse/components/TableBrowser/TableBrowser.styled.tsx b/frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.styled.tsx similarity index 100% rename from frontend/src/metabase/browse/components/TableBrowser/TableBrowser.styled.tsx rename to frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.styled.tsx diff --git a/frontend/src/metabase/browse/components/TableBrowser/TableBrowser.unit.spec.js b/frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.unit.spec.js similarity index 100% rename from frontend/src/metabase/browse/components/TableBrowser/TableBrowser.unit.spec.js rename to frontend/src/metabase/browse/tables/TableBrowser/TableBrowser.unit.spec.js diff --git a/frontend/src/metabase/browse/components/TableBrowser/index.js b/frontend/src/metabase/browse/tables/TableBrowser/index.js similarity index 100% rename from frontend/src/metabase/browse/components/TableBrowser/index.js rename to frontend/src/metabase/browse/tables/TableBrowser/index.js diff --git a/frontend/src/metabase/browse/analytics.ts b/frontend/src/metabase/browse/tables/analytics.ts similarity index 50% rename from frontend/src/metabase/browse/analytics.ts rename to frontend/src/metabase/browse/tables/analytics.ts index ffa3dcd1c64a1..0e2fb444b7f90 100644 --- a/frontend/src/metabase/browse/analytics.ts +++ b/frontend/src/metabase/browse/tables/analytics.ts @@ -1,11 +1,5 @@ import { trackSchemaEvent } from "metabase/lib/analytics"; -import type { CardId, ConcreteTableId } from "metabase-types/api"; - -export const trackModelClick = (modelId: CardId) => - trackSchemaEvent("browse_data", { - event: "browse_data_model_clicked", - model_id: modelId, - }); +import type { ConcreteTableId } from "metabase-types/api"; export const trackTableClick = (tableId: ConcreteTableId) => trackSchemaEvent("browse_data", { diff --git a/frontend/src/metabase/browse/tables/index.tsx b/frontend/src/metabase/browse/tables/index.tsx new file mode 100644 index 0000000000000..0d1921cbd3703 --- /dev/null +++ b/frontend/src/metabase/browse/tables/index.tsx @@ -0,0 +1 @@ +export { BrowseTables } from "./BrowseTables"; diff --git a/frontend/src/metabase/browse/types.tsx b/frontend/src/metabase/browse/types.tsx deleted file mode 100644 index 7ecc3a879b6ab..0000000000000 --- a/frontend/src/metabase/browse/types.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { - RecentCollectionItem, - RecentItem, - SearchResult, -} from "metabase-types/api"; - -/** Model retrieved through the search endpoint */ -export type ModelResult = SearchResult; - -/** Model retrieved through the recent views endpoint */ -export interface RecentModel extends RecentCollectionItem { - model: "dataset"; -} - -export const isRecentModel = (item: RecentItem): item is RecentModel => - item.model === "dataset"; - -/** A model retrieved through either endpoint. - * This type is needed so that our filtering functions can - * filter arrays of models retrieved from either endpoint. */ -export type FilterableModel = ModelResult | RecentModel; - -/** Metric retrieved through the search endpoint */ -export type MetricResult = SearchResult; - -export interface RecentMetric extends RecentCollectionItem { - model: "metric"; -} diff --git a/frontend/src/metabase/browse/utils.ts b/frontend/src/metabase/browse/utils.ts deleted file mode 100644 index 4d0f22bf4100b..0000000000000 --- a/frontend/src/metabase/browse/utils.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { Dispatch, SetStateAction } from "react"; -import { t } from "ttag"; -import _ from "underscore"; - -import { - canonicalCollectionId, - coerceCollectionId, - isRootCollection, -} from "metabase/collections/utils"; -import { entityForObject } from "metabase/lib/schema"; -import type { IconName } from "metabase/ui"; -import type { CollectionEssentials } from "metabase-types/api"; - -import type { FilterableModel } from "./types"; - -export const getCollectionName = (collection: CollectionEssentials) => { - if (isRootCollection(collection)) { - return t`Our analytics`; - } - return collection?.name || t`Untitled collection`; -}; - -/** The root collection's id might be null or 'root' in different contexts. - * Use 'root' instead of null, for the sake of sorting */ -export const getCollectionIdForSorting = (collection: CollectionEssentials) => { - return coerceCollectionId(canonicalCollectionId(collection.id)); -}; - -export type AvailableModelFilters = Record< - string, - { - predicate: (value: FilterableModel) => boolean; - activeByDefault: boolean; - } ->; - -export type ModelFilterControlsProps = { - actualModelFilters: ActualModelFilters; - setActualModelFilters: Dispatch>; -}; - -export type MetricFilterSettings = { - verified?: boolean; -}; - -export type MetricFilterControlsProps = { - metricFilters: MetricFilterSettings; - setMetricFilters: (settings: MetricFilterSettings) => void; -}; - -/** Mapping of filter names to true if the filter is active - * or false if it is inactive */ -export type ActualModelFilters = Record; - -export const filterModels = ( - unfilteredModels: T[] | undefined, - actualModelFilters: ActualModelFilters, - availableModelFilters: AvailableModelFilters, -): T[] => { - return _.reduce( - actualModelFilters, - (acc, shouldFilterBeActive, filterName) => - shouldFilterBeActive - ? acc.filter(availableModelFilters[filterName].predicate) - : acc, - unfilteredModels || [], - ); -}; - -export const getIcon = (item: unknown): { name: IconName; color: string } => { - const entity = entityForObject(item); - return entity?.objectSelectors?.getIcon?.(item) || { name: "folder" }; -}; diff --git a/frontend/src/metabase/browse/utils.unit.spec.ts b/frontend/src/metabase/browse/utils.unit.spec.ts deleted file mode 100644 index 97e058c0684dc..0000000000000 --- a/frontend/src/metabase/browse/utils.unit.spec.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { defaultRootCollection } from "metabase/admin/permissions/pages/CollectionPermissionsPage/tests/setup"; -import type { SearchResult } from "metabase-types/api"; -import { createMockCollection } from "metabase-types/api/mocks"; - -import { createMockModelResult } from "./test-utils"; -import type { ModelResult } from "./types"; -import type { ActualModelFilters, AvailableModelFilters } from "./utils"; -import { filterModels } from "./utils"; - -const collectionAlpha = createMockCollection({ id: 0, name: "Alpha" }); -const collectionBeta = createMockCollection({ id: 1, name: "Beta" }); -const collectionCharlie = createMockCollection({ id: 2, name: "Charlie" }); -const collectionDelta = createMockCollection({ id: 3, name: "Delta" }); -const collectionZulu = createMockCollection({ id: 4, name: "Zulu" }); -const collectionAngstrom = createMockCollection({ id: 5, name: "Ångström" }); -const collectionOzgur = createMockCollection({ id: 6, name: "Özgür" }); - -const mockModels: ModelResult[] = [ - { - id: 0, - name: "Model 0", - collection: collectionAlpha, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-15T11:59:59.000Z", - }, - { - id: 1, - name: "Model 1", - collection: collectionAlpha, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-15T11:59:30.000Z", - }, - { - id: 2, - name: "Model 2", - collection: collectionAlpha, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-15T11:59:00.000Z", - }, - { - id: 3, - name: "Model 3", - collection: collectionBeta, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-15T11:50:00.000Z", - }, - { - id: 4, - name: "Model 4", - collection: collectionBeta, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-15T11:00:00.000Z", - }, - { - id: 5, - name: "Model 5", - collection: collectionBeta, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-14T22:00:00.000Z", - }, - { - id: 6, - name: "Model 6", - collection: collectionCharlie, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-14T12:00:00.000Z", - }, - { - id: 7, - name: "Model 7", - collection: collectionCharlie, - last_editor_common_name: "Bobby", - last_edited_at: "2024-12-10T12:00:00.000Z", - }, - { - id: 8, - name: "Model 8", - collection: collectionCharlie, - last_editor_common_name: "Bobby", - last_edited_at: "2024-11-15T12:00:00.000Z", - }, - { - id: 9, - name: "Model 9", - collection: collectionDelta, - last_editor_common_name: "Bobby", - last_edited_at: "2024-02-15T12:00:00.000Z", - }, - { - id: 10, - name: "Model 10", - collection: collectionDelta, - last_editor_common_name: "Bobby", - last_edited_at: "2023-12-15T12:00:00.000Z", - }, - { - id: 11, - name: "Model 11", - collection: collectionDelta, - last_editor_common_name: "Bobby", - last_edited_at: "2020-01-01T00:00:00.000Z", - }, - { - id: 12, - name: "Model 12", - collection: collectionZulu, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 13, - name: "Model 13", - collection: collectionZulu, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 14, - name: "Model 14", - collection: collectionZulu, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 15, - name: "Model 15", - collection: collectionAngstrom, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 16, - name: "Model 16", - collection: collectionAngstrom, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 17, - name: "Model 17", - collection: collectionAngstrom, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 18, - name: "Model 18", - collection: collectionOzgur, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 19, - name: "Model 19", - collection: collectionOzgur, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 20, - name: "Model 20", - collection: collectionOzgur, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 21, - name: "Model 20", - collection: defaultRootCollection, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, - { - id: 22, - name: "Model 21", - collection: defaultRootCollection, - last_editor_common_name: "Bobby", - last_edited_at: "2000-01-01T00:00:00.000Z", - }, -].map(model => createMockModelResult(model)); - -describe("Browse utils", () => { - const diverseModels = mockModels.map((model, index) => ({ - ...model, - name: index % 2 === 0 ? `red ${index}` : `blue ${index}`, - moderated_status: index % 3 === 0 ? `good ${index}` : `bad ${index}`, - })); - const availableModelFilters: AvailableModelFilters = { - onlyRed: { - predicate: model => model.name.startsWith("red"), - activeByDefault: false, - }, - onlyGood: { - predicate: model => Boolean(model.moderated_status?.startsWith("good")), - activeByDefault: false, - }, - onlyBig: { - predicate: model => Boolean(model.description?.startsWith("big")), - activeByDefault: true, - }, - }; - - it("include a function that filters models, based on the object provided", () => { - const onlyRedAndGood: ActualModelFilters = { - onlyRed: true, - onlyGood: true, - onlyBig: false, - }; - const onlyRedAndGoodModels = filterModels( - diverseModels, - onlyRedAndGood, - availableModelFilters, - ); - const everySixthModel = diverseModels.reduce( - (acc, model, index) => { - return index % 6 === 0 ? [...acc, model] : acc; - }, - [], - ); - // Since every other model is red and every third model is good, - // we expect every sixth model to be both red and good - expect(onlyRedAndGoodModels).toEqual(everySixthModel); - }); - - it("filterModels does not filter out models if no filters are active", () => { - const noActiveFilters: ActualModelFilters = { - onlyRed: false, - onlyGood: false, - onlyBig: false, - }; - const filteredModels = filterModels( - diverseModels, - noActiveFilters, - availableModelFilters, - ); - expect(filteredModels).toEqual(diverseModels); - }); -}); diff --git a/frontend/src/metabase/collections/components/CollectionHeader/tests/enterprise.unit.spec.tsx b/frontend/src/metabase/collections/components/CollectionHeader/tests/enterprise.unit.spec.tsx index a5f900ba6b0f1..0293bdaa1fc72 100644 --- a/frontend/src/metabase/collections/components/CollectionHeader/tests/enterprise.unit.spec.tsx +++ b/frontend/src/metabase/collections/components/CollectionHeader/tests/enterprise.unit.spec.tsx @@ -8,7 +8,7 @@ import { setup } from "./setup"; describe("Instance Analytics Collection Header", () => { const defaultOptions = { collection: { - name: "Metabase Analytics", + name: "Usage Analytics", type: "instance-analytics" as CollectionType, can_write: false, }, diff --git a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx index c8cb966b6a9d5..81563327e643f 100644 --- a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx +++ b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx @@ -1,7 +1,7 @@ import { action } from "@storybook/addon-actions"; -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import PinnedItemCard from "./PinnedItemCard"; +import PinnedItemCard, { type PinnedItemCardProps } from "./PinnedItemCard"; export default { title: "Collections/PinnedItemCard", @@ -22,70 +22,79 @@ const collection = { const onCopy = action("onCopy"); const onMove = action("onMove"); -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Question = Template.bind({}); -Question.args = { - collection, - item: { - id: 1, - collection_position: 1, - collection_id: null, - model: "card", - name: "Question", - description: "This is a description of the question", - getIcon: () => ({ name: "question" }), - getUrl: () => "/question/1", - setArchived: action("setArchived"), - setPinned: action("setPinned"), - copy: true, - setCollection: action("setCollection"), - archived: false, +export const Question = { + render: Template, + + args: { + collection, + item: { + id: 1, + collection_position: 1, + collection_id: null, + model: "card", + name: "Question", + description: "This is a description of the question", + getIcon: () => ({ name: "question" }), + getUrl: () => "/question/1", + setArchived: action("setArchived"), + setPinned: action("setPinned"), + copy: true, + setCollection: action("setCollection"), + archived: false, + }, + onCopy, + onMove, }, - onCopy, - onMove, }; -export const Dashboard = Template.bind({}); -Dashboard.args = { - collection, - item: { - id: 1, - model: "dashboard", - collection_position: 1, - collection_id: null, - name: "Dashboard", - description: Array(20) - .fill("This is a description of the dashboard.") - .join(" "), - getIcon: () => ({ name: "dashboard" }), - getUrl: () => "/dashboard/1", - setArchived: action("setArchived"), - setPinned: action("setPinned"), - archived: false, +export const Dashboard = { + render: Template, + + args: { + collection, + item: { + id: 1, + model: "dashboard", + collection_position: 1, + collection_id: null, + name: "Dashboard", + description: Array(20) + .fill("This is a description of the dashboard.") + .join(" "), + getIcon: () => ({ name: "dashboard" }), + getUrl: () => "/dashboard/1", + setArchived: action("setArchived"), + setPinned: action("setPinned"), + archived: false, + }, + onCopy, + onMove, }, - onCopy, - onMove, }; -export const Model = Template.bind({}); -Model.args = { - collection, - item: { - id: 1, - model: "dataset", - collection_position: 1, - collection_id: null, - name: "Model", - description: "This is a description of the model", - getIcon: () => ({ name: "model" }), - getUrl: () => "/question/1", - setArchived: action("setArchived"), - setPinned: action("setPinned"), - archived: false, +export const Model = { + render: Template, + + args: { + collection, + item: { + id: 1, + model: "dataset", + collection_position: 1, + collection_id: null, + name: "Model", + description: "This is a description of the model", + getIcon: () => ({ name: "model" }), + getUrl: () => "/question/1", + setArchived: action("setArchived"), + setPinned: action("setPinned"), + archived: false, + }, + onCopy, + onMove, }, - onCopy, - onMove, }; diff --git a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.tsx b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.tsx index 66e6885e9a647..eca0615b06b28 100644 --- a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.tsx +++ b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.tsx @@ -45,7 +45,7 @@ type ItemOrSkeleton = iconForSkeleton: IconName; }; -type Props = { +export type PinnedItemCardProps = { databases?: Database[]; bookmarks?: Bookmark[]; createBookmark?: CreateBookmark; @@ -84,7 +84,7 @@ function PinnedItemCard({ onMove, onClick, iconForSkeleton, -}: Props) { +}: PinnedItemCardProps) { const [showTitleTooltip, setShowTitleTooltip] = useState(false); const icon = iconForSkeleton ?? diff --git a/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.unit.spec.tsx b/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.unit.spec.tsx index 32d5cd1ec9b79..e3c5cd305f7a1 100644 --- a/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.unit.spec.tsx +++ b/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.unit.spec.tsx @@ -414,7 +414,8 @@ describe("AggregationPicker", () => { }); }); - describe("column compare shortcut", () => { + // eslint-disable-next-line jest/no-disabled-tests + describe.skip("column compare shortcut", () => { it("does not display the shortcut if there are no aggregations", () => { setup({ allowCustomExpressions: true, allowTemporalComparisons: true }); expect(screen.queryByText(/compare/i)).not.toBeInTheDocument(); diff --git a/frontend/src/metabase/common/components/Sidesheet/Sidesheet.mdx b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.mdx new file mode 100644 index 0000000000000..ac08230a5b236 --- /dev/null +++ b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.mdx @@ -0,0 +1,60 @@ +import { Canvas, Story, Meta } from "@storybook/blocks"; +import { Box, Flex } from "metabase/ui"; +import * as SidesheetStories from "./Sidesheet.stories"; + +import { Sidesheet } from "./Sidesheet"; +import { SidesheetCard } from "./SidesheetCard"; +import { SidesheetCardSection } from "./SidesheetCardSection"; +import { SidesheetButton, SidesheetButtonWithChevron } from "./SidesheetButton"; + +import { TestTabbedSidesheet, TestPagedSidesheet } from "./Sidesheet.samples"; + + + +# Sidesheet + +## When to use a Sidesheet + +## Docs + +## Caveats + +## Usage guidelines + +## Examples + +// TODO: figure out how to get CSS modules working with storybook 🔥 + + + + + +### With cards + + + + + +### With sectioned cards + + + + + +### With pages + + + + + +### With tabs + + + + + +### Sidesheet Buttons + + + + diff --git a/frontend/src/metabase/common/components/Sidesheet/Sidesheet.stories.mdx b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.stories.mdx deleted file mode 100644 index 1a9348b3393e4..0000000000000 --- a/frontend/src/metabase/common/components/Sidesheet/Sidesheet.stories.mdx +++ /dev/null @@ -1,165 +0,0 @@ -import { Canvas, Story, Meta } from "@storybook/addon-docs"; -import { Box, Flex } from "metabase/ui"; - -import { Sidesheet } from "./Sidesheet"; -import { SidesheetCard } from "./SidesheetCard"; -import { SidesheetCardSection } from "./SidesheetCardSection"; -import { SidesheetButton, SidesheetButtonWithChevron } from "./SidesheetButton"; - -import { TestTabbedSidesheet, TestPagedSidesheet} from "./Sidesheet.samples"; - -export const args = { - size: "md", - title: "My Awesome Sidesheet", - onClose: () => {}, - isOpen: true, -}; - -export const argTypes = { - size: { - options: ["xs", "sm", "md", "lg", "xl", "auto"], - control: { type: "inline-radio" }, - }, - title: { - control: { type: "text" }, - }, - isOpen: { - control: { type: "boolean" }, - } -}; - - - -# Sidesheet - - -## When to use a Sidesheet - - - -## Docs - - -## Caveats - - - -## Usage guidelines - - - -## Examples - -// TODO: figure out how to get CSS modules working with storybook 🔥 - -export const DefaultTemplate = args => ( - - Call me Ishmael ... - -); - -export const WithCardsTemplate = args => ( - - - Here is even more cool information - - - titles are neat - - -); - -export const WithSectionedCardsTemplate = args => ( - - - - Some cards have so much information - - - that you need a bunch - - - of sections to display it all - - - -); - -export const PagedSidesheetTemplate = () => ( - -); - -export const TabbedSidesheetTemplate = () => ( - -); - - -export const SidesheetButtonTemplate = () => ( - - - - Do something fun - - - - - Favorite Pokemon - - Naclstack - - - - - - Configure favorite pokemon - - - -); - -export const Default = DefaultTemplate.bind({}); - - - {Default} - - -### With cards - -export const WithCards = WithCardsTemplate.bind({}); - - - {WithCards} - - - -### With sectioned cards - -export const WithSectionedCards = WithSectionedCardsTemplate.bind({}); - - - {WithSectionedCards} - - -### With pages - -export const WithPages = PagedSidesheetTemplate.bind({}); - - - {WithPages} - - -### With tabs - -export const WithTabs = TabbedSidesheetTemplate.bind({}); - - - {WithTabs} - - -### Sidesheet Buttons - -export const SidesheetButtonStory = SidesheetButtonTemplate.bind({}); - - - {SidesheetButtonStory} - diff --git a/frontend/src/metabase/common/components/Sidesheet/Sidesheet.stories.tsx b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.stories.tsx new file mode 100644 index 0000000000000..4fe79f4d90dd3 --- /dev/null +++ b/frontend/src/metabase/common/components/Sidesheet/Sidesheet.stories.tsx @@ -0,0 +1,120 @@ +import type { ComponentProps } from "react"; + +import { Flex } from "metabase/ui"; + +import { Sidesheet } from "./Sidesheet"; +import { TestPagedSidesheet, TestTabbedSidesheet } from "./Sidesheet.samples"; +import { SidesheetButton, SidesheetButtonWithChevron } from "./SidesheetButton"; +import { SidesheetCard } from "./SidesheetCard"; +import { SidesheetCardSection } from "./SidesheetCardSection"; + +const args = { + size: "md", + title: "My Awesome Sidesheet", + onClose: () => {}, + isOpen: true, +}; + +const argTypes = { + size: { + options: ["xs", "sm", "md", "lg", "xl", "auto"], + control: { type: "inline-radio" }, + }, + title: { + control: { type: "text" }, + }, + isOpen: { + control: { type: "boolean" }, + }, +}; + +type SidesheetProps = ComponentProps; + +const DefaultTemplate = (args: SidesheetProps) => ( + Call me Ishmael ... +); + +const WithCardsTemplate = (args: SidesheetProps) => ( + + Here is even more cool information + + titles are neat + + +); + +const WithSectionedCardsTemplate = (args: SidesheetProps) => ( + + + + Some cards have so much information + + + that you need a bunch + + + of sections to display it all + + + +); + +const PagedSidesheetTemplate = () => ; + +const TabbedSidesheetTemplate = () => ; + +const SidesheetButtonTemplate = () => ( + + + Do something fun + + + + Favorite Pokemon + Naclstack + + + + + Configure favorite pokemon + + + +); + +export default { + title: "Components/Sidesheet", + component: Sidesheet, + args, + argTypes, +}; + +export const Default = { + render: DefaultTemplate, + name: "Default", +}; + +export const WithCards = { + render: WithCardsTemplate, + name: "With cards", +}; + +export const WithSectionedCards = { + render: WithSectionedCardsTemplate, + name: "With sectioned cards", +}; + +export const WithSubPages = { + render: PagedSidesheetTemplate, + name: "With sub pages", +}; + +export const WithTabs = { + render: TabbedSidesheetTemplate, + name: "With tabs", +}; + +export const SidesheetButtons = { + render: SidesheetButtonTemplate, + name: "Sidesheet buttons", +}; diff --git a/frontend/src/metabase/common/hooks/use-fetch-models.tsx b/frontend/src/metabase/common/hooks/use-fetch-models.tsx index 3a52f8feeef1f..931e4d9ca83a0 100644 --- a/frontend/src/metabase/common/hooks/use-fetch-models.tsx +++ b/frontend/src/metabase/common/hooks/use-fetch-models.tsx @@ -1,12 +1,18 @@ -import { useSearchQuery } from "metabase/api"; +import { skipToken, useSearchQuery } from "metabase/api"; import type { SearchRequest } from "metabase-types/api"; -export const useFetchModels = (req: Partial = {}) => { - const modelsResult = useSearchQuery({ - models: ["dataset"], // 'model' in the sense of 'type of thing' - filter_items_in_personal_collection: "exclude", - model_ancestors: false, - ...req, - }); +export const useFetchModels = ( + req: Partial | typeof skipToken = {}, +) => { + const modelsResult = useSearchQuery( + req === skipToken + ? req + : { + models: ["dataset"], // 'model' in the sense of 'type of thing' + filter_items_in_personal_collection: "exclude", + model_ancestors: false, + ...req, + }, + ); return modelsResult; }; diff --git a/frontend/src/metabase/components/Calendar/Calendar.stories.tsx b/frontend/src/metabase/components/Calendar/Calendar.stories.tsx index 9ca7f37f9ace5..543f3cd28e7d1 100644 --- a/frontend/src/metabase/components/Calendar/Calendar.stories.tsx +++ b/frontend/src/metabase/components/Calendar/Calendar.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import Calendar from "./Calendar"; @@ -7,9 +7,11 @@ export default { component: Calendar, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = {}; +export const Default = { + render: Template, + args: {}, +}; diff --git a/frontend/src/metabase/components/DateMonthYearWidget/DateMonthYearWidget.stories.tsx b/frontend/src/metabase/components/DateMonthYearWidget/DateMonthYearWidget.stories.tsx index 3560f1a04c778..b373787865b7e 100644 --- a/frontend/src/metabase/components/DateMonthYearWidget/DateMonthYearWidget.stories.tsx +++ b/frontend/src/metabase/components/DateMonthYearWidget/DateMonthYearWidget.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/client-api"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { DateMonthYearWidget } from "./DateMonthYearWidget"; @@ -8,7 +8,7 @@ export default { component: DateMonthYearWidget, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleSetValue = (v: string) => { @@ -29,17 +29,26 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - value: "", +export const Default = { + render: Template, + + args: { + value: "", + }, }; -export const ThisYear = Template.bind({}); -ThisYear.args = { - value: "2022", +export const ThisYear = { + render: Template, + + args: { + value: "2022", + }, }; -export const LastYear = Template.bind({}); -LastYear.args = { - value: "2021-07", +export const LastYear = { + render: Template, + + args: { + value: "2021-07", + }, }; diff --git a/frontend/src/metabase/components/DateQuarterYearWidget/DateQuarterYearWidget.stories.tsx b/frontend/src/metabase/components/DateQuarterYearWidget/DateQuarterYearWidget.stories.tsx index 035c0e1a622c3..b0270e56135bf 100644 --- a/frontend/src/metabase/components/DateQuarterYearWidget/DateQuarterYearWidget.stories.tsx +++ b/frontend/src/metabase/components/DateQuarterYearWidget/DateQuarterYearWidget.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { DateQuarterYearWidget } from "./DateQuarterYearWidget"; @@ -8,7 +8,7 @@ export default { component: DateQuarterYearWidget, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleSetValue = (v: string) => { @@ -29,17 +29,26 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - value: "", +export const Default = { + render: Template, + + args: { + value: "", + }, }; -export const SomeTimeLastYear = Template.bind({}); -SomeTimeLastYear.args = { - value: "4-2021", +export const SomeTimeLastYear = { + render: Template, + + args: { + value: "4-2021", + }, }; -export const SomeTimeAgo = Template.bind({}); -SomeTimeAgo.args = { - value: "2-1981", +export const SomeTimeAgo = { + render: Template, + + args: { + value: "2-1981", + }, }; diff --git a/frontend/src/metabase/components/DateRelativeWidget/DateRelativeWidget.stories.tsx b/frontend/src/metabase/components/DateRelativeWidget/DateRelativeWidget.stories.tsx index 6c7eadbc8097a..7d25bdd57455b 100644 --- a/frontend/src/metabase/components/DateRelativeWidget/DateRelativeWidget.stories.tsx +++ b/frontend/src/metabase/components/DateRelativeWidget/DateRelativeWidget.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { DateRelativeWidget } from "./DateRelativeWidget"; @@ -8,7 +8,7 @@ export default { component: DateRelativeWidget, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleSetValue = (v?: string) => { @@ -29,22 +29,34 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - value: "", +export const Default = { + render: Template, + + args: { + value: "", + }, }; -export const Yesterday = Template.bind({}); -Yesterday.args = { - value: "yesterday", +export const Yesterday = { + render: Template, + + args: { + value: "yesterday", + }, }; -export const LastMonth = Template.bind({}); -LastMonth.args = { - value: "lastmonth", +export const LastMonth = { + render: Template, + + args: { + value: "lastmonth", + }, }; -export const ThisWeek = Template.bind({}); -ThisWeek.args = { - value: "thisweek", +export const ThisWeek = { + render: Template, + + args: { + value: "thisweek", + }, }; diff --git a/frontend/src/metabase/components/EntityMenu.stories.tsx b/frontend/src/metabase/components/EntityMenu.stories.tsx index db3bcef951d45..2e554217aa99e 100644 --- a/frontend/src/metabase/components/EntityMenu.stories.tsx +++ b/frontend/src/metabase/components/EntityMenu.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import EntityMenu from "./EntityMenu"; @@ -7,7 +7,7 @@ export default { component: EntityMenu, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; @@ -30,8 +30,11 @@ const items = [ }, ]; -export const Default = Template.bind({}); -Default.args = { - items, - trigger: Click Me, +export const Default = { + render: Template, + + args: { + items, + trigger: Click Me, + }, }; diff --git a/frontend/src/metabase/components/HelpCard/HelpCard.stories.tsx b/frontend/src/metabase/components/HelpCard/HelpCard.stories.tsx index 9e014945d2c21..25c02f9ffb3c0 100644 --- a/frontend/src/metabase/components/HelpCard/HelpCard.stories.tsx +++ b/frontend/src/metabase/components/HelpCard/HelpCard.stories.tsx @@ -1,20 +1,23 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import HelpCard from "./HelpCard"; +import HelpCard, { type HelpCardProps } from "./HelpCard"; export default { title: "Components/HelpCard", component: HelpCard, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - title: "Need help with anything?", - helpUrl: "https://metabase.com", - children: - "See our docs for step-by-step directions on how to do what you need.", +export const Default = { + render: Template, + + args: { + title: "Need help with anything?", + helpUrl: "https://metabase.com", + children: + "See our docs for step-by-step directions on how to do what you need.", + }, }; diff --git a/frontend/src/metabase/components/ModalContent/ModalContent.stories.tsx b/frontend/src/metabase/components/ModalContent/ModalContent.stories.tsx index f03e257be8c2b..c2281e4c723b9 100644 --- a/frontend/src/metabase/components/ModalContent/ModalContent.stories.tsx +++ b/frontend/src/metabase/components/ModalContent/ModalContent.stories.tsx @@ -1,9 +1,12 @@ import { action } from "@storybook/addon-actions"; -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { color } from "metabase/lib/colors"; -import ModalContent, { ModalContentActionIcon } from "./index"; +import ModalContent, { + ModalContentActionIcon, + type ModalContentProps, +} from "./index"; export default { title: "Components/ModalContent", @@ -24,7 +27,7 @@ export default { }, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return (
- - - ), +export const WithHeaderActions = { + render: Template, + + args: { + ...args, + headerActions: ( + <> + + + ), + }, }; -export const WithBackButton = Template.bind({}); -WithBackButton.args = { - ...args, - onBack: action("onBack"), +export const WithBackButton = { + render: Template, + + args: { + ...args, + onBack: action("onBack"), + }, }; diff --git a/frontend/src/metabase/components/Schedule/Schedule.stories.tsx b/frontend/src/metabase/components/Schedule/Schedule.stories.tsx index fdce96762fc5b..e4b6a130926dd 100644 --- a/frontend/src/metabase/components/Schedule/Schedule.stories.tsx +++ b/frontend/src/metabase/components/Schedule/Schedule.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { Schedule } from "./Schedule"; @@ -8,7 +8,7 @@ export default { component: Schedule, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [ { schedule, @@ -29,13 +29,16 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - schedule: { - schedule_day: "mon", - schedule_frame: null, - schedule_hour: 0, - schedule_type: "daily", +export const Default = { + render: Template, + + args: { + schedule: { + schedule_day: "mon", + schedule_frame: null, + schedule_hour: 0, + schedule_type: "daily", + }, + verb: "Deliver", }, - verb: "Deliver", }; diff --git a/frontend/src/metabase/components/SchedulePicker/SchedulePicker.stories.tsx b/frontend/src/metabase/components/SchedulePicker/SchedulePicker.stories.tsx index 57b89f1b8660f..0b85d9e0865fb 100644 --- a/frontend/src/metabase/components/SchedulePicker/SchedulePicker.stories.tsx +++ b/frontend/src/metabase/components/SchedulePicker/SchedulePicker.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import SchedulePicker from "./SchedulePicker"; @@ -8,7 +8,7 @@ export default { component: SchedulePicker, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [ { schedule, @@ -29,13 +29,16 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - schedule: { - schedule_day: "mon", - schedule_frame: null, - schedule_hour: 0, - schedule_type: "daily", +export const Default = { + render: Template, + + args: { + schedule: { + schedule_day: "mon", + schedule_frame: null, + schedule_hour: 0, + schedule_type: "daily", + }, + textBeforeInterval: "Deliver", }, - textBeforeInterval: "Deliver", }; diff --git a/frontend/src/metabase/components/SegmentedControl/SegmentedControl.stories.tsx b/frontend/src/metabase/components/SegmentedControl/SegmentedControl.stories.tsx index 379b035ea14f8..c791573561c6d 100644 --- a/frontend/src/metabase/components/SegmentedControl/SegmentedControl.stories.tsx +++ b/frontend/src/metabase/components/SegmentedControl/SegmentedControl.stories.tsx @@ -1,14 +1,17 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; -import { SegmentedControl } from "./SegmentedControl"; +import { + SegmentedControl, + type SegmentedControlProps, +} from "./SegmentedControl"; export default { title: "Components/SegmentedControl", component: SegmentedControl, }; -const Template: ComponentStory = args => { +const Template: StoryFn> = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (value: unknown) => updateArgs({ value }); @@ -19,42 +22,54 @@ Template.args = { value: 0, }; -export const Default = Template.bind({}); -Default.args = { - options: [ - { name: "Gadget", value: 0 }, - { name: "Gizmo", value: 1 }, - ], +export const Default = { + render: Template, + + args: { + options: [ + { name: "Gadget", value: 0 }, + { name: "Gizmo", value: 1 }, + ], + }, }; -export const WithIcons = Template.bind({}); -WithIcons.args = { - options: [ - { name: "Gadget", value: 0, icon: "lightbulb" }, - { name: "Gizmo", value: 1, icon: "folder" }, - { name: "Doohickey", value: 2, icon: "insight" }, - ], +export const WithIcons = { + render: Template, + + args: { + options: [ + { name: "Gadget", value: 0, icon: "lightbulb" }, + { name: "Gizmo", value: 1, icon: "folder" }, + { name: "Doohickey", value: 2, icon: "insight" }, + ], + }, }; -export const OnlyIcons = Template.bind({}); -OnlyIcons.args = { - options: [ - { value: 0, icon: "lightbulb" }, - { value: 1, icon: "folder" }, - { value: 2, icon: "insight" }, - ], +export const OnlyIcons = { + render: Template, + + args: { + options: [ + { value: 0, icon: "lightbulb" }, + { value: 1, icon: "folder" }, + { value: 2, icon: "insight" }, + ], + }, }; -export const WithColors = Template.bind({}); -WithColors.args = { - options: [ - { - name: "Gadget", - value: 0, - icon: "lightbulb", - selectedColor: "accent1", - }, - { name: "Gizmo", value: 1, icon: "folder", selectedColor: "accent2" }, - { name: "Doohickey", value: 2, icon: "insight" }, - ], +export const WithColors = { + render: Template, + + args: { + options: [ + { + name: "Gadget", + value: 0, + icon: "lightbulb", + selectedColor: "accent1", + }, + { name: "Gizmo", value: 1, icon: "folder", selectedColor: "accent2" }, + { name: "Doohickey", value: 2, icon: "insight" }, + ], + }, }; diff --git a/frontend/src/metabase/components/SegmentedControl/SegmentedControl.tsx b/frontend/src/metabase/components/SegmentedControl/SegmentedControl.tsx index c2864fc65946f..07777f5f3311b 100644 --- a/frontend/src/metabase/components/SegmentedControl/SegmentedControl.tsx +++ b/frontend/src/metabase/components/SegmentedControl/SegmentedControl.tsx @@ -26,7 +26,7 @@ export type SegmentedControlOption = { selectedColor?: string; }; -interface Props { +export interface SegmentedControlProps { name?: string; value?: Value; options: SegmentedControlOption[]; @@ -47,7 +47,7 @@ export function SegmentedControl({ inactiveColor = "text-dark", variant = "fill-background", ...props -}: Props) { +}: SegmentedControlProps) { const id = useMemo(() => _.uniqueId("radio-"), []); const name = nameProp || id; const selectedOptionIndex = options.findIndex( diff --git a/frontend/src/metabase/components/SelectList/SelectListItem.stories.tsx b/frontend/src/metabase/components/SelectList/SelectListItem.stories.tsx index ee3c6766a1445..fb3817e3714ef 100644 --- a/frontend/src/metabase/components/SelectList/SelectListItem.stories.tsx +++ b/frontend/src/metabase/components/SelectList/SelectListItem.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { useState } from "react"; import SelectList from "./SelectList"; @@ -10,7 +10,7 @@ export default { const items = ["alert", "all", "archive", "dyno", "history"]; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [value, setValue] = useState("dyno"); return ( @@ -30,9 +30,11 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); +export const Default = { + render: Template, -Default.args = { - items: items, - rightIcon: "check", + args: { + items: items, + rightIcon: "check", + }, }; diff --git a/frontend/src/metabase/components/TextWidget/TextWidget.stories.tsx b/frontend/src/metabase/components/TextWidget/TextWidget.stories.tsx index 615b7fe16c279..30e07da6a7465 100644 --- a/frontend/src/metabase/components/TextWidget/TextWidget.stories.tsx +++ b/frontend/src/metabase/components/TextWidget/TextWidget.stories.tsx @@ -1,14 +1,14 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; -import { TextWidget } from "./TextWidget"; +import { TextWidget, type TextWidgetProps } from "./TextWidget"; export default { title: "Parameters/TextWidget", component: TextWidget, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const setValue = (value: string | number | null) => { @@ -18,18 +18,27 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - value: "", +export const Default = { + render: Template, + + args: { + value: "", + }, }; -export const InitialValue = Template.bind({}); -InitialValue.args = { - value: "Toucan McBird", +export const InitialValue = { + render: Template, + + args: { + value: "Toucan McBird", + }, }; -export const Placeholder = Template.bind({}); -Placeholder.args = { - value: "", - placeholder: "What's your wish?", +export const Placeholder = { + render: Template, + + args: { + value: "", + placeholder: "What's your wish?", + }, }; diff --git a/frontend/src/metabase/components/TextWidget/TextWidget.tsx b/frontend/src/metabase/components/TextWidget/TextWidget.tsx index 51a71ccaa9277..17f71f3e62242 100644 --- a/frontend/src/metabase/components/TextWidget/TextWidget.tsx +++ b/frontend/src/metabase/components/TextWidget/TextWidget.tsx @@ -4,7 +4,7 @@ import { t } from "ttag"; import { forceRedraw } from "metabase/lib/dom"; -type Props = { +export type TextWidgetProps = { value: string | number; setValue: (v: string | number | null) => void; className?: string; @@ -20,14 +20,14 @@ type State = { isFocused: boolean; }; -export class TextWidget extends Component { +export class TextWidget extends Component { static defaultProps = { isEditing: false, commitImmediately: false, disabled: false, }; - constructor(props: Props) { + constructor(props: TextWidgetProps) { super(props); this.state = { @@ -40,7 +40,7 @@ export class TextWidget extends Component { this.UNSAFE_componentWillReceiveProps(this.props); } - UNSAFE_componentWillReceiveProps(nextProps: Props) { + UNSAFE_componentWillReceiveProps(nextProps: TextWidgetProps) { if (nextProps.value !== this.props.value) { this.setState({ value: nextProps.value }, () => { // HACK: Address Safari rendering bug which causes https://github.com/metabase/metabase/issues/5335 diff --git a/frontend/src/metabase/components/Toaster/Toaster.stories.tsx b/frontend/src/metabase/components/Toaster/Toaster.stories.tsx index 185d9f0996b2e..f2e25a9435e10 100644 --- a/frontend/src/metabase/components/Toaster/Toaster.stories.tsx +++ b/frontend/src/metabase/components/Toaster/Toaster.stories.tsx @@ -1,24 +1,28 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import Toaster from "./Toaster"; +import Toaster, { type ToasterProps } from "./Toaster"; export default { title: "Dashboard/Toaster", component: Toaster, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - message: "Would you like to be notified when this dashboard is done loading?", - isShown: true, - onConfirm: () => { - alert("Confirmed"); - }, - onDismiss: () => { - alert("Dismissed"); +export const Default = { + render: Template, + + args: { + message: + "Would you like to be notified when this dashboard is done loading?", + isShown: true, + onConfirm: () => { + alert("Confirmed"); + }, + onDismiss: () => { + alert("Dismissed"); + }, }, }; diff --git a/frontend/src/metabase/components/TokenFieldItem/TokenFieldItem.stories.tsx b/frontend/src/metabase/components/TokenFieldItem/TokenFieldItem.stories.tsx index a142191978c70..d629ef9d466c1 100644 --- a/frontend/src/metabase/components/TokenFieldItem/TokenFieldItem.stories.tsx +++ b/frontend/src/metabase/components/TokenFieldItem/TokenFieldItem.stories.tsx @@ -1,5 +1,6 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import cx from "classnames"; +import type { ComponentProps } from "react"; import CS from "metabase/css/core/index.css"; import { Icon } from "metabase/ui"; @@ -17,7 +18,7 @@ const Wrapper = ({ children }: { children: JSX.Element | JSX.Element[] }) => (
); -const Template: ComponentStory = args => { +const Template: StoryFn> = args => { return ( @@ -25,7 +26,7 @@ const Template: ComponentStory = args => { ); }; -const ManyTemplate: ComponentStory = args => { +const ManyTemplate: StoryFn> = args => { return ( {`${args.children} 1`} @@ -37,7 +38,7 @@ const ManyTemplate: ComponentStory = args => { ); }; -const AddonTemplate: ComponentStory = args => { +const AddonTemplate: StoryFn> = args => { return ( @@ -54,21 +55,29 @@ const AddonTemplate: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -export const Many = ManyTemplate.bind({}); -export const WithAddon = AddonTemplate.bind({}); +export const Default = { + render: Template, -Default.args = { - isValid: true, - children: "Token Item Value", + args: { + isValid: true, + children: "Token Item Value", + }, }; -Many.args = { - isValid: true, - children: "Token Item Value", +export const Many = { + render: ManyTemplate, + + args: { + isValid: true, + children: "Token Item Value", + }, }; -WithAddon.args = { - isValid: true, - children: "Token Item Value", +export const WithAddon = { + render: AddonTemplate, + + args: { + isValid: true, + children: "Token Item Value", + }, }; diff --git a/frontend/src/metabase/components/UserAvatar/UserAvartar.stories.tsx b/frontend/src/metabase/components/UserAvatar/UserAvartar.stories.tsx index dc775bda6de5a..f126f0e0122ea 100644 --- a/frontend/src/metabase/components/UserAvatar/UserAvartar.stories.tsx +++ b/frontend/src/metabase/components/UserAvatar/UserAvartar.stories.tsx @@ -1,5 +1,3 @@ -import type { ComponentStory } from "@storybook/react"; - import UserAvatar from "./UserAvatar"; export default { @@ -7,46 +5,46 @@ export default { component: UserAvatar, }; -const Template: ComponentStory = args => ( - -); - -export const Default = Template.bind({}); -Default.args = { - user: { - first_name: "Testy", - last_name: "Tableton", - email: "user@metabase.test", - common_name: "Testy Tableton", +export const Default = { + args: { + user: { + first_name: "Testy", + last_name: "Tableton", + email: "user@metabase.test", + common_name: "Testy Tableton", + }, }, }; -export const SingleName = Template.bind({}); -SingleName.args = { - user: { - first_name: "Testy", - last_name: null, - email: "user@metabase.test", - common_name: "Testy", +export const SingleName = { + args: { + user: { + first_name: "Testy", + last_name: null, + email: "user@metabase.test", + common_name: "Testy", + }, }, }; -export const OnlyEmail = Template.bind({}); -OnlyEmail.args = { - user: { - first_name: null, - last_name: null, - email: "user@metabase.test", - common_name: "user@metabase.test", +export const OnlyEmail = { + args: { + user: { + first_name: null, + last_name: null, + email: "user@metabase.test", + common_name: "user@metabase.test", + }, }, }; -export const ShortEmail = Template.bind({}); -ShortEmail.args = { - user: { - first_name: null, - last_name: null, - email: "u@metabase.test", - common_name: "u@metabase.test", +export const ShortEmail = { + args: { + user: { + first_name: null, + last_name: null, + email: "u@metabase.test", + common_name: "u@metabase.test", + }, }, }; diff --git a/frontend/src/metabase/components/YearPicker/YearPicker.stories.tsx b/frontend/src/metabase/components/YearPicker/YearPicker.stories.tsx index 27c34981a5dae..52ab023e939e1 100644 --- a/frontend/src/metabase/components/YearPicker/YearPicker.stories.tsx +++ b/frontend/src/metabase/components/YearPicker/YearPicker.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import YearPicker from "./YearPicker"; @@ -8,7 +8,7 @@ export default { component: YearPicker, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (year: number) => { @@ -18,7 +18,10 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - value: 2022, +export const Default = { + render: Template, + + args: { + value: 2022, + }, }; diff --git a/frontend/src/metabase/core/components/AccordionList/AccordionList.stories.js b/frontend/src/metabase/core/components/AccordionList/AccordionList.stories.tsx similarity index 65% rename from frontend/src/metabase/core/components/AccordionList/AccordionList.stories.js rename to frontend/src/metabase/core/components/AccordionList/AccordionList.stories.tsx index e2af0a3360004..dc5f342a1baee 100644 --- a/frontend/src/metabase/core/components/AccordionList/AccordionList.stories.js +++ b/frontend/src/metabase/core/components/AccordionList/AccordionList.stories.tsx @@ -16,12 +16,8 @@ export default { component: AccordionList, }; -const Template = args => { - return ; -}; - -export const Default = Template.bind({}); - -Default.args = { - sections: SECTIONS, +export const Default = { + args: { + sections: SECTIONS, + }, }; diff --git a/frontend/src/metabase/core/components/Alert/Alert.stories.tsx b/frontend/src/metabase/core/components/Alert/Alert.stories.tsx index f24b01906bdce..3cbf1324cdce1 100644 --- a/frontend/src/metabase/core/components/Alert/Alert.stories.tsx +++ b/frontend/src/metabase/core/components/Alert/Alert.stories.tsx @@ -1,32 +1,41 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import Alert from "./Alert"; +import Alert, { type AlertProps } from "./Alert"; export default { title: "Core/Alert", component: Alert, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - children: "Info alert", - icon: "info", +export const Default = { + render: Template, + + args: { + children: "Info alert", + icon: "info", + }, }; -export const Warning = Template.bind({}); -Warning.args = { - children: "Warning alert", - variant: "warning", - icon: "warning", +export const Warning = { + render: Template, + + args: { + children: "Warning alert", + variant: "warning", + icon: "warning", + }, }; -export const Error = Template.bind({}); -Error.args = { - children: "Error alert", - variant: "error", - icon: "warning", +export const Error = { + render: Template, + + args: { + children: "Error alert", + variant: "error", + icon: "warning", + }, }; diff --git a/frontend/src/metabase/core/components/Alert/Alert.tsx b/frontend/src/metabase/core/components/Alert/Alert.tsx index d19f2c25eac0c..b521c34d659de 100644 --- a/frontend/src/metabase/core/components/Alert/Alert.tsx +++ b/frontend/src/metabase/core/components/Alert/Alert.tsx @@ -6,7 +6,7 @@ import { AlertIcon, AlertRoot } from "./Alert.styled"; export type AlertVariant = "info" | "warning" | "error"; -interface AlertProps { +export interface AlertProps { children: ReactNode; icon?: IconName; hasBorder?: boolean; diff --git a/frontend/src/metabase/core/components/AutocompleteInput/AutocompleteInput.stories.tsx b/frontend/src/metabase/core/components/AutocompleteInput/AutocompleteInput.stories.tsx index 93d7e8b311f8d..034278d5ecf81 100644 --- a/frontend/src/metabase/core/components/AutocompleteInput/AutocompleteInput.stories.tsx +++ b/frontend/src/metabase/core/components/AutocompleteInput/AutocompleteInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { useState } from "react"; import AutocompleteInput from "./AutocompleteInput"; @@ -8,7 +8,7 @@ export default { component: AutocompleteInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [value, setValue] = useState(""); return ( = args => { ); }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; + +export const CustomFilter = { + render: Template, -export const CustomFilter = Template.bind({}); -CustomFilter.args = { - filterOptions: (value: string | undefined, options: string[]) => { - if (!value) { - return []; - } else { - return options.filter(o => o.includes(value[0])); - } + args: { + filterOptions: (value: string | undefined, options: string[]) => { + if (!value) { + return []; + } else { + return options.filter(o => o.includes(value[0])); + } + }, }, }; -const CustomOptionClickTemplate: ComponentStory< - typeof AutocompleteInput -> = args => { +const CustomOptionClickTemplate: StoryFn = args => { const [value, setValue] = useState(""); const handleOptionSelect = (option: string) => { @@ -72,4 +75,7 @@ const CustomOptionClickTemplate: ComponentStory< /> ); }; -export const CustomOptionClick = CustomOptionClickTemplate.bind({}); + +export const CustomOptionClick = { + render: CustomOptionClickTemplate, +}; diff --git a/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx index a630afd9e2a40..ba6d6007fd358 100644 --- a/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx +++ b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import BookmarkToggle from "./BookmarkToggle"; @@ -8,7 +8,7 @@ export default { component: BookmarkToggle, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ isBookmarked }, updateArgs] = useArgs(); const handleCreateBookmark = () => updateArgs({ isBookmarked: true }); const handleDeleteBookmark = () => updateArgs({ isBookmarked: false }); @@ -23,7 +23,10 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - isBookmarked: false, +export const Default = { + render: Template, + + args: { + isBookmarked: false, + }, }; diff --git a/frontend/src/metabase/core/components/Button/Button.stories.tsx b/frontend/src/metabase/core/components/Button/Button.stories.tsx index 781c47237f001..5037f2837761b 100644 --- a/frontend/src/metabase/core/components/Button/Button.stories.tsx +++ b/frontend/src/metabase/core/components/Button/Button.stories.tsx @@ -1,34 +1,46 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import Button from "./Button"; +import Button, { type ButtonProps } from "./Button"; export default { title: "Core/Button", component: Button, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return @@ -19,4 +19,6 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; diff --git a/frontend/src/metabase/core/components/CheckBox/CheckBox.stories.tsx b/frontend/src/metabase/core/components/CheckBox/CheckBox.stories.tsx index 8b7565f882a2d..3009f29b54204 100644 --- a/frontend/src/metabase/core/components/CheckBox/CheckBox.stories.tsx +++ b/frontend/src/metabase/core/components/CheckBox/CheckBox.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import type { ChangeEvent } from "react"; import CheckBox from "./CheckBox"; @@ -9,7 +9,7 @@ export default { component: CheckBox, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ checked }, updateArgs] = useArgs(); const handleChange = (event: ChangeEvent) => { @@ -19,19 +19,28 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - checked: false, +export const Default = { + render: Template, + + args: { + checked: false, + }, }; -export const WithLabel = Template.bind({}); -WithLabel.args = { - checked: false, - label: "Label", +export const WithLabel = { + render: Template, + + args: { + checked: false, + label: "Label", + }, }; -export const WithCustomLabel = Template.bind({}); -WithCustomLabel.args = { - checked: false, - label: Label, +export const WithCustomLabel = { + render: Template, + + args: { + checked: false, + label: Label, + }, }; diff --git a/frontend/src/metabase/core/components/ColorInput/ColorInput.stories.tsx b/frontend/src/metabase/core/components/ColorInput/ColorInput.stories.tsx index bcba2aed52234..e8bf0f8c63ad6 100644 --- a/frontend/src/metabase/core/components/ColorInput/ColorInput.stories.tsx +++ b/frontend/src/metabase/core/components/ColorInput/ColorInput.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import ColorInput from "./ColorInput"; @@ -8,7 +8,7 @@ export default { component: ColorInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (value?: string) => { @@ -18,4 +18,6 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; diff --git a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx index ed9c9fe17fd96..b563f8ca8ae2a 100644 --- a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx +++ b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { color } from "metabase/lib/colors"; @@ -10,7 +10,7 @@ export default { component: ColorPicker, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (value?: string) => { @@ -20,8 +20,11 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - value: color("brand"), - placeholder: color("brand"), +export const Default = { + render: Template, + + args: { + value: color("brand"), + placeholder: color("brand"), + }, }; diff --git a/frontend/src/metabase/core/components/ColorPill/ColorPill.stories.tsx b/frontend/src/metabase/core/components/ColorPill/ColorPill.stories.tsx index 2977679b22d83..e10a7c405c316 100644 --- a/frontend/src/metabase/core/components/ColorPill/ColorPill.stories.tsx +++ b/frontend/src/metabase/core/components/ColorPill/ColorPill.stories.tsx @@ -1,25 +1,31 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { color } from "metabase/lib/colors"; -import ColorPill from "./ColorPill"; +import ColorPill, { type ColorPillProps } from "./ColorPill"; export default { title: "Core/ColorPill", component: ColorPill, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - color: color("brand"), +export const Default = { + render: Template, + + args: { + color: color("brand"), + }, }; -export const Auto = Template.bind({}); -Auto.args = { - color: color("brand"), - isAuto: true, +export const Auto = { + render: Template, + + args: { + color: color("brand"), + isAuto: true, + }, }; diff --git a/frontend/src/metabase/core/components/ColorRange/ColorRange.stories.tsx b/frontend/src/metabase/core/components/ColorRange/ColorRange.stories.tsx index ed1b097cb3584..c2c4b1c548957 100644 --- a/frontend/src/metabase/core/components/ColorRange/ColorRange.stories.tsx +++ b/frontend/src/metabase/core/components/ColorRange/ColorRange.stories.tsx @@ -1,29 +1,38 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { color } from "metabase/lib/colors"; -import ColorRange from "./ColorRange"; +import ColorRange, { type ColorRangeProps } from "./ColorRange"; export default { title: "Core/ColorRange", component: ColorRange, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - colors: [color("white"), color("brand")], +export const Default = { + render: Template, + + args: { + colors: [color("white"), color("brand")], + }, }; -export const Inverted = Template.bind({}); -Inverted.args = { - colors: [color("brand"), color("white")], +export const Inverted = { + render: Template, + + args: { + colors: [color("brand"), color("white")], + }, }; -export const ThreeColors = Template.bind({}); -ThreeColors.args = { - colors: [color("error"), color("white"), color("success")], +export const ThreeColors = { + render: Template, + + args: { + colors: [color("error"), color("white"), color("success")], + }, }; diff --git a/frontend/src/metabase/core/components/ColorRangeSelector/ColorRangeSelector.stories.tsx b/frontend/src/metabase/core/components/ColorRangeSelector/ColorRangeSelector.stories.tsx index bd5b4085409ef..5faa1b52d26b6 100644 --- a/frontend/src/metabase/core/components/ColorRangeSelector/ColorRangeSelector.stories.tsx +++ b/frontend/src/metabase/core/components/ColorRangeSelector/ColorRangeSelector.stories.tsx @@ -1,16 +1,18 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { color } from "metabase/lib/colors"; -import ColorRangeSelector from "./ColorRangeSelector"; +import ColorRangeSelector, { + type ColorRangeSelectorProps, +} from "./ColorRangeSelector"; export default { title: "Core/ColorRangeSelector", component: ColorRangeSelector, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (value: string[]) => { @@ -20,29 +22,42 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - value: [color("white"), color("brand")], - colors: [color("brand"), color("summarize"), color("filter")], +export const Default = { + render: Template, + + args: { + value: [color("white"), color("brand")], + colors: [color("brand"), color("summarize"), color("filter")], + }, }; -export const WithColorRanges = Template.bind({}); -WithColorRanges.args = { - value: [color("white"), color("brand")], - colors: [color("brand"), color("summarize"), color("filter")], - colorRanges: [ - [color("error"), color("white"), color("success")], - [color("error"), color("warning"), color("success")], - ], +export const WithColorRanges = { + render: Template, + + args: { + value: [color("white"), color("brand")], + colors: [color("brand"), color("summarize"), color("filter")], + colorRanges: [ + [color("error"), color("white"), color("success")], + [color("error"), color("warning"), color("success")], + ], + }, }; -export const WithColorMapping = Template.bind({}); -WithColorMapping.args = { - value: [color("white"), color("brand")], - colors: [color("brand"), color("summarize"), color("filter")], - colorMapping: { - [color("brand")]: [color("brand"), color("white"), color("brand")], - [color("summarize")]: [color("summarize"), color("white"), color("error")], - [color("filter")]: [color("filter"), color("white"), color("filter")], +export const WithColorMapping = { + render: Template, + + args: { + value: [color("white"), color("brand")], + colors: [color("brand"), color("summarize"), color("filter")], + colorMapping: { + [color("brand")]: [color("brand"), color("white"), color("brand")], + [color("summarize")]: [ + color("summarize"), + color("white"), + color("error"), + ], + [color("filter")]: [color("filter"), color("white"), color("filter")], + }, }, }; diff --git a/frontend/src/metabase/core/components/ColorSelector/ColorSelector.stories.tsx b/frontend/src/metabase/core/components/ColorSelector/ColorSelector.stories.tsx index 47368eb4f8323..909ce20462c52 100644 --- a/frontend/src/metabase/core/components/ColorSelector/ColorSelector.stories.tsx +++ b/frontend/src/metabase/core/components/ColorSelector/ColorSelector.stories.tsx @@ -1,16 +1,16 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import { color } from "metabase/lib/colors"; -import ColorSelector from "./ColorSelector"; +import ColorSelector, { type ColorSelectorProps } from "./ColorSelector"; export default { title: "Core/ColorSelector", component: ColorSelector, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (value: string) => { @@ -20,8 +20,11 @@ const Template: ComponentStory = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - value: color("brand"), - colors: [color("brand"), color("summarize"), color("filter")], +export const Default = { + render: Template, + + args: { + value: color("brand"), + colors: [color("brand"), color("summarize"), color("filter")], + }, }; diff --git a/frontend/src/metabase/core/components/DateInput/DateInput.stories.tsx b/frontend/src/metabase/core/components/DateInput/DateInput.stories.tsx index 1ab46b2a47c42..3dcda8c2f8c50 100644 --- a/frontend/src/metabase/core/components/DateInput/DateInput.stories.tsx +++ b/frontend/src/metabase/core/components/DateInput/DateInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import type { Moment } from "moment-timezone"; import { useState } from "react"; @@ -9,14 +9,19 @@ export default { component: DateInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [value, setValue] = useState(); return ; }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; + +export const WithTime = { + render: Template, -export const WithTime = Template.bind({}); -WithTime.args = { - hasTime: true, + args: { + hasTime: true, + }, }; diff --git a/frontend/src/metabase/core/components/DateSelector/DateSelector.stories.tsx b/frontend/src/metabase/core/components/DateSelector/DateSelector.stories.tsx index f235584e07bc6..697e64f5b8556 100644 --- a/frontend/src/metabase/core/components/DateSelector/DateSelector.stories.tsx +++ b/frontend/src/metabase/core/components/DateSelector/DateSelector.stories.tsx @@ -1,23 +1,28 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import moment from "moment-timezone"; import { useState } from "react"; -import DateSelector from "./DateSelector"; +import DateSelector, { type DateSelectorProps } from "./DateSelector"; export default { title: "Core/DateSelector", component: DateSelector, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [value, setValue] = useState(args.value); return ; }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; + +export const WithTime = { + render: Template, -export const WithTime = Template.bind({}); -WithTime.args = { - value: moment("2015-01-01"), - hasTime: true, + args: { + value: moment("2015-01-01"), + hasTime: true, + }, }; diff --git a/frontend/src/metabase/core/components/DateWidget/DateWidget.stories.tsx b/frontend/src/metabase/core/components/DateWidget/DateWidget.stories.tsx index 9701bff1aca5e..7e7a92bb5966b 100644 --- a/frontend/src/metabase/core/components/DateWidget/DateWidget.stories.tsx +++ b/frontend/src/metabase/core/components/DateWidget/DateWidget.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import type { Moment } from "moment-timezone"; import { useState } from "react"; @@ -9,14 +9,19 @@ export default { component: DateWidget, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [value, setValue] = useState(); return ; }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; + +export const WithTime = { + render: Template, -export const WithTime = Template.bind({}); -WithTime.args = { - hasTime: true, + args: { + hasTime: true, + }, }; diff --git a/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx b/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx index 7accb1a8f0d14..d9695da17564d 100644 --- a/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx +++ b/frontend/src/metabase/core/components/EditableText/EditableText.stories.tsx @@ -1,44 +1,56 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import EditableText from "./EditableText"; +import EditableText, { type EditableTextProps } from "./EditableText"; export default { title: "Core/EditableText", component: EditableText, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - initialValue: "Question", - placeholder: "Enter title", +export const Default = { + render: Template, + + args: { + initialValue: "Question", + placeholder: "Enter title", + }, }; -export const Multiline = Template.bind({}); -Multiline.args = { - initialValue: "Question", - placeholder: "Enter title", - isMultiline: true, +export const Multiline = { + render: Template, + + args: { + initialValue: "Question", + placeholder: "Enter title", + isMultiline: true, + }, }; -export const WithMaxWidth = Template.bind({}); -WithMaxWidth.args = { - initialValue: "Question", - placeholder: "Enter title", - style: { maxWidth: 500 }, +export const WithMaxWidth = { + render: Template, + + args: { + initialValue: "Question", + placeholder: "Enter title", + style: { maxWidth: 500 }, + }, }; -export const WithMarkdown = Template.bind({}); -WithMarkdown.args = { - initialValue: `**bold** text +export const WithMarkdown = { + render: Template, + + args: { + initialValue: `**bold** text - *multiline* + *multiline* - and [link](https://metabase.com)`, - placeholder: "Enter description", - isMultiline: true, - isMarkdown: true, + and [link](https://metabase.com)`, + placeholder: "Enter description", + isMultiline: true, + isMarkdown: true, + }, }; diff --git a/frontend/src/metabase/core/components/Ellipsified/Ellipsified.stories.tsx b/frontend/src/metabase/core/components/Ellipsified/Ellipsified.stories.tsx index 5d3f140691c7f..6455788de0497 100644 --- a/frontend/src/metabase/core/components/Ellipsified/Ellipsified.stories.tsx +++ b/frontend/src/metabase/core/components/Ellipsified/Ellipsified.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Ellipsified } from "./Ellipsified"; @@ -16,7 +16,7 @@ export default { component: Ellipsified, }; -const Template: ComponentStory = args => ( +const Template: StoryFn = args => (
    {testLabels.map((label: string) => (
  • @@ -26,8 +26,12 @@ const Template: ComponentStory = args => (
); -export const SingleLineEllipsify = Template.bind({}); -SingleLineEllipsify.args = { lines: 1 }; +export const SingleLineEllipsify = { + render: Template, + args: { lines: 1 }, +}; -export const MultiLineClamp = Template.bind({}); -MultiLineClamp.args = { lines: 8 }; +export const MultiLineClamp = { + render: Template, + args: { lines: 8 }, +}; diff --git a/frontend/src/metabase/core/components/ExternalLink/ExternalLink.stories.tsx b/frontend/src/metabase/core/components/ExternalLink/ExternalLink.stories.tsx index 5d1820255add0..73a9d106dc0f9 100644 --- a/frontend/src/metabase/core/components/ExternalLink/ExternalLink.stories.tsx +++ b/frontend/src/metabase/core/components/ExternalLink/ExternalLink.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import ExternalLink from "./ExternalLink"; @@ -7,12 +7,15 @@ export default { component: ExternalLink, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - href: "/", - children: "Link", +export const Default = { + render: Template, + + args: { + href: "/", + children: "Link", + }, }; diff --git a/frontend/src/metabase/core/components/FileInput/FileInput.stories.tsx b/frontend/src/metabase/core/components/FileInput/FileInput.stories.tsx index 0efcf1592b130..8cc4cd502f892 100644 --- a/frontend/src/metabase/core/components/FileInput/FileInput.stories.tsx +++ b/frontend/src/metabase/core/components/FileInput/FileInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import FileInput from "./FileInput"; @@ -7,11 +7,14 @@ export default { component: FileInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - name: "file", +export const Default = { + render: Template, + + args: { + name: "file", + }, }; diff --git a/frontend/src/metabase/core/components/FormCheckBox/FormCheckBox.stories.tsx b/frontend/src/metabase/core/components/FormCheckBox/FormCheckBox.stories.tsx index 1f02415f000d3..4da80c2e8480a 100644 --- a/frontend/src/metabase/core/components/FormCheckBox/FormCheckBox.stories.tsx +++ b/frontend/src/metabase/core/components/FormCheckBox/FormCheckBox.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -9,7 +9,7 @@ export default { component: FormCheckBox, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: false }; const handleSubmit = () => undefined; @@ -22,13 +22,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormDateInput/FormDateInput.stories.tsx b/frontend/src/metabase/core/components/FormDateInput/FormDateInput.stories.tsx index 77ae6c0a6944d..73d0167922d20 100644 --- a/frontend/src/metabase/core/components/FormDateInput/FormDateInput.stories.tsx +++ b/frontend/src/metabase/core/components/FormDateInput/FormDateInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -9,7 +9,7 @@ export default { component: FormDateInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: undefined }; const handleSubmit = () => undefined; @@ -22,13 +22,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormField/FormField.stories.tsx b/frontend/src/metabase/core/components/FormField/FormField.stories.tsx index d9ca6952e1a3c..fb2172d268a0d 100644 --- a/frontend/src/metabase/core/components/FormField/FormField.stories.tsx +++ b/frontend/src/metabase/core/components/FormField/FormField.stories.tsx @@ -1,5 +1,5 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; import type { ComponentProps } from "react"; import { cloneElement, isValidElement } from "react"; @@ -17,7 +17,7 @@ type inputProps = { onChange: (value: unknown) => void; }; -const Template: ComponentStory = ({ +const Template: StoryFn = ({ children, ...args }: ComponentProps) => { @@ -37,23 +37,32 @@ const Template: ComponentStory = ({ ); }; -export const ToggleStory = Template.bind({}); -ToggleStory.storyName = "Toggle"; -ToggleStory.args = { - children: , +export const ToggleStory = { + render: Template, + name: "Toggle", + + args: { + children: , + }, }; -export const ToggleWithTitle = Template.bind({}); -ToggleWithTitle.args = { - children: , - title: "Toggle this value?", - infoTooltip: "Info tooltip", +export const ToggleWithTitle = { + render: Template, + + args: { + children: , + title: "Toggle this value?", + infoTooltip: "Info tooltip", + }, }; -export const ToggleWithInlineTitle = Template.bind({}); -ToggleWithInlineTitle.args = { - children: , - title: "Toggle this value?", - orientation: "horizontal", - infoTooltip: "Info tooltip", +export const ToggleWithInlineTitle = { + render: Template, + + args: { + children: , + title: "Toggle this value?", + orientation: "horizontal", + infoTooltip: "Info tooltip", + }, }; diff --git a/frontend/src/metabase/core/components/FormFileInput/FormFileInput.stories.tsx b/frontend/src/metabase/core/components/FormFileInput/FormFileInput.stories.tsx index 61b7e7770360b..8b72d7ba9567b 100644 --- a/frontend/src/metabase/core/components/FormFileInput/FormFileInput.stories.tsx +++ b/frontend/src/metabase/core/components/FormFileInput/FormFileInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -9,7 +9,7 @@ export default { component: FormFileInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: undefined }; const handleSubmit = () => undefined; @@ -22,13 +22,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormInput/FormInput.stories.tsx b/frontend/src/metabase/core/components/FormInput/FormInput.stories.tsx index 49586aa96edd5..7f0b5defd3b42 100644 --- a/frontend/src/metabase/core/components/FormInput/FormInput.stories.tsx +++ b/frontend/src/metabase/core/components/FormInput/FormInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { useState } from "react"; import { Form, FormProvider } from "metabase/forms"; @@ -30,7 +30,7 @@ export default { }, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: false }; const handleSubmit = () => undefined; @@ -43,21 +43,30 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; -export const WithTitleAndActions = Template.bind({}); -WithTitleAndActions.args = { - title: "Title", - description: "Description", - optional: true, - actions: "Default", +export const WithTitleAndActions = { + render: Template, + + args: { + title: "Title", + description: "Description", + optional: true, + actions: "Default", + }, }; diff --git a/frontend/src/metabase/core/components/FormNumericInput/FormNumericInput.stories.tsx b/frontend/src/metabase/core/components/FormNumericInput/FormNumericInput.stories.tsx index dfc32d4e02fb7..c99126d53cfc6 100644 --- a/frontend/src/metabase/core/components/FormNumericInput/FormNumericInput.stories.tsx +++ b/frontend/src/metabase/core/components/FormNumericInput/FormNumericInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -9,7 +9,7 @@ export default { component: FormNumericInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: undefined }; const handleSubmit = () => undefined; @@ -22,13 +22,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormRadio/FormRadio.stories.tsx b/frontend/src/metabase/core/components/FormRadio/FormRadio.stories.tsx index 0fbfb2bdb6bc8..5eac12865f3f0 100644 --- a/frontend/src/metabase/core/components/FormRadio/FormRadio.stories.tsx +++ b/frontend/src/metabase/core/components/FormRadio/FormRadio.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -15,7 +15,7 @@ export default { component: FormRadio, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: undefined }; const handleSubmit = () => undefined; @@ -28,13 +28,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormSelect/FormSelect.stories.tsx b/frontend/src/metabase/core/components/FormSelect/FormSelect.stories.tsx index c1f326248d7c2..2130ad8635473 100644 --- a/frontend/src/metabase/core/components/FormSelect/FormSelect.stories.tsx +++ b/frontend/src/metabase/core/components/FormSelect/FormSelect.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -15,7 +15,7 @@ export default { component: FormSelect, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: undefined }; const handleSubmit = () => undefined; @@ -28,15 +28,21 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", - placeholder: "Use default", +export const Default = { + render: Template, + + args: { + title: "Title", + placeholder: "Use default", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - placeholder: "Use default", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + placeholder: "Use default", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormTextArea/FormTextArea.stories.tsx b/frontend/src/metabase/core/components/FormTextArea/FormTextArea.stories.tsx index 65fe4b49be76a..8cb7bff34d0e4 100644 --- a/frontend/src/metabase/core/components/FormTextArea/FormTextArea.stories.tsx +++ b/frontend/src/metabase/core/components/FormTextArea/FormTextArea.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -9,7 +9,7 @@ export default { component: FormTextArea, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: false }; const handleSubmit = () => undefined; @@ -22,13 +22,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/FormToggle/FormToggle.stories.tsx b/frontend/src/metabase/core/components/FormToggle/FormToggle.stories.tsx index ca0f7ef18a13c..c8007d2ea1c03 100644 --- a/frontend/src/metabase/core/components/FormToggle/FormToggle.stories.tsx +++ b/frontend/src/metabase/core/components/FormToggle/FormToggle.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { Form, FormProvider } from "metabase/forms"; @@ -9,7 +9,7 @@ export default { component: FormToggle, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const initialValues = { value: false }; const handleSubmit = () => undefined; @@ -22,13 +22,19 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); -Default.args = { - title: "Title", +export const Default = { + render: Template, + + args: { + title: "Title", + }, }; -export const WithDescription = Template.bind({}); -WithDescription.args = { - title: "Title", - description: "Description", +export const WithDescription = { + render: Template, + + args: { + title: "Title", + description: "Description", + }, }; diff --git a/frontend/src/metabase/core/components/Input/Input.stories.tsx b/frontend/src/metabase/core/components/Input/Input.stories.tsx index d5d4ada42bd63..7f610b4da1428 100644 --- a/frontend/src/metabase/core/components/Input/Input.stories.tsx +++ b/frontend/src/metabase/core/components/Input/Input.stories.tsx @@ -1,18 +1,18 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { useState } from "react"; -import Input from "./Input"; +import Input, { type InputProps } from "./Input"; export default { title: "Core/Input", component: Input, }; -const UncontrolledTemplate: ComponentStory = args => { +const UncontrolledTemplate: StoryFn = args => { return ; }; -const ControlledTemplate: ComponentStory = args => { +const ControlledTemplate: StoryFn = args => { const [value, setValue] = useState(""); return ( = args => { ); }; -export const Default = UncontrolledTemplate.bind({}); +export const Default = { + render: UncontrolledTemplate, +}; + +export const WithError = { + render: UncontrolledTemplate, -export const WithError = UncontrolledTemplate.bind({}); -WithError.args = { - error: true, + args: { + error: true, + }, }; -export const WithRightIcon = UncontrolledTemplate.bind({}); -WithRightIcon.args = { - rightIcon: "info", - rightIconTooltip: "Useful tips", +export const WithRightIcon = { + render: UncontrolledTemplate, + + args: { + rightIcon: "info", + rightIconTooltip: "Useful tips", + }, }; -export const Controlled = ControlledTemplate.bind({}); +export const Controlled = { + render: ControlledTemplate, +}; diff --git a/frontend/src/metabase/core/components/Link/Link.stories.tsx b/frontend/src/metabase/core/components/Link/Link.stories.tsx index d50b7ea43fe39..21e2ace4561f0 100644 --- a/frontend/src/metabase/core/components/Link/Link.stories.tsx +++ b/frontend/src/metabase/core/components/Link/Link.stories.tsx @@ -1,6 +1,6 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import Link from "./"; +import Link, { type LinkProps } from "./"; export default { title: "Core/Link", @@ -13,7 +13,7 @@ const sampleStyle = { gap: "2rem", }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return (
Click Me @@ -21,9 +21,11 @@ const Template: ComponentStory = args => { ); }; -export const Default = Template.bind({}); +export const Default = { + render: Template, -Default.args = { - to: "/foo/bar", - variant: "default", + args: { + to: "/foo/bar", + variant: "default", + }, }; diff --git a/frontend/src/metabase/core/components/Markdown/Markdown.stories.tsx b/frontend/src/metabase/core/components/Markdown/Markdown.stories.tsx index ec922015c8640..a34bb2867c55c 100644 --- a/frontend/src/metabase/core/components/Markdown/Markdown.stories.tsx +++ b/frontend/src/metabase/core/components/Markdown/Markdown.stories.tsx @@ -1,22 +1,25 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import Markdown from "./Markdown"; +import Markdown, { type MarkdownProps } from "./Markdown"; export default { title: "Core/Markdown", component: Markdown, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ; }; -export const Default = Template.bind({}); -Default.args = { - children: ` -Our first email blast to the mailing list not directly linked to the release -of a new version. We wanted to see if this would effect visits to landing pages -for the features in 0.41. +export const Default = { + render: Template, -Here’s a [doc](https://metabase.test) with the findings.`, + args: { + children: ` + Our first email blast to the mailing list not directly linked to the release + of a new version. We wanted to see if this would effect visits to landing pages + for the features in 0.41. + + Here’s a [doc](https://metabase.test) with the findings.`, + }, }; diff --git a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.stories.tsx b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.stories.tsx index cd71a8526e91b..7bbdb66cb10a0 100644 --- a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.stories.tsx +++ b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.stories.tsx @@ -1,14 +1,14 @@ import styled from "@emotion/styled"; -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; -import { MarkdownPreview } from "./MarkdownPreview"; +import { MarkdownPreview, type MarkdownPreviewProps } from "./MarkdownPreview"; export default { title: "Core/MarkdownPreview", component: MarkdownPreview, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return ( @@ -20,22 +20,28 @@ const Container = styled.div` width: 200px; `; -export const PlainText = Template.bind({}); -PlainText.args = { - children: `Our first email blast to the mailing list not directly linked to the release of a new version. We wanted to see if this would effect visits to landing pages for the features in 0.41.`, +export const PlainText = { + render: Template, + + args: { + children: `Our first email blast to the mailing list not directly linked to the release of a new version. We wanted to see if this would effect visits to landing pages for the features in 0.41.`, + }, }; -export const Markdown = Template.bind({}); -Markdown.args = { - children: `![Metabase logo](https://www.metabase.com/images/logo.svg) +export const Markdown = { + render: Template, + + args: { + children: `![Metabase logo](https://www.metabase.com/images/logo.svg) -# New version + # New version -Our first email blast to the mailing list not directly linked to the release -of a new version. We wanted to see if this would effect visits to landing pages -for the features in 0.41. + Our first email blast to the mailing list not directly linked to the release + of a new version. We wanted to see if this would effect visits to landing pages + for the features in 0.41. ----- + ---- -Here’s a [doc](https://metabase.test) with the findings.`, + Here’s a [doc](https://metabase.test) with the findings.`, + }, }; diff --git a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx index 36762dd2f73f1..5a35a2de26805 100644 --- a/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx +++ b/frontend/src/metabase/core/components/MarkdownPreview/MarkdownPreview.tsx @@ -8,7 +8,7 @@ import Tooltip from "../Tooltip"; import C from "./MarkdownPreview.module.css"; -interface Props { +export interface MarkdownPreviewProps { children: string; className?: string; tooltipMaxWidth?: ComponentProps["maxWidth"]; @@ -26,7 +26,7 @@ export const MarkdownPreview = ({ lineClamp, allowedElements = DEFAULT_ALLOWED_ELEMENTS, oneLine, -}: Props) => { +}: MarkdownPreviewProps) => { const { isTruncated, ref } = useIsTruncated(); const setReactMarkdownRef: LegacyRef = div => { diff --git a/frontend/src/metabase/core/components/NumericInput/NumericInput.stories.tsx b/frontend/src/metabase/core/components/NumericInput/NumericInput.stories.tsx index dfac3bd278e8e..ee3c4e3bf0e73 100644 --- a/frontend/src/metabase/core/components/NumericInput/NumericInput.stories.tsx +++ b/frontend/src/metabase/core/components/NumericInput/NumericInput.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import { useState } from "react"; import NumericInput from "./NumericInput"; @@ -8,9 +8,11 @@ export default { component: NumericInput, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { const [value, setValue] = useState(); return ; }; -export const Default = Template.bind({}); +export const Default = { + render: Template, +}; diff --git a/frontend/src/metabase/core/components/Radio/Radio.stories.tsx b/frontend/src/metabase/core/components/Radio/Radio.stories.tsx index 16842711d817e..b4cfc8d04ab9b 100644 --- a/frontend/src/metabase/core/components/Radio/Radio.stories.tsx +++ b/frontend/src/metabase/core/components/Radio/Radio.stories.tsx @@ -1,14 +1,14 @@ -import { useArgs } from "@storybook/addons"; -import type { ComponentStory } from "@storybook/react"; +import { useArgs } from "@storybook/preview-api"; +import type { StoryFn } from "@storybook/react"; -import Radio from "./Radio"; +import Radio, { type RadioProps } from "./Radio"; export default { title: "Deprecated/Radio", component: Radio, }; -const Template: ComponentStory = args => { +const Template: StoryFn> = args => { const [{ value }, updateArgs] = useArgs(); const handleChange = (value: unknown) => updateArgs({ value }); @@ -23,20 +23,29 @@ Template.args = { ], }; -export const Default = Template.bind({}); -Default.args = { - ...Template.args, - variant: "normal", +export const Default = { + render: Template, + + args: { + ...Template.args, + variant: "normal", + }, }; -export const Underlined = Template.bind({}); -Underlined.args = { - ...Template.args, - variant: "underlined", +export const Underlined = { + render: Template, + + args: { + ...Template.args, + variant: "underlined", + }, }; -export const Bubble = Template.bind({}); -Bubble.args = { - ...Template.args, - variant: "bubble", +export const Bubble = { + render: Template, + + args: { + ...Template.args, + variant: "bubble", + }, }; diff --git a/frontend/src/metabase/core/components/Select/Select.stories.tsx b/frontend/src/metabase/core/components/Select/Select.stories.tsx index 11c018a85a4ac..fff58b33ce6f7 100644 --- a/frontend/src/metabase/core/components/Select/Select.stories.tsx +++ b/frontend/src/metabase/core/components/Select/Select.stories.tsx @@ -1,4 +1,4 @@ -import type { ComponentStory } from "@storybook/react"; +import type { StoryFn } from "@storybook/react"; import Select from "./Select"; @@ -7,90 +7,93 @@ export default { component: Select, }; -const Template: ComponentStory = args => { +const Template: StoryFn = args => { return