Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6f10f15
feat: first rpc stub
mindspank Feb 11, 2025
81d6583
add emit
mindspank Feb 11, 2025
63f5d76
feat: embed public api
mindspank Feb 14, 2025
ced764d
fix: better name for state stream
mindspank Feb 14, 2025
ba61995
fix: error handling and better spec conformatiy
mindspank Feb 14, 2025
610aabb
fix: cleanup
mindspank Feb 14, 2025
e5f248e
Merge branch 'main' into feat/emit-state-embed
mindspank Feb 14, 2025
4065767
fix: format
mindspank Feb 14, 2025
516af60
Merge branch 'feat/emit-state-embed' of github.com:rilldata/rill into…
mindspank Feb 14, 2025
00a065b
fix: make id optional
mindspank Feb 16, 2025
92a2478
Merge remote-tracking branch 'origin/main' into feat/emit-state-embed
mindspank Feb 16, 2025
2e33082
fix: first stub of tests
mindspank Feb 16, 2025
fbbe87a
Revert "fix: first stub of tests"
mindspank Feb 17, 2025
b002ef5
fix: clear method names
mindspank Feb 17, 2025
880fd04
fix: check input
mindspank Feb 17, 2025
ec97c8e
fix: gitignore
mindspank Feb 17, 2025
46ee2b3
fix: ignore
mindspank Feb 17, 2025
92aa0b2
Revert "fix: ignore"
mindspank Feb 17, 2025
6fc5e76
Revert "fix: gitignore"
mindspank Feb 17, 2025
008d5b8
fix: add tests
mindspank Feb 17, 2025
abc6272
Merge remote-tracking branch 'origin/main' into feat/emit-state-embed
mindspank Feb 17, 2025
1010941
Merge remote-tracking branch 'origin/main' into feat/emit-state-embed
mindspank Feb 17, 2025
2e0c395
fix: comments
mindspank Feb 18, 2025
1f40902
fix: moving org and project to const
mindspank Feb 18, 2025
cbb0655
fix: moving embed to test hook
mindspank Feb 18, 2025
087287e
fix: format
mindspank Feb 18, 2025
662b5e7
Merge branch 'main' into feat/emit-state-embed
mindspank Feb 18, 2025
523f8aa
fix: read token
mindspank Feb 18, 2025
e39c60d
fix: more fixes
mindspank Feb 19, 2025
23cb12e
fix: comments
mindspank Feb 19, 2025
639b877
fix: format
mindspank Feb 19, 2025
49ad482
.gitignore
mindspank Feb 19, 2025
f84603d
delete test file
mindspank Feb 19, 2025
382ffca
fix: pr comments
mindspank Feb 20, 2025
b883803
fix: cleaner init
mindspank Feb 20, 2025
1af607c
Merge remote-tracking branch 'origin/main' into feat/emit-state-embed
mindspank Feb 20, 2025
a85c482
fix: format
mindspank Feb 20, 2025
62e6657
fix: lint
mindspank Feb 20, 2025
bb0957c
fix: more lint (in my pocket)
mindspank Feb 20, 2025
e158eca
fix: wait for bids
mindspank Feb 21, 2025
f71a539
fix: format
mindspank Feb 21, 2025
5c170ef
fix: nits
mindspank Feb 21, 2025
01d6777
fix: format
mindspank Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion web-admin/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/.svelte-kit
build
build
tests/embed.html
1 change: 1 addition & 0 deletions web-admin/src/features/embeds/ExploreEmbed.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
});
$: ({ isSuccess, isError, error, data } = $explore);
$: isExploreNotFound = isError && error?.response?.status === 404;

// We check for explore.state.validSpec instead of meta.reconcileError. validSpec persists
// from previous valid explores, allowing display even when the current explore spec is invalid
// and a meta.reconcileError exists.
Expand Down
126 changes: 126 additions & 0 deletions web-admin/src/features/embeds/init-embed-public-api.ts
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;
}
6 changes: 6 additions & 0 deletions web-admin/src/routes/-/embed/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<script lang="ts">
import { page } from "$app/stores";
import { onMount } from "svelte";
import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte";
import { createIframeRPCHandler } from "@rilldata/web-common/lib/rpc";

const instanceId = $page.url.searchParams.get("instance_id");
const runtimeHost = $page.url.searchParams
.get("runtime_host")
.replace("localhost:9091", "localhost:8081");
const accessToken = $page.url.searchParams.get("access_token");

onMount(() => {
Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor

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.

createIframeRPCHandler();
});
</script>

<RuntimeProvider
Expand Down
104 changes: 104 additions & 0 deletions web-admin/tests/embeds.spec.ts
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();
});
});
40 changes: 40 additions & 0 deletions web-admin/tests/setup/base.ts
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>({
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion, so we don't have embed.html hardcoded in two places:

const embedPath = await generateEmbed(
  RILL_ORG_NAME,
  RILL_PROJECT_NAME,
  "bids_explore",
  rillServiceToken
);
const filePath = "file://" + path.resolve(embedPath);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep the two versions for now since resolve is relative to the callers directory. Could make sense to move into a constant though since the file location would always have to be in the root of tests


const context = await browser.newContext();
const embedPage = await context.newPage();
await embedPage.goto(filePath);

await use(embedPage);

await context.close();
},
{ scope: "test" },
],
});
5 changes: 5 additions & 0 deletions web-admin/tests/setup/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export const VIEWER_STORAGE_STATE = "playwright/.auth/viewer.json";

export const RILL_DEVTOOL_BACKGROUND_PROCESS_PID_FILE =
"playwright/rill-devtool-background-process-pid.txt";
export const RILL_EMBED_SERVICE_TOKEN_FILE =
"playwright/rill-service-token.txt";
export const RILL_ORG_NAME = "e2e";
export const RILL_PROJECT_NAME = "openrtb";
export const RILL_SERVICE_NAME = "e2e";
Loading
Loading