diff --git a/web-common/src/features/dashboards/workspace/DashboardCTAs.svelte b/web-common/src/features/dashboards/workspace/DashboardCTAs.svelte index 0b1724b7efb..165b394b37d 100644 --- a/web-common/src/features/dashboards/workspace/DashboardCTAs.svelte +++ b/web-common/src/features/dashboards/workspace/DashboardCTAs.svelte @@ -2,18 +2,18 @@ import MetricsIcon from "@rilldata/web-common/components/icons/Metrics.svelte"; import LocalAvatarButton from "@rilldata/web-common/features/authentication/LocalAvatarButton.svelte"; import GlobalDimensionSearch from "@rilldata/web-common/features/dashboards/dimension-search/GlobalDimensionSearch.svelte"; + import { useExplore } from "@rilldata/web-common/features/explores/selectors"; import { Button } from "../../../components/button"; import { runtime } from "../../../runtime-client/runtime-store"; import { featureFlags } from "../../feature-flags"; import ViewAsButton from "../granular-access-policies/ViewAsButton.svelte"; import { useDashboardPolicyCheck } from "../granular-access-policies/useDashboardPolicyCheck"; - import { useMetricsView } from "../selectors"; import DeployDashboardCta from "./DeployDashboardCTA.svelte"; - export let metricsViewName: string; + export let exploreName: string; - $: metricsViewQuery = useMetricsView($runtime.instanceId, metricsViewName); - $: filePath = $metricsViewQuery.data?.meta?.filePaths?.[0] ?? ""; + $: exploreQuery = useExplore($runtime.instanceId, exploreName); + $: filePath = $exploreQuery.data?.explore?.meta?.filePaths?.[0] ?? ""; $: dashboardPolicyCheck = useDashboardPolicyCheck( $runtime.instanceId, diff --git a/web-common/src/features/file-explorer/new-files.ts b/web-common/src/features/file-explorer/new-files.ts index f7e47b4ac42..5b421bac30f 100644 --- a/web-common/src/features/file-explorer/new-files.ts +++ b/web-common/src/features/file-explorer/new-files.ts @@ -126,7 +126,7 @@ export function getBaseNameForNewResourceFile( ? `${baseResource.meta!.name!.name}_explore` : ResourceKindMap[newKind].baseName; default: - return ResourceKindMap[newKind].baseFileName; + return ResourceKindMap[newKind].baseName; } } diff --git a/web-local/src/routes/(viz)/+layout.svelte b/web-local/src/routes/(viz)/+layout.svelte index a170020c5e4..578f755c91d 100644 --- a/web-local/src/routes/(viz)/+layout.svelte +++ b/web-local/src/routes/(viz)/+layout.svelte @@ -66,7 +66,7 @@ {#if route.id?.includes("explore") && metricsViewName} - + {/if} diff --git a/web-local/tests/dashboards/dashboard-flow-template.ts b/web-local/tests/explores/dashboard-flow-template.ts similarity index 85% rename from web-local/tests/dashboards/dashboard-flow-template.ts rename to web-local/tests/explores/dashboard-flow-template.ts index 85a4d88bbe4..f1c4b29e00f 100644 --- a/web-local/tests/dashboards/dashboard-flow-template.ts +++ b/web-local/tests/explores/dashboard-flow-template.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { useDashboardFlowTestSetup } from "web-local/tests/dashboards/dashboard-flow-test-setup"; +import { useDashboardFlowTestSetup } from "web-local/tests/explores/dashboard-flow-test-setup"; import { startRuntimeForEachTest } from "../utils/startRuntimeForEachTest"; test.describe("~~~~~~~~~~~~~~~~~~~~FIXME RENAME THIS~~~~~~~~~~~~~~~~~~~~~~~", () => { diff --git a/web-local/tests/dashboards/dashboard-flow-test-setup.ts b/web-local/tests/explores/dashboard-flow-test-setup.ts similarity index 75% rename from web-local/tests/dashboards/dashboard-flow-test-setup.ts rename to web-local/tests/explores/dashboard-flow-test-setup.ts index d657bde1ab7..4e8808010f7 100644 --- a/web-local/tests/dashboards/dashboard-flow-test-setup.ts +++ b/web-local/tests/explores/dashboard-flow-test-setup.ts @@ -1,4 +1,4 @@ -import { createDashboardFromModel } from "web-local/tests/utils/dashboardHelpers"; +import { createMetricsViewFromModel } from "web-local/tests/utils/metricsViewHelpers"; import { createAdBidsModel } from "web-local/tests/utils/dataSpecifcHelpers"; import { test } from "../utils/test"; import { waitForFileNavEntry } from "../utils/waitHelpers"; @@ -14,7 +14,7 @@ export function useDashboardFlowTestSetup() { `/dashboards/AdBids_model_dashboard.yaml`, true, ), - createDashboardFromModel(page, "/models/AdBids_model.sql"), + createMetricsViewFromModel(page, "/models/AdBids_model.sql"), ]); }); } diff --git a/web-local/tests/dashboards/dimension-and-measure-selection.spec.ts b/web-local/tests/explores/dimension-and-measure-selection.spec.ts similarity index 95% rename from web-local/tests/dashboards/dimension-and-measure-selection.spec.ts rename to web-local/tests/explores/dimension-and-measure-selection.spec.ts index 6359d29cfb6..1a24c299b34 100644 --- a/web-local/tests/dashboards/dimension-and-measure-selection.spec.ts +++ b/web-local/tests/explores/dimension-and-measure-selection.spec.ts @@ -1,5 +1,5 @@ import { expect } from "@playwright/test"; -import { useDashboardFlowTestSetup } from "web-local/tests/dashboards/dashboard-flow-test-setup"; +import { useDashboardFlowTestSetup } from "web-local/tests/explores/dashboard-flow-test-setup"; import { test } from "../utils/test"; import { clickMenuButton } from "../utils/commonHelpers"; diff --git a/web-local/tests/dashboards/dashboards.spec.ts b/web-local/tests/explores/explores.spec.ts similarity index 94% rename from web-local/tests/dashboards/dashboards.spec.ts rename to web-local/tests/explores/explores.spec.ts index 2b90918100c..287259e80eb 100644 --- a/web-local/tests/dashboards/dashboards.spec.ts +++ b/web-local/tests/explores/explores.spec.ts @@ -1,12 +1,16 @@ import { expect } from "@playwright/test"; +import { + createExploreFromModel, + createExploreFromSource, +} from "web-local/tests/utils/exploreHelpers"; import { ResourceWatcher } from "web-local/tests/utils/ResourceWatcher"; import { updateCodeEditor, wrapRetryAssertion } from "../utils/commonHelpers"; import { assertLeaderboards, - createDashboardFromModel, - createDashboardFromSource, + createMetricsViewFromModel, + createMetricsViewFromSource, interactWithTimeRangeMenu, -} from "../utils/dashboardHelpers"; +} from "web-local/tests/utils/metricsViewHelpers"; import { assertAdBidsDashboard, createAdBidsModel, @@ -15,26 +19,38 @@ import { createSource } from "../utils/sourceHelpers"; import { test } from "../utils/test"; import { waitForFileNavEntry } from "../utils/waitHelpers"; -test.describe("dashboard", () => { - test("Autogenerate dashboard from source", async ({ page }) => { +test.describe("explores", () => { + test("Autogenerate explore from source", async ({ page }) => { await createSource(page, "AdBids.csv", "/sources/AdBids.yaml"); - await createDashboardFromSource(page, "/sources/AdBids.yaml"); - await waitForFileNavEntry(page, `/dashboards/AdBids_dashboard.yaml`, true); + await createExploreFromSource( + page, + "/sources/AdBids.yaml", + "/metrics/AdBids_metrics.yaml", + ); + await waitForFileNavEntry( + page, + "/explore-dashboards/AdBids_metrics_explore.yaml", + true, + ); await page.getByRole("button", { name: "Preview" }).click(); // Temporary timeout while the issue is looked into await page.waitForTimeout(1000); await assertAdBidsDashboard(page); }); - test("Autogenerate dashboard from model", async ({ page }) => { + test("Autogenerate explore from model", async ({ page }) => { await createAdBidsModel(page); await Promise.all([ waitForFileNavEntry( page, - `/dashboards/AdBids_model_dashboard.yaml`, + "/explore-dashboards/AdBids_model_metrics_explore.yaml", true, ), - createDashboardFromModel(page, "/models/AdBids_model.sql"), + createExploreFromModel( + page, + "/models/AdBids_model.sql", + "/metrics/AdBids_model_metrics.yaml", + ), ]); await page.getByRole("button", { name: "Preview" }).click(); @@ -69,7 +85,7 @@ test.describe("dashboard", () => { const watcher = new ResourceWatcher(page); await createAdBidsModel(page); - await createDashboardFromModel(page, "/models/AdBids_model.sql"); + await createMetricsViewFromModel(page, "/models/AdBids_model.sql"); await page.getByRole("button", { name: "Preview" }).click(); // Check the total records are 100k diff --git a/web-local/tests/dashboards/leaderboard-and-dim-table-sort.spec.ts b/web-local/tests/explores/leaderboard-and-dim-table-sort.spec.ts similarity index 98% rename from web-local/tests/dashboards/leaderboard-and-dim-table-sort.spec.ts rename to web-local/tests/explores/leaderboard-and-dim-table-sort.spec.ts index b834f3899c0..4434163130a 100644 --- a/web-local/tests/dashboards/leaderboard-and-dim-table-sort.spec.ts +++ b/web-local/tests/explores/leaderboard-and-dim-table-sort.spec.ts @@ -1,5 +1,5 @@ import { expect, type Locator } from "@playwright/test"; -import { useDashboardFlowTestSetup } from "web-local/tests/dashboards/dashboard-flow-test-setup"; +import { useDashboardFlowTestSetup } from "web-local/tests/explores/dashboard-flow-test-setup"; import { test } from "../utils/test"; async function assertAAboveB(locA: Locator, locB: Locator) { diff --git a/web-local/tests/dashboards/leaderboard-context-column.spec.ts b/web-local/tests/explores/leaderboard-context-column.spec.ts similarity index 98% rename from web-local/tests/dashboards/leaderboard-context-column.spec.ts rename to web-local/tests/explores/leaderboard-context-column.spec.ts index af3aeedfe9b..c83bc7fad33 100644 --- a/web-local/tests/dashboards/leaderboard-context-column.spec.ts +++ b/web-local/tests/explores/leaderboard-context-column.spec.ts @@ -1,7 +1,7 @@ import { expect } from "@playwright/test"; import { ResourceWatcher } from "web-local/tests/utils/ResourceWatcher"; import { clickMenuButton } from "../utils/commonHelpers"; -import { interactWithTimeRangeMenu } from "../utils/dashboardHelpers"; +import { interactWithTimeRangeMenu } from "web-local/tests/utils/metricsViewHelpers"; import { test } from "../utils/test"; import { useDashboardFlowTestSetup } from "./dashboard-flow-test-setup"; diff --git a/web-local/tests/dashboards/metrics-editor.spec.ts b/web-local/tests/explores/metrics-editor.spec.ts similarity index 100% rename from web-local/tests/dashboards/metrics-editor.spec.ts rename to web-local/tests/explores/metrics-editor.spec.ts diff --git a/web-local/tests/dashboards/number-formatting.spec.ts b/web-local/tests/explores/number-formatting.spec.ts similarity index 97% rename from web-local/tests/dashboards/number-formatting.spec.ts rename to web-local/tests/explores/number-formatting.spec.ts index 9151411cc19..0aea56454dc 100644 --- a/web-local/tests/dashboards/number-formatting.spec.ts +++ b/web-local/tests/explores/number-formatting.spec.ts @@ -1,6 +1,6 @@ -import { useDashboardFlowTestSetup } from "web-local/tests/dashboards/dashboard-flow-test-setup"; +import { useDashboardFlowTestSetup } from "web-local/tests/explores/dashboard-flow-test-setup"; import { ResourceWatcher } from "web-local/tests/utils/ResourceWatcher"; -import { interactWithTimeRangeMenu } from "../utils/dashboardHelpers"; +import { interactWithTimeRangeMenu } from "web-local/tests/utils/metricsViewHelpers"; import { expect } from "@playwright/test"; import { test } from "../utils/test"; diff --git a/web-local/tests/dashboards/time-controls-from-config.spec.ts b/web-local/tests/explores/time-controls-from-config.spec.ts similarity index 98% rename from web-local/tests/dashboards/time-controls-from-config.spec.ts rename to web-local/tests/explores/time-controls-from-config.spec.ts index fa90519143a..45c8f75ab9f 100644 --- a/web-local/tests/dashboards/time-controls-from-config.spec.ts +++ b/web-local/tests/explores/time-controls-from-config.spec.ts @@ -1,6 +1,6 @@ import { expect } from "@playwright/test"; -import { useDashboardFlowTestSetup } from "web-local/tests/dashboards/dashboard-flow-test-setup"; -import { interactWithTimeRangeMenu } from "web-local/tests/utils/dashboardHelpers"; +import { useDashboardFlowTestSetup } from "web-local/tests/explores/dashboard-flow-test-setup"; +import { interactWithTimeRangeMenu } from "web-local/tests/utils/metricsViewHelpers"; import { ResourceWatcher } from "web-local/tests/utils/ResourceWatcher"; import { test } from "../utils/test"; diff --git a/web-local/tests/utils/dashboardHelpers.ts b/web-local/tests/utils/dashboardHelpers.ts deleted file mode 100644 index 8d27c6bf555..00000000000 --- a/web-local/tests/utils/dashboardHelpers.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { expect } from "@playwright/test"; -import type { V1Expression } from "@rilldata/web-common/runtime-client"; -import type { Page, Response } from "playwright"; -import { - clickMenuButton, - openFileNavEntryContextMenu, - updateCodeEditor, - waitForValidResource, -} from "./commonHelpers"; - -export async function createDashboardFromSource( - page: Page, - sourcePath: string, -) { - await openFileNavEntryContextMenu(page, sourcePath); - await clickMenuButton(page, "Generate dashboard"); -} - -export async function createDashboardFromModel(page: Page, modelPath: string) { - await openFileNavEntryContextMenu(page, modelPath); - await clickMenuButton(page, "Generate dashboard"); -} - -export async function assertLeaderboards( - page: Page, - leaderboards: Array<{ - label: string; - values: Array; - }>, -) { - for (const { label, values } of leaderboards) { - const leaderboardBlock = page.getByRole("table", { - name: `${label} leaderboard`, - }); - await expect(leaderboardBlock).toBeVisible(); - - const actualValues = await leaderboardBlock - .locator("tr > td:nth-child(2)") - .allInnerTexts(); - expect(actualValues).toEqual(values); - } -} - -export type RequestMatcher = (response: Response) => boolean; - -/** - * Waits for a time series query to end. - * Optionally takes a filter matcher: {@link metricsViewRequestFilterMatcher}. - */ -export async function waitForTimeSeries( - page: Page, - metricsView: string, - filterMatcher?: RequestMatcher, -) { - const timeSeriesUrlRegex = new RegExp( - `/metrics-views/${metricsView}/timeseries`, - ); - await page.waitForResponse( - (response) => - timeSeriesUrlRegex.test(response.url()) && - (filterMatcher ? filterMatcher(response) : true), - ); -} - -/** - * Waits for a set of top list queries to end. - * Optionally takes a filter matcher: {@link metricsViewRequestFilterMatcher}. - */ -export async function waitForTopLists( - page: Page, - metricsView: string, - dimensions: Array, - filterMatcher?: RequestMatcher, -) { - const topListUrlRegex = new RegExp(`/metrics-views/${metricsView}/toplist`); - await Promise.all( - dimensions.map((dimension) => - page.waitForResponse( - (response) => - topListUrlRegex.test(response.url()) && - response.request().postDataJSON().dimensionName === dimension && - (filterMatcher ? filterMatcher(response) : true), - ), - ), - ); -} - -/** - * Waits for a set of top list queries to end. - * Optionally takes a filter matcher: {@link metricsViewRequestFilterMatcher}. - */ -export async function waitForAggregationTopLists( - page: Page, - metricsView: string, - dimensions: Array, - filterMatcher?: RequestMatcher, -) { - const topListUrlRegex = new RegExp( - `/metrics-views/${metricsView}/aggregation`, - ); - await Promise.all( - dimensions.map((dimension) => - page.waitForResponse( - (response) => - topListUrlRegex.test(response.url()) && - !!response - .request() - .postDataJSON() - ?.dimensions?.find((n) => n.name === dimension) && - (filterMatcher ? filterMatcher(response) : true), - ), - ), - ); -} - -export type RequestMatcherFilter = { label: string; values: unknown[] }; - -/** - * Helper to add a request matcher to match metrics view queries with certain filter - */ -export function metricsViewRequestFilterMatcher( - response: Response, - includeFilters: RequestMatcherFilter[], - excludeFilters: RequestMatcherFilter[], -) { - const filterRequest = response.request().postDataJSON().where as V1Expression; - const includeFilterRequest = new Map(); - const excludeFilterRequest = new Map(); - - if (filterRequest?.cond?.exprs) { - for (const expr of filterRequest.cond.exprs) { - if (!expr.cond?.exprs?.[0]?.ident) continue; - if (expr.cond.op === "OPERATION_IN") { - includeFilterRequest.set( - expr.cond.exprs[0].ident, - expr.cond.exprs.slice(1).map((e) => e.val as string), - ); - } else if (expr.cond.op === "OPERATION_NIN") { - excludeFilterRequest.set( - expr.cond.exprs[0].ident, - expr.cond.exprs.slice(1).map((e) => e.val as string), - ); - } - } - } - - return ( - includeFilters.every( - ({ label, values }) => - includeFilterRequest - .get(label) - ?.every((val) => values.indexOf(val) >= 0) ?? false, - ) && - excludeFilters.every( - ({ label, values }) => - excludeFilterRequest - .get(label) - ?.every((val) => values.indexOf(val) >= 0) ?? false, - ) - ); -} - -// Helper that opens the time range menu, calls your interactions, and then waits until the menu closes -export async function interactWithTimeRangeMenu( - page: Page, - cb: () => void | Promise, -) { - // Open the menu - await page.getByLabel("Select time range").click(); - // Run the defined interactions - await cb(); - // Wait for menu to close - await expect( - page.getByRole("menu", { name: "Select time range" }), - ).not.toBeVisible(); -} - -export async function waitForDashboard(page: Page) { - return waitForValidResource( - page, - "AdBids_model_dashboard", - "rill.runtime.v1.MetricsView", - ); -} - -export async function updateAndWaitForDashboard(page: Page, code: string) { - return Promise.all([ - updateCodeEditor(page, code), - waitForValidResource( - page, - "AdBids_model_dashboard", - "rill.runtime.v1.MetricsView", - ), - ]); -} diff --git a/web-local/tests/utils/dataSpecifcHelpers.ts b/web-local/tests/utils/dataSpecifcHelpers.ts index 5f331a7816f..c221d0798f4 100644 --- a/web-local/tests/utils/dataSpecifcHelpers.ts +++ b/web-local/tests/utils/dataSpecifcHelpers.ts @@ -4,7 +4,7 @@ import { waitForProfiling, wrapRetryAssertion, } from "./commonHelpers"; -import { assertLeaderboards } from "./dashboardHelpers"; +import { assertLeaderboards } from "web-local/tests/utils/metricsViewHelpers"; import { createModel } from "./modelHelpers"; import { uploadFile, waitForSource } from "./sourceHelpers"; diff --git a/web-local/tests/utils/exploreHelpers.ts b/web-local/tests/utils/exploreHelpers.ts new file mode 100644 index 00000000000..b491f53a80e --- /dev/null +++ b/web-local/tests/utils/exploreHelpers.ts @@ -0,0 +1,28 @@ +import type { Page } from "playwright"; +import { + clickMenuButton, + openFileNavEntryContextMenu, +} from "web-local/tests/utils/commonHelpers"; +import { waitForFileNavEntry } from "web-local/tests/utils/waitHelpers"; + +export async function createExploreFromSource( + page: Page, + sourcePath: string, + metricsViewPath: string, +) { + await openFileNavEntryContextMenu(page, sourcePath); + await clickMenuButton(page, "Generate metrics"); + await waitForFileNavEntry(page, metricsViewPath, true); + await page.getByText("Create Explore dashboard").click(); +} + +export async function createExploreFromModel( + page: Page, + modelPath: string, + metricsViewPath: string, +) { + await openFileNavEntryContextMenu(page, modelPath); + await clickMenuButton(page, "Generate metrics"); + await waitForFileNavEntry(page, metricsViewPath, true); + await page.getByText("Create Explore dashboard").click(); +} diff --git a/web-local/tests/utils/metricsViewHelpers.ts b/web-local/tests/utils/metricsViewHelpers.ts new file mode 100644 index 00000000000..1c9c355d497 --- /dev/null +++ b/web-local/tests/utils/metricsViewHelpers.ts @@ -0,0 +1,54 @@ +import { expect } from "@playwright/test"; +import type { Page } from "playwright"; +import { clickMenuButton, openFileNavEntryContextMenu } from "./commonHelpers"; + +export async function createMetricsViewFromSource( + page: Page, + sourcePath: string, +) { + await openFileNavEntryContextMenu(page, sourcePath); + await clickMenuButton(page, "Generate metrics"); +} + +export async function createMetricsViewFromModel( + page: Page, + modelPath: string, +) { + await openFileNavEntryContextMenu(page, modelPath); + await clickMenuButton(page, "Generate metrics"); +} + +export async function assertLeaderboards( + page: Page, + leaderboards: Array<{ + label: string; + values: Array; + }>, +) { + for (const { label, values } of leaderboards) { + const leaderboardBlock = page.getByRole("table", { + name: `${label} leaderboard`, + }); + await expect(leaderboardBlock).toBeVisible(); + + const actualValues = await leaderboardBlock + .locator("tr > td:nth-child(2)") + .allInnerTexts(); + expect(actualValues).toEqual(values); + } +} + +// Helper that opens the time range menu, calls your interactions, and then waits until the menu closes +export async function interactWithTimeRangeMenu( + page: Page, + cb: () => void | Promise, +) { + // Open the menu + await page.getByLabel("Select time range").click(); + // Run the defined interactions + await cb(); + // Wait for menu to close + await expect( + page.getByRole("menu", { name: "Select time range" }), + ).not.toBeVisible(); +}