-
Notifications
You must be signed in to change notification settings - Fork 166
feat: public api for embeds #6680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6f10f15
81d6583
63f5d76
ced764d
ba61995
610aabb
e5f248e
4065767
516af60
00a065b
92a2478
2e33082
fbbe87a
b002ef5
880fd04
ec97c8e
46ee2b3
92aa0b2
6fc5e76
008d5b8
abc6272
1010941
2e0c395
1f40902
cbb0655
087287e
662b5e7
523f8aa
e39c60d
23cb12e
639b877
49ad482
f84603d
382ffca
b883803
1af607c
a85c482
62e6657
bb0957c
e158eca
f71a539
5c170ef
01d6777
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| /.svelte-kit | ||
| build | ||
| build | ||
| tests/embed.html |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| import { goto } from "$app/navigation"; | ||
| import { page } from "$app/stores"; | ||
| import { get, derived, type Readable } from "svelte/store"; | ||
|
|
||
| import { getStateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers"; | ||
| import { convertExploreStateToURLSearchParams } from "@rilldata/web-common/features/dashboards/url-state/convertExploreStateToURLSearchParams"; | ||
| import { getDefaultExplorePreset } from "@rilldata/web-common/features/dashboards/url-state/getDefaultExplorePreset"; | ||
| import { useMetricsViewTimeRange } from "@rilldata/web-common/features/dashboards/selectors"; | ||
|
|
||
| import { | ||
| getTimeControlState, | ||
| type TimeControlState, | ||
| } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store"; | ||
| import { | ||
| registerRPCMethod, | ||
| emitNotification, | ||
| } from "@rilldata/web-common/lib/rpc"; | ||
|
|
||
| export default function initEmbedPublicAPI(instanceId: string): () => void { | ||
| const { | ||
| metricsViewName, | ||
| validSpecStore, | ||
| dashboardStore, | ||
| timeRangeSummaryStore, | ||
| } = getStateManagers(); | ||
|
|
||
| const metricsViewNameValue = get(metricsViewName); | ||
| const metricsViewTimeRange = useMetricsViewTimeRange( | ||
| instanceId, | ||
| metricsViewNameValue, | ||
| ); | ||
|
|
||
| const derivedState: Readable<string> = derived( | ||
| [ | ||
| validSpecStore, | ||
| dashboardStore, | ||
| timeRangeSummaryStore, | ||
| metricsViewTimeRange, | ||
| ], | ||
| ([ | ||
| $validSpecStore, | ||
| $dashboardStore, | ||
| $timeRangeSummaryStore, | ||
| $metricsViewTimeRange, | ||
| ]) => { | ||
| const exploreSpec = $validSpecStore.data?.explore ?? {}; | ||
| const metricsViewSpec = $validSpecStore.data?.metricsView ?? {}; | ||
|
|
||
| const defaultExplorePreset = getDefaultExplorePreset( | ||
| exploreSpec, | ||
| $metricsViewTimeRange?.data, | ||
| ); | ||
|
|
||
| let timeControlsState: TimeControlState | undefined = undefined; | ||
| if (metricsViewSpec && exploreSpec && $dashboardStore) { | ||
| timeControlsState = getTimeControlState( | ||
| metricsViewSpec, | ||
| exploreSpec, | ||
| $timeRangeSummaryStore.data?.timeRangeSummary, | ||
| $dashboardStore, | ||
| ); | ||
| } | ||
|
|
||
| return decodeURIComponent( | ||
| convertExploreStateToURLSearchParams( | ||
| $dashboardStore, | ||
| exploreSpec, | ||
| timeControlsState, | ||
| defaultExplorePreset, | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| const unsubscribe = derivedState.subscribe((stateString) => { | ||
| emitNotification("stateChange", { state: stateString }); | ||
| }); | ||
|
|
||
| registerRPCMethod("getState", () => { | ||
| const validSpec = get(validSpecStore); | ||
| const dashboard = get(dashboardStore); | ||
| const timeSummary = get(timeRangeSummaryStore).data; | ||
| const metricsTime = get(metricsViewTimeRange); | ||
|
|
||
| const exploreSpec = validSpec.data?.explore ?? {}; | ||
| const metricsViewSpec = validSpec.data?.metricsView ?? {}; | ||
|
|
||
| const defaultExplorePreset = getDefaultExplorePreset( | ||
| exploreSpec, | ||
| metricsTime?.data, | ||
| ); | ||
|
|
||
| let timeControlsState: TimeControlState | undefined = undefined; | ||
| if (metricsViewSpec && exploreSpec && dashboard) { | ||
| timeControlsState = getTimeControlState( | ||
| metricsViewSpec, | ||
| exploreSpec, | ||
| timeSummary?.timeRangeSummary, | ||
| dashboard, | ||
| ); | ||
| } | ||
| const stateString = decodeURIComponent( | ||
| convertExploreStateToURLSearchParams( | ||
| dashboard, | ||
| exploreSpec, | ||
| timeControlsState, | ||
| defaultExplorePreset, | ||
| ), | ||
| ); | ||
| return { state: stateString }; | ||
| }); | ||
|
|
||
| registerRPCMethod("setState", (state: string) => { | ||
| if (typeof state !== "string") { | ||
| return new Error("Expected state to be a string"); | ||
| } | ||
| const currentUrl = new URL(get(page).url); | ||
| currentUrl.search = state; | ||
| void goto(currentUrl, { replaceState: true }); | ||
| return true; | ||
| }); | ||
|
|
||
| emitNotification("ready"); | ||
|
|
||
| return unsubscribe; | ||
| } |
mindspank marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { expect } from "@playwright/test"; | ||
| import { test } from "./setup/base"; | ||
|
|
||
| import { type Page } from "@playwright/test"; | ||
|
|
||
| async function waitForReadyMessage(embedPage: Page, logMessages: string[]) { | ||
| return new Promise<void>((resolve) => { | ||
| embedPage.on("console", async (msg) => { | ||
| if (msg.type() === "log") { | ||
| const args = await Promise.all( | ||
| msg.args().map((arg) => arg.jsonValue()), | ||
| ); | ||
| const logMessage = JSON.stringify(args); | ||
| logMessages.push(logMessage); | ||
| if (logMessage.includes(`{"method":"ready"}`)) { | ||
| resolve(); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| test.describe("Embeds", () => { | ||
| test("embeds should load", async ({ embedPage }) => { | ||
| const frame = embedPage.frameLocator("iframe"); | ||
|
|
||
| await frame.getByLabel("Timezone selector").click(); | ||
| await frame.getByRole("menuitem", { name: "UTC GMT +00:00 UTC" }).click(); | ||
|
|
||
| await expect( | ||
| frame.getByRole("button", { name: "Advertising Spend Overall $1.30M" }), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| test("state is emitted for embeds", async ({ embedPage }) => { | ||
| const logMessages: string[] = []; | ||
| await waitForReadyMessage(embedPage, logMessages); | ||
| const frame = embedPage.frameLocator("iframe"); | ||
|
|
||
| await frame.getByLabel("Timezone selector").click(); | ||
| await frame.getByRole("menuitem", { name: "UTC GMT +00:00 UTC" }).click(); | ||
|
|
||
| await frame.getByRole("row", { name: "Instacart $107.3k" }).click(); | ||
| await embedPage.waitForTimeout(500); | ||
|
|
||
| expect( | ||
| logMessages.some((msg) => | ||
| msg.includes("tz=UTC&f=advertiser_name+IN+('Instacart')"), | ||
| ), | ||
| ).toBeTruthy(); | ||
| }); | ||
|
|
||
| test("getState returns from embed", async ({ embedPage }) => { | ||
| const logMessages: string[] = []; | ||
| await waitForReadyMessage(embedPage, logMessages); | ||
| const frame = embedPage.frameLocator("iframe"); | ||
|
|
||
| await frame.getByLabel("Timezone selector").click(); | ||
| await frame.getByRole("menuitem", { name: "UTC GMT +00:00 UTC" }).click(); | ||
|
|
||
| await frame.getByRole("row", { name: "Instacart $107.3k" }).click(); | ||
| await embedPage.waitForTimeout(500); | ||
|
|
||
| await embedPage.evaluate(() => { | ||
| const iframe = document.querySelector("iframe"); | ||
| iframe?.contentWindow?.postMessage({ id: 1337, method: "getState" }, "*"); | ||
| }); | ||
|
|
||
| await embedPage.waitForTimeout(500); | ||
| expect( | ||
| logMessages.some((msg) => | ||
| msg.includes( | ||
| `{"id":1337,"result":{"state":"tz=UTC&f=advertiser_name+IN+('Instacart')"}}`, | ||
| ), | ||
| ), | ||
| ).toBeTruthy(); | ||
| }); | ||
|
|
||
| test("setState changes embedded explore", async ({ embedPage }) => { | ||
| const logMessages: string[] = []; | ||
| await waitForReadyMessage(embedPage, logMessages); | ||
| const frame = embedPage.frameLocator("iframe"); | ||
|
|
||
| await embedPage.evaluate(() => { | ||
| const iframe = document.querySelector("iframe"); | ||
| iframe?.contentWindow?.postMessage( | ||
| { | ||
| id: 1337, | ||
| method: "setState", | ||
| params: "tz=UTC&f=advertiser_name+IN+('Instacart')", | ||
| }, | ||
| "*", | ||
| ); | ||
| }); | ||
|
|
||
| await expect(frame.getByLabel("Timezone selector")).toHaveText("UTC"); | ||
| await expect( | ||
| frame.getByRole("row", { name: "Instacart $107.3k" }), | ||
| ).toBeVisible(); | ||
| expect( | ||
| logMessages.some((msg) => msg.includes(`{"id":1337,"result":true}`)), | ||
| ).toBeTruthy(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,22 @@ | ||
| import { test as base, type Page } from "@playwright/test"; | ||
| import { ADMIN_STORAGE_STATE, VIEWER_STORAGE_STATE } from "./constants"; | ||
| import { cliLogin, cliLogout } from "./fixtures/cli"; | ||
| import path from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| import { | ||
| RILL_EMBED_SERVICE_TOKEN_FILE, | ||
| RILL_ORG_NAME, | ||
| RILL_PROJECT_NAME, | ||
| } from "./constants"; | ||
| import fs from "fs"; | ||
| import { generateEmbed } from "../utils/generate-embed"; | ||
|
|
||
| type MyFixtures = { | ||
| adminPage: Page; | ||
| viewerPage: Page; | ||
| anonPage: Page; | ||
| cli: void; | ||
| embedPage: Page; | ||
| }; | ||
|
|
||
| export const test = base.extend<MyFixtures>({ | ||
|
|
@@ -44,4 +54,34 @@ export const test = base.extend<MyFixtures>({ | |
| await use(); | ||
| await cliLogout(); | ||
| }, | ||
|
|
||
| embedPage: [ | ||
| async ({ browser }, use) => { | ||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| const readPath = path.join( | ||
| __dirname, | ||
| "..", | ||
| "..", | ||
| RILL_EMBED_SERVICE_TOKEN_FILE, | ||
| ); | ||
| const rillServiceToken = fs.readFileSync(readPath, "utf-8"); | ||
|
|
||
| await generateEmbed( | ||
| RILL_ORG_NAME, | ||
| RILL_PROJECT_NAME, | ||
| "bids_explore", | ||
| rillServiceToken, | ||
| ); | ||
| const filePath = "file://" + path.resolve(__dirname, "..", "embed.html"); | ||
|
Comment on lines
+69
to
+75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion, so we don't have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll keep the two versions for now since |
||
|
|
||
| const context = await browser.newContext(); | ||
| const embedPage = await context.newPage(); | ||
| await embedPage.goto(filePath); | ||
|
|
||
| await use(embedPage); | ||
|
|
||
| await context.close(); | ||
| }, | ||
| { scope: "test" }, | ||
| ], | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kept the initializing here if we want to support state for canvas in the future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Though for now I'd propose this code would be easier-to-follow (more co-located) if this went in the
initEmbedPublicAPI()function.