-
-
- {#if $selectedView === "code"}
-
- {:else}
- {#key fileArtifact}
-
+ {/snippet}
+
+
+
+
+
+
+ {#if effectiveView === "code"}
+ {
- $selectedView = "code";
- }}
+ {filePath}
+ {parseError}
+ {metricsViewName}
/>
- {/key}
- {/if}
-
+ {:else if effectiveView === "explore"}
+ {#if parseError || rootCauseReconcileError}
+
+
+
+
+
+ {:else if exploreResource && metricsViewName}
+
+
+
+ {:else}
+
+ {/if}
+ {:else}
+ {#key fileArtifact}
+ {
+ $selectedView = "code";
+ }}
+ />
+ {/key}
+ {/if}
+
+
+
-
-
-
-
-
-
+
+
+
+ {#if effectiveView === "explore"}
+ {#if ready}
+ {
+ $selectedView = "code";
+ }}
+ />
+ {/if}
+ {:else}
+
+ {/if}
+
+
+{/snippet}
+
+{#if exploreEnabled && metricsViewName}
+ {#key metricsViewName + inlineExploreName}
+
+ {@render workspaceContent(ready)}
+
+ {/key}
+{:else}
+ {@render workspaceContent(false)}
+{/if}
diff --git a/web-common/src/features/workspaces/VisualExploreEditing.svelte b/web-common/src/features/workspaces/VisualExploreEditing.svelte
index cd7d87a925f1..0e3ec815439a 100644
--- a/web-common/src/features/workspaces/VisualExploreEditing.svelte
+++ b/web-common/src/features/workspaces/VisualExploreEditing.svelte
@@ -56,6 +56,12 @@
export let viewingDashboard: boolean;
export let autoSave: boolean;
export let switchView: () => void;
+ // When the explore is defined inline in a metrics view file, keyPath points at the
+ // nested block holding the explore properties (e.g. ["explore"]). Empty for a
+ // standalone explore file, where the properties live at the top level.
+ export let keyPath: string[] = [];
+
+ $: isInlineExplore = keyPath.length > 0;
const runtimeClient = useRuntimeClient();
const StateManagers = getStateManagers();
@@ -109,15 +115,15 @@
$: metricsViewSpec = metricsViewResource?.state?.validSpec;
- $: rawTitle = parsedDocument.get("title");
- $: rawDisplayName = parsedDocument.get("display_name");
- $: rawMetricsView = parsedDocument.get("metrics_view");
- $: rawDimensions = parsedDocument.get("dimensions");
- $: rawMeasures = parsedDocument.get("measures");
- $: rawTimeZones = parsedDocument.get("time_zones");
- $: rawTheme = parsedDocument.get("theme");
- $: rawTimeRanges = parsedDocument.get("time_ranges");
- $: rawDefaults = parsedDocument.get("defaults");
+ $: rawTitle = parsedDocument.getIn([...keyPath, "title"]);
+ $: rawDisplayName = parsedDocument.getIn([...keyPath, "display_name"]);
+ $: rawMetricsView = parsedDocument.getIn([...keyPath, "metrics_view"]);
+ $: rawDimensions = parsedDocument.getIn([...keyPath, "dimensions"]);
+ $: rawMeasures = parsedDocument.getIn([...keyPath, "measures"]);
+ $: rawTimeZones = parsedDocument.getIn([...keyPath, "time_zones"]);
+ $: rawTheme = parsedDocument.getIn([...keyPath, "theme"]);
+ $: rawTimeRanges = parsedDocument.getIn([...keyPath, "time_ranges"]);
+ $: rawDefaults = parsedDocument.getIn([...keyPath, "defaults"]);
$: resourceTags = readRootYamlTags(parsedDocument);
$: tagSuggestions = getResourceTagSuggestions(
@@ -260,9 +266,9 @@
) {
Object.entries(newRecord).forEach(([property, value]) => {
if (!value) {
- parsedDocument.delete(property);
+ parsedDocument.deleteIn([...keyPath, property]);
} else {
- parsedDocument.set(property, value);
+ parsedDocument.setIn([...keyPath, property], value);
}
});
@@ -270,9 +276,9 @@
removeProperties.forEach((prop) => {
try {
if (Array.isArray(prop)) {
- parsedDocument.deleteIn(prop);
+ parsedDocument.deleteIn([...keyPath, ...prop]);
} else {
- parsedDocument.delete(prop);
+ parsedDocument.deleteIn([...keyPath, prop]);
}
} catch {
// ignore
@@ -410,32 +416,34 @@
onChange={updateResourceTags}
/>
-
({
- label: name,
- value: name,
- }))}
- onChange={() => {
- killState();
-
- updateProperties(
- {
- metrics_view: metricsView,
- measures: "*",
- dimensions: "*",
- },
- ["defaults"],
- );
- }}
- />
+ {#if !isInlineExplore}
+
({
+ label: name,
+ value: name,
+ }))}
+ onChange={() => {
+ killState();
+
+ updateProperties(
+ {
+ metrics_view: metricsView,
+ measures: "*",
+ dimensions: "*",
+ },
+ ["defaults"],
+ );
+ }}
+ />
+ {/if}
{#each itemTypes as type (type)}
{@const items = type === "measures" ? measures : dimensions}
@@ -558,14 +566,30 @@
const altMode = isDarkMode ? "light" : "dark";
// check if theme exists for alt mode
- const setAltMode = !parsedDocument.hasIn(["theme", altMode]);
-
- parsedDocument.setIn(["theme", modeKey, "primary"], primary);
- parsedDocument.setIn(["theme", modeKey, "secondary"], secondary);
+ const setAltMode = !parsedDocument.hasIn([
+ ...keyPath,
+ "theme",
+ altMode,
+ ]);
+
+ parsedDocument.setIn(
+ [...keyPath, "theme", modeKey, "primary"],
+ primary,
+ );
+ parsedDocument.setIn(
+ [...keyPath, "theme", modeKey, "secondary"],
+ secondary,
+ );
if (setAltMode) {
- parsedDocument.setIn(["theme", altMode, "primary"], primary);
- parsedDocument.setIn(["theme", altMode, "secondary"], secondary);
+ parsedDocument.setIn(
+ [...keyPath, "theme", altMode, "primary"],
+ primary,
+ );
+ parsedDocument.setIn(
+ [...keyPath, "theme", altMode, "secondary"],
+ secondary,
+ );
}
killState();
diff --git a/web-common/src/layout/workspace/WorkspaceHeader.svelte b/web-common/src/layout/workspace/WorkspaceHeader.svelte
index bc497d43e925..61bf01c2ad68 100644
--- a/web-common/src/layout/workspace/WorkspaceHeader.svelte
+++ b/web-common/src/layout/workspace/WorkspaceHeader.svelte
@@ -8,7 +8,9 @@
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
import { getIconComponent } from "@rilldata/web-common/features/entity-management/resource-icon-mapping";
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
- import CodeToggle from "@rilldata/web-common/features/visual-editing/CodeToggle.svelte";
+ import CodeToggle, {
+ type ViewOption,
+ } from "@rilldata/web-common/features/visual-editing/CodeToggle.svelte";
import WorkspaceBreadcrumbs from "@rilldata/web-common/features/workspaces/WorkspaceBreadcrumbs.svelte";
import type { V1Resource } from "@rilldata/web-common/runtime-client";
import { navigationOpen } from "../navigation/Navigation.svelte";
@@ -27,6 +29,7 @@
hasUnsavedChanges,
filePath,
codeToggle = false,
+ codeToggleViews = undefined,
showBreadcrumbs = true,
onTitleChange,
workspaceControls,
@@ -41,6 +44,7 @@
hasUnsavedChanges: boolean;
filePath: string;
codeToggle?: boolean;
+ codeToggleViews?: ViewOption[] | undefined;
showBreadcrumbs?: boolean;
onTitleChange?: (title: string) => void;
workspaceControls?: Snippet<[number]>;
@@ -79,7 +83,7 @@
{#if codeToggle && resourceKind}
-
+
{:else}
diff --git a/web-common/src/layout/workspace/workspace-stores.ts b/web-common/src/layout/workspace/workspace-stores.ts
index db47fb5f848f..4db34f017fe7 100644
--- a/web-common/src/layout/workspace/workspace-stores.ts
+++ b/web-common/src/layout/workspace/workspace-stores.ts
@@ -5,6 +5,8 @@ import {
DEFAULT_PREVIEW_TABLE_HEIGHT,
} from "../config";
+export type WorkspaceView = "code" | "split" | "viz" | "explore";
+
type WorkspaceLayout = {
inspector: {
width: number;
@@ -14,7 +16,7 @@ type WorkspaceLayout = {
height: number;
visible: boolean;
};
- view: "code" | "split" | "viz";
+ view: WorkspaceView;
};
class WorkspaceLayoutStore {
@@ -22,7 +24,7 @@ class WorkspaceLayoutStore {
private inspectorWidth = writable(DEFAULT_INSPECTOR_WIDTH);
private tableVisible = writable(true);
private tableHeight = writable(DEFAULT_PREVIEW_TABLE_HEIGHT);
- public view = writable<"code" | "split" | "viz">("viz");
+ public view = writable("viz");
constructor(key: string) {
const history = localStorage.getItem(key);
diff --git a/web-local/tests/breadcrumbs.spec.ts b/web-local/tests/breadcrumbs.spec.ts
index 068038777b5e..9eecef7b6005 100644
--- a/web-local/tests/breadcrumbs.spec.ts
+++ b/web-local/tests/breadcrumbs.spec.ts
@@ -27,39 +27,16 @@ test.describe("Breadcrumbs", () => {
exact: true,
});
- await expect(link).toBeVisible();
- await expect(link).toHaveClass(/selected/g);
-
- await page.getByText("Generate Explore Dashboard").click();
-
- await page.waitForURL("**/files/dashboards/AdBids_metrics_explore.yaml");
-
- link = page.getByRole("link", {
- name: "AdBids_metrics_explore",
- exact: true,
- });
+ // The generated metrics view defines its explore dashboard inline, so the
+ // metrics view crumb and the dashboard crumb both point at this file.
+ await expect(link.first()).toBeVisible();
+ await expect(link.first()).toHaveClass(/selected/g);
- await expect(link).toBeVisible();
- await expect(link).toHaveClass(/selected/g);
-
- await page
- .getByRole("link", {
- name: "AdBids_metrics",
- exact: true,
- })
- .click();
-
- await page.getByRole("button", { name: "Create resource menu" }).click();
await page
- .getByRole("menuitem", { name: "Generate Explore Dashboard" })
+ .getByRole("link", { name: "AdBids", exact: true })
+ .first()
.click();
- await page.waitForURL(
- "**/files/dashboards/AdBids_metrics_explore_1.yaml",
- );
-
- await page.getByRole("link", { name: "AdBids", exact: true }).click();
-
await page.waitForURL("**/files/models/AdBids.yaml");
await expect(
@@ -70,36 +47,20 @@ test.describe("Breadcrumbs", () => {
).toBeVisible();
await expect(
- page.getByRole("link", {
- name: "AdBids_metrics",
- exact: true,
- }),
- ).toBeVisible();
-
- await expect(
- page.getByRole("button", {
- name: "2 dashboards",
- exact: true,
- }),
+ page
+ .getByRole("link", {
+ name: "AdBids_metrics",
+ exact: true,
+ })
+ .first(),
).toBeVisible();
await page
.getByRole("link", { name: "AdBids_metrics", exact: true })
+ .first()
.click();
await page.waitForURL("**/files/metrics/AdBids_metrics.yaml");
-
- await page
- .getByRole("button", { name: "2 dashboards", exact: true })
- .click();
- await page
- .getByRole("menuitem", {
- name: "AdBids_metrics_explore",
- exact: true,
- })
- .click();
-
- await page.waitForURL("**/files/dashboards/AdBids_metrics_explore.yaml");
});
});
});
diff --git a/web-local/tests/explores/explores.spec.ts b/web-local/tests/explores/explores.spec.ts
index d3676f4907bb..494db2b45f5a 100644
--- a/web-local/tests/explores/explores.spec.ts
+++ b/web-local/tests/explores/explores.spec.ts
@@ -3,8 +3,6 @@ import { interactWithTimeRangeMenu } from "@rilldata/web-common/tests/utils/expl
import { test } from "../setup/base";
import { updateCodeEditor, wrapRetryAssertion } from "../utils/commonHelpers";
import {
- AD_BIDS_EXPLORE_PATH,
- AD_BIDS_METRICS_PATH,
assertAdBidsDashboard,
createAdBidsModel,
} from "../utils/dataSpecifcHelpers";
@@ -16,7 +14,6 @@ import {
import { assertLeaderboards } from "../utils/metricsViewHelpers";
import { ResourceWatcher } from "../utils/ResourceWatcher";
import { createSourceV2 } from "../utils/sourceHelpers";
-import { gotoNavEntry } from "../utils/waitHelpers";
test.describe("explores", () => {
test.use({ project: "Blank" });
@@ -68,37 +65,47 @@ test.describe("explores", () => {
await page.getByRole("button", { name: "switch to code editor" }).click();
- // Add `inf` alias to the time range
+ // Add `inf` alias to the time range of the inline explore
const addAllTime = `
-type: explore
-
-title: "Adbids dashboard"
-metrics_view: AdBids_model_metrics
-
-dimensions: '*'
-measures: '*'
-
-time_ranges:
- - PT6H
- - PT24H
- - P7D
- - P14D
- - P4W
- - P12M
- - rill-TD
- - rill-WTD
- - rill-MTD
- - rill-QTD
- - rill-YTD
- - rill-PDC
- - rill-PWC
- - rill-PMC
- - rill-PQC
- - rill-PYC
- - inf
+version: 1
+type: metrics_view
+
+model: AdBids_model
+timeseries: timestamp
+
+dimensions:
+ - column: publisher
+ - column: domain
+
+measures:
+ - name: total_records
+ display_name: Total records
+ expression: COUNT(*)
+ format_preset: humanize
+
+explore:
+ display_name: "Adbids dashboard"
+ time_ranges:
+ - PT6H
+ - PT24H
+ - P7D
+ - P14D
+ - P4W
+ - P12M
+ - rill-TD
+ - rill-WTD
+ - rill-MTD
+ - rill-QTD
+ - rill-YTD
+ - rill-PDC
+ - rill-PWC
+ - rill-PMC
+ - rill-PQC
+ - rill-PYC
+ - inf
`;
- await watcher.updateAndWaitForExplore(addAllTime);
+ await watcher.updateAndWaitForExplore(addAllTime, "AdBids_model_metrics");
await clickPreviewButton(page);
@@ -255,42 +262,56 @@ time_ranges:
// Check that the data is updated for last 6 hours
// Change time range back to all time
- // Edit Explore
+ // Edit Explore (defined inline in the metrics view file)
await page.getByRole("button", { name: "Edit" }).click();
await page.getByRole("menuitem", { name: "Explore" }).click();
+ await page.getByRole("button", { name: "switch to code editor" }).click();
// Get the dashboard name field and change it
const changeDisplayNameDoc = `
-type: explore
-
-title: "Adbids dashboard renamed"
-metrics_view: AdBids_model_metrics
-
-dimensions: '*'
-measures: '*'
-
-time_ranges:
- - PT6H
- - PT24H
- - P7D
- - P14D
- - P4W
- - P12M
- - rill-TD
- - rill-WTD
- - rill-MTD
- - rill-QTD
- - rill-YTD
- - rill-PDC
- - rill-PWC
- - rill-PMC
- - rill-PQC
- - rill-PYC
- - inf
+version: 1
+type: metrics_view
+
+model: AdBids_model
+timeseries: timestamp
+
+dimensions:
+ - column: publisher
+ - column: domain
+
+measures:
+ - name: total_records
+ display_name: Total records
+ expression: COUNT(*)
+ format_preset: humanize
+
+explore:
+ display_name: "Adbids dashboard renamed"
+ time_ranges:
+ - PT6H
+ - PT24H
+ - P7D
+ - P14D
+ - P4W
+ - P12M
+ - rill-TD
+ - rill-WTD
+ - rill-MTD
+ - rill-QTD
+ - rill-YTD
+ - rill-PDC
+ - rill-PWC
+ - rill-PMC
+ - rill-PQC
+ - rill-PYC
+ - inf
`;
- await watcher.updateAndWaitForExplore(changeDisplayNameDoc);
+ await watcher.updateAndWaitForExplore(
+ changeDisplayNameDoc,
+ "AdBids_model_metrics",
+ );
// Remove timestamp column
// await page.getByLabel("Remove timestamp column").click();
@@ -313,37 +334,33 @@ time_ranges:
const addBackTimestampColumnDoc = `# Visit https://docs.rilldata.com/reference/project-files to learn more about Rill project files.
- version: 1
- type: metrics_view
- title: "AdBids_model_dashboard"
- model: "AdBids_model"
- default_time_range: ""
- smallest_time_grain: "week"
- timeseries: "timestamp"
- measures:
- - label: Total records
- expression: count(*)
- name: total_records
- description: Total number of records present
- format_preset: humanize
- dimensions:
- - name: publisher
- label: Publisher
- column: publisher
- description: ""
- - name: domain
- label: Domain
- column: domain
- description: ""
-
- `;
+version: 1
+type: metrics_view
+title: "AdBids_model_dashboard"
+model: "AdBids_model"
+smallest_time_grain: "week"
+timeseries: "timestamp"
+measures:
+ - label: Total records
+ expression: count(*)
+ name: total_records
+ description: Total number of records present
+ format_preset: humanize
+dimensions:
+ - name: publisher
+ label: Publisher
+ column: publisher
+ description: ""
+ - name: domain
+ label: Domain
+ column: domain
+ description: ""
+explore:
+ display_name: "Adbids dashboard renamed"
+`;
await page.getByRole("button", { name: "switch to code editor" }).click();
await watcher.updateAndWaitForDashboard(addBackTimestampColumnDoc);
- await page.getByRole("button", { name: "Create resource menu" }).click();
- await page
- .getByRole("menuitem", { name: "Adbids dashboard renamed" })
- .click();
// Preview
await clickPreviewButton(page);
@@ -351,40 +368,37 @@ time_ranges:
// Assert that time dimension is now week
await expect(timeGrainSelector).toHaveText("as of latest week end");
- // Edit Explore
+ // Edit the metrics view (the explore is defined inline in the same file)
await page.getByRole("button", { name: "Edit" }).click();
- await page.getByRole("menuitem", { name: "Explore" }).click();
-
- await gotoNavEntry(page, AD_BIDS_METRICS_PATH);
+ await page.getByRole("menuitem", { name: "Metrics View" }).click();
+ await page.getByRole("button", { name: "switch to code editor" }).click();
// Write an incomplete measure
const docWithIncompleteMeasure = `# Visit https://docs.rilldata.com/reference/project-files to learn more about Rill project files.
- version: 1
- type: metrics_view
- title: "AdBids_model_dashboard"
- model: "AdBids_model"
- default_time_range: ""
- smallest_time_grain: "week"
- timeseries: "timestamp"
- measures:
- - label: Avg Bid Price
- dimensions:
- - name: publisher
- label: Publisher
- column: publisher
- description: ""
- - name: domain
- label: Domain
- column: domain
- description: ""
-
- `;
+version: 1
+type: metrics_view
+title: "AdBids_model_dashboard"
+model: "AdBids_model"
+smallest_time_grain: "week"
+timeseries: "timestamp"
+measures:
+ - label: Avg Bid Price
+dimensions:
+ - name: publisher
+ label: Publisher
+ column: publisher
+ description: ""
+ - name: domain
+ label: Domain
+ column: domain
+ description: ""
+explore:
+ display_name: "Adbids dashboard renamed"
+`;
await updateCodeEditor(page, docWithIncompleteMeasure);
- await gotoNavEntry(page, AD_BIDS_EXPLORE_PATH);
await expect(page.getByRole("button", { name: "Preview" })).toBeDisabled();
- await gotoNavEntry(page, AD_BIDS_METRICS_PATH);
// Complete the measure
const docWithCompleteMeasure = `# Visit https://docs.rilldata.com/reference/project-files to learn more about Rill project files.
@@ -393,7 +407,6 @@ version: 1
type: metrics_view
title: "AdBids_model_dashboard_rename"
model: "AdBids_model"
-default_time_range: ""
smallest_time_grain: "week"
timeseries: "timestamp"
measures:
@@ -415,10 +428,11 @@ dimensions:
label: Domain Name
column: domain
description: ""
- `;
+explore:
+ display_name: "Adbids dashboard renamed"
+`;
await updateCodeEditor(page, docWithCompleteMeasure);
- await gotoNavEntry(page, AD_BIDS_EXPLORE_PATH);
await expect(page.getByRole("button", { name: "Preview" })).toBeEnabled();
// Preview
diff --git a/web-local/tests/explores/inline-explore-editing.spec.ts b/web-local/tests/explores/inline-explore-editing.spec.ts
new file mode 100644
index 000000000000..38b1a9886b5f
--- /dev/null
+++ b/web-local/tests/explores/inline-explore-editing.spec.ts
@@ -0,0 +1,149 @@
+import { expect } from "@playwright/test";
+import { test } from "../setup/base";
+import { updateCodeEditor } from "../utils/commonHelpers";
+import {
+ assertAdBidsDashboard,
+ createAdBidsModel,
+} from "../utils/dataSpecifcHelpers";
+import { createExploreFromModel } from "../utils/exploreHelpers";
+import { gotoNavEntry } from "../utils/waitHelpers";
+
+test.describe("inline explore editing", () => {
+ test.use({ project: "Blank" });
+
+ test("edit the inline explore from the metrics view workspace", async ({
+ page,
+ }) => {
+ test.setTimeout(60_000);
+
+ // Generate a metrics view, which defines its explore dashboard inline
+ await createAdBidsModel(page);
+ await createExploreFromModel(page);
+
+ // Switch to the explore view via the three-way editor toggle
+ await page
+ .getByRole("button", { name: "switch to explore editor" })
+ .click();
+
+ // The visual explore editor renders next to the live dashboard preview
+ await expect(page.getByText("Edit dashboard")).toBeVisible();
+ await assertAdBidsDashboard(page);
+
+ // Restrict measures and dimensions to a subset via the sidebar
+ await page.getByRole("button", { name: "Subset" }).first().click();
+ await page.getByRole("button", { name: "Subset" }).nth(1).click();
+
+ // The edits land under the `explore:` key of the metrics view file
+ await page.getByRole("button", { name: "switch to code editor" }).click();
+ const editor = page.getByRole("textbox", { name: "codemirror editor" });
+ const editorText = () => editor.textContent();
+ await expect.poll(editorText, { timeout: 10_000 }).toContain("explore:");
+ await expect.poll(editorText).toContain("type: metrics_view");
+
+ // The explore block sits at the end of the file, which can fall outside
+ // CodeMirror's virtualized viewport; page down to the end to render it.
+ await editor.click();
+ for (let i = 0; i < 5; i++) {
+ await page.keyboard.press("PageDown");
+ }
+ await expect.poll(editorText, { timeout: 10_000 }).toContain("- publisher");
+ await expect
+ .poll(editorText, { timeout: 10_000 })
+ .toContain("- total_records");
+ });
+
+ test("header CTA offers canvas generation", async ({ page }) => {
+ test.setTimeout(60_000);
+
+ await createAdBidsModel(page);
+ await createExploreFromModel(page);
+
+ // The inline explore is reachable via the Preview button and the editor toggle,
+ // so the CTA only deals in canvases. With none in the project yet, it offers
+ // canvas generation, but neither "Go to dashboard" nor another explore.
+ await expect(
+ page.getByRole("button", { name: "Generate Canvas Dashboard" }),
+ ).toBeVisible({ timeout: 10_000 });
+ await expect(
+ page.getByRole("button", { name: "Generate Explore Dashboard" }),
+ ).toHaveCount(0);
+ await expect(page.getByRole("link", { name: /Go to/ })).toHaveCount(0);
+ });
+});
+
+test.describe("inline explore go-to-dashboard CTA", () => {
+ test.use({ project: "AdBids" });
+
+ test("lists the canvas dashboards of the metrics view", async ({ page }) => {
+ test.setTimeout(60_000);
+
+ // Define the explore inline in the existing metrics view, which the project's
+ // canvas dashboard is built on
+ await page.getByLabel("/metrics").click();
+ await gotoNavEntry(page, "/metrics/AdBids_metrics.yaml");
+ await page.getByRole("button", { name: "switch to code editor" }).click();
+ await updateCodeEditor(page, METRICS_VIEW_WITH_INLINE_EXPLORE);
+
+ // The header CTA links to the metrics view's canvas dashboards
+ await expect(
+ page.getByRole("link", { name: "Go to canvas dashboard" }),
+ ).toBeVisible({ timeout: 10_000 });
+ await page.getByRole("button", { name: "Create resource menu" }).click();
+ await expect(
+ page.getByRole("menuitem", { name: "Adbids Canvas Dashboard" }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("menuitem", { name: "Generate Canvas Dashboard" }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("menuitem", { name: "Generate Explore Dashboard" }),
+ ).toHaveCount(0);
+
+ // The project's standalone explore is not listed; the CTA only deals in canvases
+ await expect(
+ page.getByRole("menuitem", { name: "Adbids dashboard" }),
+ ).toHaveCount(0);
+
+ await page
+ .getByRole("menuitem", { name: "Adbids Canvas Dashboard" })
+ .click();
+ await page.waitForURL("**/files/dashboards/AdBids_metrics_canvas.yaml");
+ });
+});
+
+const METRICS_VIEW_WITH_INLINE_EXPLORE = `version: 1
+type: metrics_view
+
+display_name: Adbids
+table: AdBids_model
+timeseries: timestamp
+
+dimensions:
+ - name: publisher
+ display_name: Publisher
+ column: publisher
+ - name: domain
+ display_name: Domain
+ column: domain
+ - name: timestamp
+ display_name: Timestamp
+ column: timestamp
+ type: time
+ - name: offset_timestamp
+ display_name: Offset Timestamp
+ column: offset_timestamp
+ type: time
+
+measures:
+ - name: total_records
+ display_name: Total records
+ expression: COUNT(*)
+ format_preset: humanize
+ - name: bid_price_sum
+ display_name: Sum of Bid Price
+ expression: SUM(bid_price)
+ format_preset: humanize
+
+explore:
+ display_name: Adbids Inline Explore
+`;
diff --git a/web-local/tests/utils/dataSpecifcHelpers.ts b/web-local/tests/utils/dataSpecifcHelpers.ts
index ace6f1c13092..04c07927dcf0 100644
--- a/web-local/tests/utils/dataSpecifcHelpers.ts
+++ b/web-local/tests/utils/dataSpecifcHelpers.ts
@@ -19,8 +19,6 @@ export interface TimeSeriesResponse {
}
export const AD_BIDS_METRICS_PATH = "/metrics/AdBids_model_metrics.yaml";
-export const AD_BIDS_EXPLORE_PATH =
- "/dashboards/AdBids_model_metrics_explore.yaml";
export async function createAdBidsModel(page: Page) {
await Promise.all([
diff --git a/web-local/tests/utils/exploreHelpers.ts b/web-local/tests/utils/exploreHelpers.ts
index bf2706a0c9bc..689a6a5dfe21 100644
--- a/web-local/tests/utils/exploreHelpers.ts
+++ b/web-local/tests/utils/exploreHelpers.ts
@@ -17,6 +17,8 @@ export async function clickPreviewButton(page: Page, timeout = 10_000) {
await previewButton.click();
}
+// Generated metrics views define their explore dashboard inline, so generating
+// metrics is enough to get an explore; these helpers optionally open its preview.
export async function createExploreFromSource(
page: Page,
sourcePath = "/models/AdBids.yaml",
@@ -25,7 +27,7 @@ export async function createExploreFromSource(
await openFileNavEntryContextMenu(page, sourcePath);
await clickMenuButton(page, "Generate metrics");
await waitForFileNavEntry(page, metricsViewPath, true);
- await page.getByText("Generate Explore Dashboard").click();
+ await clickPreviewButton(page);
}
export async function createExploreFromModel(
@@ -37,7 +39,6 @@ export async function createExploreFromModel(
await openFileNavEntryContextMenu(page, modelPath);
await clickMenuButton(page, "Generate metrics");
await waitForFileNavEntry(page, metricsViewPath, true);
- await page.getByText("Generate Explore Dashboard").click();
if (navigateToPreview) {
await clickPreviewButton(page);