diff --git a/web-admin/src/features/embeds/EmbedReportButton.svelte b/web-admin/src/features/embeds/EmbedReportButton.svelte
new file mode 100644
index 000000000000..dae6c333ac94
--- /dev/null
+++ b/web-admin/src/features/embeds/EmbedReportButton.svelte
@@ -0,0 +1,56 @@
+
+
+{#if enableReports}
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+
+ Go to reports
+
+ {#if activeResource?.kind === ResourceKind.Explore}
+ (open = true)}>
+ Create report
+
+ {/if}
+
+
+{/if}
+
+{#if open && activeResource?.name}
+
+{/if}
diff --git a/web-admin/src/features/embeds/init-embed-public-api.ts b/web-admin/src/features/embeds/init-embed-public-api.ts
index 34f1b791c642..9bda5aa571e1 100644
--- a/web-admin/src/features/embeds/init-embed-public-api.ts
+++ b/web-admin/src/features/embeds/init-embed-public-api.ts
@@ -33,6 +33,20 @@ type SetValidStateParams = {
failOnError?: boolean;
};
+type NavigateParams =
+ | {
+ type: "page";
+ name: "home" | "reports" | "alerts";
+ }
+ | {
+ type: "explore";
+ name: string;
+ }
+ | {
+ type: "canvas";
+ name: string;
+ };
+
export default function initEmbedPublicAPI(client: RuntimeClient): () => void {
const embedThemeStore = getEmbedThemeStoreInstance();
@@ -121,6 +135,23 @@ export default function initEmbedPublicAPI(client: RuntimeClient): () => void {
};
});
+ registerRPCMethod("navigate", async (params: NavigateParams) => {
+ // Do not block if navigate=false. This is meant for that specific use case.
+
+ switch (params.type) {
+ case "page": {
+ const page = params.name === "home" ? "" : "/" + params.name;
+ return goto(`/-/embed${page}`);
+ }
+
+ case "explore":
+ return goto(`/-/embed/explore/${params.name}`);
+
+ case "canvas":
+ return goto(`/-/embed/canvas/${params.name}`);
+ }
+ });
+
registerRPCMethod("getThemeMode", () => {
return { themeMode: get(themeControl.preference) };
});
diff --git a/web-admin/src/features/embeds/selectors.ts b/web-admin/src/features/embeds/selectors.ts
new file mode 100644
index 000000000000..e98226ec252b
--- /dev/null
+++ b/web-admin/src/features/embeds/selectors.ts
@@ -0,0 +1,12 @@
+import { RuntimeClient } from "@rilldata/web-common/runtime-client/v2";
+import { createRuntimeServiceGetInstance } from "@rilldata/web-common/runtime-client";
+import { derived } from "svelte/store";
+
+export function getOrgAndProjectForEmbed(runtimeClient: RuntimeClient) {
+ return derived(createRuntimeServiceGetInstance(runtimeClient, {}), (resp) => {
+ const urlParts = resp.data?.instance?.frontendUrl?.split("/") ?? [];
+ const organization = urlParts[urlParts.length - 2] ?? "";
+ const project = urlParts[urlParts.length - 1] ?? "";
+ return { organization, project };
+ });
+}
diff --git a/web-admin/src/features/projects/ProjectPage.svelte b/web-admin/src/features/projects/ProjectPage.svelte
index 30a6372d1738..3cc3d5a69b58 100644
--- a/web-admin/src/features/projects/ProjectPage.svelte
+++ b/web-admin/src/features/projects/ProjectPage.svelte
@@ -3,13 +3,19 @@
import ResourceError from "@rilldata/web-common/features/resources/ResourceError.svelte";
import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte";
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";
- import type { V1ListResourcesResponse } from "@rilldata/web-common/runtime-client";
+ import type {
+ V1ListResourcesResponse,
+ V1Resource,
+ } from "@rilldata/web-common/runtime-client";
import type { CreateQueryResult } from "@tanstack/svelte-query";
type ProjectPageKindParam = "report" | "dashboard" | "alert";
export let kind: ProjectPageKindParam;
- export let query: CreateQueryResult
;
+ export let query: CreateQueryResult<
+ V1ListResourcesResponse | V1Resource[],
+ Error
+ >;
$: ({ isLoading, isError, isSuccess, error } = $query);
diff --git a/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte b/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte
index 3d1bbc8201b1..e0ca45964c4a 100644
--- a/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte
+++ b/web-admin/src/features/scheduled-reports/metadata/ReportMetadata.svelte
@@ -183,6 +183,7 @@
{organization}
{project}
ownerId={reportSpec.annotations["admin_owner_user_id"]}
+ ownerEmail={reportSpec.annotations["admin_owner_user_email"]}
/>
diff --git a/web-admin/src/features/scheduled-reports/metadata/ReportOwnerBlock.svelte b/web-admin/src/features/scheduled-reports/metadata/ReportOwnerBlock.svelte
index 21150000e62a..5cbe1d564f95 100644
--- a/web-admin/src/features/scheduled-reports/metadata/ReportOwnerBlock.svelte
+++ b/web-admin/src/features/scheduled-reports/metadata/ReportOwnerBlock.svelte
@@ -4,15 +4,18 @@
export let organization: string;
export let project: string;
- export let ownerId: string;
+ export let ownerId: string | undefined = undefined;
+ export let ownerEmail: string | undefined = undefined;
- $: ownerName = useReportOwnerName(organization, project, ownerId);
+ $: ownerNameQuery = useReportOwnerName(organization, project, ownerId);
+ $: ownerName = $ownerNameQuery.data ?? ownerEmail;
+ $: isSuccess = ownerId ? $ownerNameQuery.isSuccess : true;
-{#if $ownerName.isSuccess}
+{#if isSuccess}
- {$ownerName.data
- ? m.report_meta_created_by({ name: $ownerName.data })
+ {ownerName
+ ? m.report_meta_created_by({ name: ownerName })
: m.report_meta_created_through_code()} •
{/if}
diff --git a/web-admin/src/features/scheduled-reports/selectors.ts b/web-admin/src/features/scheduled-reports/selectors.ts
index fc4183350d3e..e870d5ddbf76 100644
--- a/web-admin/src/features/scheduled-reports/selectors.ts
+++ b/web-admin/src/features/scheduled-reports/selectors.ts
@@ -1,7 +1,6 @@
import { createAdminServiceSearchProjectUsers } from "@rilldata/web-admin/client";
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
import { getDashboardNameFromReport } from "@rilldata/web-common/features/scheduled-reports/utils";
-import type { V1ReportSpec } from "@rilldata/web-common/runtime-client/gen/index.schemas";
import {
createRuntimeServiceGetResource,
createRuntimeServiceListResources,
@@ -40,9 +39,7 @@ export function useReportDashboardName(client: RuntimeClient, name: string) {
{
query: {
select: (data) =>
- getDashboardNameFromReport(
- data.resource?.report?.spec as V1ReportSpec,
- ),
+ getDashboardNameFromReport(data.resource?.report?.spec),
},
},
);
@@ -64,6 +61,7 @@ export function useReportOwnerName(
{
query: {
select: (data) => data.users.find((u) => u.id === ownerId)?.displayName,
+ enabled: !!ownerId,
},
},
);
@@ -78,7 +76,8 @@ export function useIsReportCreatedByCode(client: RuntimeClient, name: string) {
{
query: {
select: (data) =>
- !data.resource.report.spec.annotations["admin_owner_user_id"],
+ !data.resource.report.spec.annotations["admin_owner_user_id"] &&
+ !data.resource.report.spec.annotations["admin_owner_user_email"],
},
},
);
diff --git a/web-admin/src/features/scheduled-reports/unsubscribe-report-using-token.ts b/web-admin/src/features/scheduled-reports/unsubscribe-report-using-token.ts
deleted file mode 100644
index b970ffa4fa1a..000000000000
--- a/web-admin/src/features/scheduled-reports/unsubscribe-report-using-token.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * This file implements a variant of the `UnsubscribeReport` client code that authenticates using a bearer token.
- *
- * Modifications from the original Orval-generated code in `/web-admin/src/client/gen/admin-service/admin-service.ts` include:
- * - `queryFn`: Authentication via `Authorization: Bearer ${token}` header, replacing cookie-based authentication.
- * - `queryOptions`: Conditional enabling of the query based on the presence of `token`.
- */
-
-import {
- createMutation,
- type CreateMutationOptions,
- type MutationFunction,
-} from "@tanstack/svelte-query";
-import {
- type AdminServiceUnsubscribeAlertBodyBody,
- type RpcStatus,
- type V1UnsubscribeReportResponse,
-} from "@rilldata/web-admin/client";
-import httpClient from "@rilldata/web-admin/client/http-client";
-
-const adminServiceUnsubscribeReportWithToken = (
- organization: string,
- project: string,
- name: string,
- adminServiceUnsubscribeReportBody: AdminServiceUnsubscribeAlertBodyBody,
- token: string,
-) => {
- return httpClient({
- url: `/v1/orgs/${organization}/projects/${project}/reports/${name}/unsubscribe`,
- method: "post",
- data: adminServiceUnsubscribeReportBody,
- // We use the bearer token to authenticate the request
- headers: {
- Authorization: `Bearer ${token}`,
- },
- // To be explicit, we don't need to send credentials (cookies) with the request
- withCredentials: false,
- });
-};
-
-export const createAdminServiceUnsubscribeReportUsingToken = <
- TError = RpcStatus,
- TContext = unknown,
->(options?: {
- mutation?: CreateMutationOptions<
- Awaited>,
- TError,
- {
- organization: string;
- project: string;
- name: string;
- data: AdminServiceUnsubscribeAlertBodyBody;
- token: string;
- },
- TContext
- >;
-}) => {
- const { mutation: mutationOptions } = options ?? {};
-
- const mutationFn: MutationFunction<
- Awaited>,
- {
- organization: string;
- project: string;
- name: string;
- data: AdminServiceUnsubscribeAlertBodyBody;
- token: string;
- }
- > = (props) => {
- const { organization, project, name, token, data } = props ?? {};
-
- return adminServiceUnsubscribeReportWithToken(
- organization,
- project,
- name,
- data,
- token,
- );
- };
-
- return createMutation<
- Awaited>,
- TError,
- {
- organization: string;
- project: string;
- name: string;
- data: AdminServiceUnsubscribeAlertBodyBody;
- token: string;
- },
- TContext
- >({ mutationFn, ...mutationOptions });
-};
diff --git a/web-admin/src/routes/-/embed/alerts/+page.svelte b/web-admin/src/routes/-/embed/alerts/+page.svelte
new file mode 100644
index 000000000000..c9c0bcbe1b01
--- /dev/null
+++ b/web-admin/src/routes/-/embed/alerts/+page.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/web-admin/src/routes/-/embed/alerts/[alert]/+page.svelte b/web-admin/src/routes/-/embed/alerts/[alert]/+page.svelte
new file mode 100644
index 000000000000..faa25fd52075
--- /dev/null
+++ b/web-admin/src/routes/-/embed/alerts/[alert]/+page.svelte
@@ -0,0 +1,23 @@
+
+
+
+{#if organization && project}
+
+
+
+{/if}
diff --git a/web-admin/src/routes/-/embed/reports/+page.svelte b/web-admin/src/routes/-/embed/reports/+page.svelte
new file mode 100644
index 000000000000..73d8e9c2f428
--- /dev/null
+++ b/web-admin/src/routes/-/embed/reports/+page.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/web-admin/src/routes/-/embed/reports/[report]/+page.svelte b/web-admin/src/routes/-/embed/reports/[report]/+page.svelte
new file mode 100644
index 000000000000..45a2485d0e14
--- /dev/null
+++ b/web-admin/src/routes/-/embed/reports/[report]/+page.svelte
@@ -0,0 +1,25 @@
+
+
+
+{#if organization && project}
+
+
+
+{/if}
diff --git a/web-admin/src/routes/-/unsubscribe/reports/[report]/+page.svelte b/web-admin/src/routes/-/unsubscribe/reports/[report]/+page.svelte
index 853daa998c91..b8ef14239db2 100644
--- a/web-admin/src/routes/-/unsubscribe/reports/[report]/+page.svelte
+++ b/web-admin/src/routes/-/unsubscribe/reports/[report]/+page.svelte
@@ -4,7 +4,7 @@
type AdminServiceUnsubscribeAlertBodyBody,
type RpcStatus,
} from "@rilldata/web-admin/client";
- import { createAdminServiceUnsubscribeReportUsingToken } from "@rilldata/web-admin/features/scheduled-reports/unsubscribe-report-using-token.ts";
+ import { createAdminServiceUnsubscribeReportUsingToken } from "@rilldata/web-common/features/scheduled-reports/report-client-using-token.ts";
import CtaContentContainer from "@rilldata/web-common/components/calls-to-action/CTAContentContainer.svelte";
import CtaLayoutContainer from "@rilldata/web-common/components/calls-to-action/CTALayoutContainer.svelte";
import CtaMessage from "@rilldata/web-common/components/calls-to-action/CTAMessage.svelte";
@@ -20,14 +20,15 @@
$: organization = $page.url.searchParams.get("org") ?? "";
$: project = $page.url.searchParams.get("project") ?? "";
$: report = $page.params.report;
- $: token = $page.url.searchParams.get("token");
$: email = $page.url.searchParams.get("email");
$: slackUser = $page.url.searchParams.get("slack_user");
// using this instead of reportUnsubscriber to avoid a flicker before reportUnsubscriber is triggered
let loading = true;
- const reportUnsubscriber = createAdminServiceUnsubscribeReportUsingToken();
+ const reportUnsubscriber = createAdminServiceUnsubscribeReportUsingToken(
+ $page.url.searchParams.get("token") ?? "",
+ );
$: error =
($reportUnsubscriber.error as unknown as AxiosError)?.response
@@ -39,11 +40,10 @@
if (slackUser) data.slackUser = slackUser;
await $reportUnsubscriber.mutateAsync({
- organization,
+ org: organization,
project,
name: report,
data,
- token,
});
loading = false;
}
diff --git a/web-admin/src/routes/[organization]/[project]/-/alerts/[alert]/unsubscribe/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/alerts/[alert]/unsubscribe/+page.svelte
index 1106bea70592..bed2555d1521 100644
--- a/web-admin/src/routes/[organization]/[project]/-/alerts/[alert]/unsubscribe/+page.svelte
+++ b/web-admin/src/routes/[organization]/[project]/-/alerts/[alert]/unsubscribe/+page.svelte
@@ -4,7 +4,7 @@
type AdminServiceUnsubscribeAlertBodyBody,
type RpcStatus,
} from "@rilldata/web-admin/client";
- import { createAdminServiceUnsubscribeAlertUsingToken } from "@rilldata/web-admin/features/alerts/unsubscribe-alert-using-token.ts";
+ import { createAdminServiceUnsubscribeAlertUsingToken } from "@rilldata/web-common/features/alerts/alert-client-using-token";
import CtaContentContainer from "@rilldata/web-common/components/calls-to-action/CTAContentContainer.svelte";
import CtaLayoutContainer from "@rilldata/web-common/components/calls-to-action/CTALayoutContainer.svelte";
import CtaMessage from "@rilldata/web-common/components/calls-to-action/CTAMessage.svelte";
@@ -15,14 +15,15 @@
$: organization = $page.params.organization;
$: project = $page.params.project;
$: alert = $page.params.alert;
- $: token = $page.url.searchParams.get("token");
$: email = $page.url.searchParams.get("email");
$: slackUser = $page.url.searchParams.get("slack_user");
// using this instead of alertUnsubscriber to avoid a flicker before alertUnsubscriber is triggered
let loading = true;
- const alertUnsubscriber = createAdminServiceUnsubscribeAlertUsingToken();
+ const alertUnsubscriber = createAdminServiceUnsubscribeAlertUsingToken(
+ $page.url.searchParams.get("token") ?? "",
+ );
$: error =
($alertUnsubscriber.error as unknown as AxiosError)?.response
@@ -34,11 +35,10 @@
if (slackUser) data.slackUser = slackUser;
await $alertUnsubscriber.mutateAsync({
- organization,
+ org: organization,
project,
name: alert,
data,
- token,
});
loading = false;
}
diff --git a/web-admin/src/routes/[organization]/[project]/-/reports/[report]/unsubscribe/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/reports/[report]/unsubscribe/+page.svelte
index c5ea5940e9d8..af63a2ecab77 100644
--- a/web-admin/src/routes/[organization]/[project]/-/reports/[report]/unsubscribe/+page.svelte
+++ b/web-admin/src/routes/[organization]/[project]/-/reports/[report]/unsubscribe/+page.svelte
@@ -4,7 +4,7 @@
type AdminServiceUnsubscribeAlertBodyBody,
type RpcStatus,
} from "@rilldata/web-admin/client";
- import { createAdminServiceUnsubscribeReportUsingToken } from "@rilldata/web-admin/features/scheduled-reports/unsubscribe-report-using-token";
+ import { createAdminServiceUnsubscribeReportUsingToken } from "@rilldata/web-common/features/scheduled-reports/report-client-using-token.ts";
import CtaContentContainer from "@rilldata/web-common/components/calls-to-action/CTAContentContainer.svelte";
import CtaLayoutContainer from "@rilldata/web-common/components/calls-to-action/CTALayoutContainer.svelte";
import CtaMessage from "@rilldata/web-common/components/calls-to-action/CTAMessage.svelte";
@@ -15,14 +15,15 @@
$: organization = $page.params.organization;
$: project = $page.params.project;
$: report = $page.params.report;
- $: token = $page.url.searchParams.get("token");
$: email = $page.url.searchParams.get("email");
$: slackUser = $page.url.searchParams.get("slack_user");
// using this instead of reportUnsubscriber to avoid a flicker before reportUnsubscriber is triggered
let loading = true;
- const reportUnsubscriber = createAdminServiceUnsubscribeReportUsingToken();
+ const reportUnsubscriber = createAdminServiceUnsubscribeReportUsingToken(
+ $page.url.searchParams.get("token") ?? "",
+ );
$: error =
($reportUnsubscriber.error as unknown as AxiosError)?.response
@@ -34,11 +35,10 @@
if (slackUser) data.slackUser = slackUser;
await $reportUnsubscriber.mutateAsync({
- organization,
+ org: organization,
project,
name: report,
data,
- token,
});
loading = false;
}
diff --git a/web-common/src/components/icons/ReportIcon.svelte b/web-common/src/components/icons/ReportIcon.svelte
index ac023f28d4cf..78b586794e1d 100644
--- a/web-common/src/components/icons/ReportIcon.svelte
+++ b/web-common/src/components/icons/ReportIcon.svelte
@@ -11,9 +11,10 @@
class={className}
>
diff --git a/web-common/src/features/alerts/AlertForm.svelte b/web-common/src/features/alerts/AlertForm.svelte
index 9994f7572ad4..4263739ba3ec 100644
--- a/web-common/src/features/alerts/AlertForm.svelte
+++ b/web-common/src/features/alerts/AlertForm.svelte
@@ -14,10 +14,6 @@
-{#if !isLoading && initialValues}
-
+{#if initialValues}
+
{/if}
diff --git a/web-common/src/features/alerts/AlertFormMetadataProvider.ts b/web-common/src/features/alerts/AlertFormMetadataProvider.ts
new file mode 100644
index 000000000000..e7b629be3b19
--- /dev/null
+++ b/web-common/src/features/alerts/AlertFormMetadataProvider.ts
@@ -0,0 +1,34 @@
+import {
+ createAdminServiceCreateAlert,
+ createAdminServiceEditAlert,
+} from "@rilldata/web-admin/client";
+import type {
+ FormMetadataProvider,
+ MutationFactory,
+} from "@rilldata/web-common/features/scheduled-reports/FormMetadataProvider.svelte.ts";
+import {
+ createAdminServiceCreateAlertUsingToken,
+ createAdminServiceEditAlertUsingToken,
+} from "@rilldata/web-common/features/alerts/alert-client-using-token.ts";
+
+type AlertMutation = ReturnType<
+ typeof createAdminServiceCreateAlert | typeof createAdminServiceEditAlert
+>;
+
+export type AlertFormMetadataProvider = FormMetadataProvider;
+
+export function getAlertMutationFactory(
+ isEdit: boolean,
+): MutationFactory {
+ return isEdit
+ ? {
+ admin: () => createAdminServiceEditAlert(),
+ embed: (adminToken) =>
+ createAdminServiceEditAlertUsingToken(adminToken),
+ }
+ : {
+ admin: () => createAdminServiceCreateAlert(),
+ embed: (adminToken) =>
+ createAdminServiceCreateAlertUsingToken(adminToken),
+ };
+}
diff --git a/web-common/src/features/alerts/alert-client-using-token.ts b/web-common/src/features/alerts/alert-client-using-token.ts
new file mode 100644
index 000000000000..89e7e7c3387f
--- /dev/null
+++ b/web-common/src/features/alerts/alert-client-using-token.ts
@@ -0,0 +1,198 @@
+/**
+ * This file implements variants of the `CreateAlert`, `EditAlert` and `UnsubscribeAlert` client code that authenticate using a bearer token.
+ *
+ * Modifications from the original Orval-generated code in `/web-admin/src/client/gen/default/default.ts` include:
+ * - Request functions: Authentication via `Authorization: Bearer ${token}` header, replacing cookie-based authentication.
+ * - Mutation creators: The token is bound when creating the mutation, so the mutation variables match the generated mutations.
+ */
+
+import {
+ createMutation,
+ type CreateMutationOptions,
+ type CreateMutationResult,
+ type QueryClient,
+} from "@tanstack/svelte-query";
+import {
+ type AdminServiceCreateAlertBodyBody,
+ type AdminServiceUnsubscribeAlertBodyBody,
+ type RpcStatus,
+ type V1CreateAlertResponse,
+ type V1EditAlertResponse,
+ type V1UnsubscribeAlertResponse,
+} from "@rilldata/web-admin/client";
+import httpClient from "@rilldata/web-admin/client/http-client";
+
+export const adminServiceCreateAlertUsingToken = (
+ org: string,
+ project: string,
+ adminServiceCreateAlertBodyBody: AdminServiceCreateAlertBodyBody,
+ token: string,
+) => {
+ return httpClient({
+ url: `/v1/orgs/${org}/projects/${project}/alerts`,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ // We use the bearer token to authenticate the request
+ Authorization: `Bearer ${token}`,
+ },
+ data: adminServiceCreateAlertBodyBody,
+ // To be explicit, we don't need to send credentials (cookies) with the request
+ withCredentials: false,
+ });
+};
+
+export const createAdminServiceCreateAlertUsingToken = <
+ TError = RpcStatus,
+ TContext = unknown,
+>(
+ token: string,
+ options?: {
+ mutation?: CreateMutationOptions<
+ Awaited>,
+ TError,
+ { org: string; project: string; data: AdminServiceCreateAlertBodyBody },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): CreateMutationResult<
+ Awaited>,
+ TError,
+ { org: string; project: string; data: AdminServiceCreateAlertBodyBody },
+ TContext
+> => {
+ return createMutation(
+ {
+ mutationKey: ["adminServiceCreateAlert"],
+ mutationFn: ({ org, project, data }) =>
+ adminServiceCreateAlertUsingToken(org, project, data, token),
+ ...options?.mutation,
+ },
+ queryClient,
+ );
+};
+
+export const adminServiceEditAlertUsingToken = (
+ org: string,
+ project: string,
+ name: string,
+ adminServiceCreateAlertBodyBody: AdminServiceCreateAlertBodyBody,
+ token: string,
+) => {
+ return httpClient({
+ url: `/v1/orgs/${org}/projects/${project}/alerts/${name}`,
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ // We use the bearer token to authenticate the request
+ Authorization: `Bearer ${token}`,
+ },
+ data: adminServiceCreateAlertBodyBody,
+ // To be explicit, we don't need to send credentials (cookies) with the request
+ withCredentials: false,
+ });
+};
+
+export const createAdminServiceEditAlertUsingToken = <
+ TError = RpcStatus,
+ TContext = unknown,
+>(
+ token: string,
+ options?: {
+ mutation?: CreateMutationOptions<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceCreateAlertBodyBody;
+ },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): CreateMutationResult<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceCreateAlertBodyBody;
+ },
+ TContext
+> => {
+ return createMutation(
+ {
+ mutationKey: ["adminServiceEditAlert"],
+ mutationFn: ({ org, project, name, data }) =>
+ adminServiceEditAlertUsingToken(org, project, name, data, token),
+ ...options?.mutation,
+ },
+ queryClient,
+ );
+};
+
+export const adminServiceUnsubscribeAlertUsingToken = (
+ org: string,
+ project: string,
+ name: string,
+ adminServiceUnsubscribeAlertBodyBody: AdminServiceUnsubscribeAlertBodyBody,
+ token: string,
+) => {
+ return httpClient({
+ url: `/v1/orgs/${org}/projects/${project}/alerts/${name}/unsubscribe`,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ // We use the bearer token to authenticate the request
+ Authorization: `Bearer ${token}`,
+ },
+ data: adminServiceUnsubscribeAlertBodyBody,
+ // To be explicit, we don't need to send credentials (cookies) with the request
+ withCredentials: false,
+ });
+};
+
+export const createAdminServiceUnsubscribeAlertUsingToken = <
+ TError = RpcStatus,
+ TContext = unknown,
+>(
+ token: string,
+ options?: {
+ mutation?: CreateMutationOptions<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceUnsubscribeAlertBodyBody;
+ },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): CreateMutationResult<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceUnsubscribeAlertBodyBody;
+ },
+ TContext
+> => {
+ return createMutation(
+ {
+ mutationKey: ["adminServiceUnsubscribeAlert"],
+ mutationFn: ({ org, project, name, data }) =>
+ adminServiceUnsubscribeAlertUsingToken(org, project, name, data, token),
+ ...options?.mutation,
+ },
+ queryClient,
+ );
+};
diff --git a/web-common/src/features/alerts/create-alert-utils.ts b/web-common/src/features/alerts/create-alert-utils.ts
index 1a61a956c219..f00367a71bad 100644
--- a/web-common/src/features/alerts/create-alert-utils.ts
+++ b/web-common/src/features/alerts/create-alert-utils.ts
@@ -1,4 +1,3 @@
-import type { V1User } from "@rilldata/web-admin/client";
import { getSnoozeOptions } from "@rilldata/web-common/features/alerts/delivery-tab/snooze.ts";
import type { AlertFormValues } from "@rilldata/web-common/features/alerts/form-utils.ts";
import { getEmptyMeasureFilterEntry } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-entry.ts";
@@ -18,7 +17,7 @@ export function getNewAlertInitialFormValues(
metricsViewName: string,
exploreName: string,
exploreState: Partial,
- user: V1User | undefined,
+ userEmail: string,
): AlertFormValues {
// Use comparison dimension only when in TDD view (where it's visually relevant).
// Otherwise use the expanded dimension table dimension.
@@ -52,9 +51,9 @@ export function getNewAlertInitialFormValues(
...getInitialScheduleFormValues(),
enableSlackNotification: false,
slackChannels: [""],
- slackUsers: [user?.email ?? "", ""],
+ slackUsers: [userEmail, ""],
enableEmailNotification: true,
- emailRecipients: [user?.email ?? "", ""],
+ emailRecipients: [userEmail, ""],
metricsViewName,
exploreName: exploreName,
diff --git a/web-common/src/features/alerts/delivery-tab/AlertDialogDeliveryTab.svelte b/web-common/src/features/alerts/delivery-tab/AlertDialogDeliveryTab.svelte
index b1349636901b..d672680c0ca0 100644
--- a/web-common/src/features/alerts/delivery-tab/AlertDialogDeliveryTab.svelte
+++ b/web-common/src/features/alerts/delivery-tab/AlertDialogDeliveryTab.svelte
@@ -5,26 +5,35 @@
import Input from "@rilldata/web-common/components/forms/Input.svelte";
import MultiInput from "@rilldata/web-common/components/forms/MultiInput.svelte";
import Select from "@rilldata/web-common/components/forms/Select.svelte";
- import { getHasSlackConnection } from "@rilldata/web-common/features/alerts/delivery-tab/notifiers-utils";
import { getSnoozeOptions } from "@rilldata/web-common/features/alerts/delivery-tab/snooze";
import type { AlertFormValues } from "@rilldata/web-common/features/alerts/form-utils";
import ScheduleForm from "@rilldata/web-common/features/scheduled-reports/ScheduleForm.svelte";
import { useExploreValidSpec } from "@rilldata/web-common/features/explores/selectors";
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
import type { SuperForm } from "sveltekit-superforms/client";
+ import type { AlertFormMetadataProvider } from "@rilldata/web-common/features/alerts/AlertFormMetadataProvider.ts";
- export let superFormInstance: SuperForm;
- export let exploreName: string;
+ let {
+ superFormInstance,
+ exploreName,
+ provider,
+ }: {
+ superFormInstance: SuperForm;
+ exploreName: string;
+ provider: AlertFormMetadataProvider;
+ } = $props();
const runtimeClient = useRuntimeClient();
- $: ({ form, errors } = superFormInstance);
-
- $: hasSlackNotifier = getHasSlackConnection(runtimeClient);
+ const { form, errors } = $derived(superFormInstance);
// Time zone options from the dashboard's spec
- $: exploreSpecQuery = useExploreValidSpec(runtimeClient, exploreName);
- $: availableTimeZones = $exploreSpecQuery.data?.explore?.timeZones;
+ const exploreSpecQuery = $derived(
+ useExploreValidSpec(runtimeClient, exploreName),
+ );
+ const availableTimeZones = $derived(
+ $exploreSpecQuery.data?.explore?.timeZones,
+ );
@@ -70,7 +79,7 @@
options={getSnoozeOptions()}
/>
- {#if $hasSlackNotifier.data}
+ {#if provider.hasSlackNotifier}
!!data.connectors?.some((c) => c.name === "slack"),
- },
- },
- );
-}
diff --git a/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte b/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte
index f8f24f80d18a..25221e8fc699 100644
--- a/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte
+++ b/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte
@@ -17,6 +17,7 @@
import SelectAllButton from "../toolbars/SelectAllButton.svelte";
import StartPivotButton from "../toolbars/StartPivotButton.svelte";
import { getDimensionTableExportQuery } from "./dimension-table-export";
+ import { EmbedStore } from "@rilldata/web-common/features/embeds/embed-store.ts";
let {
dimensionName,
@@ -52,7 +53,11 @@
expressionFilterManager,
} = stateManagers;
- const { adminServer, exports } = featureFlags;
+ const { reports, exports } = featureFlags;
+ let enableReports = $derived(
+ $reports &&
+ (!EmbedStore.getInstance() || !!EmbedStore.getInstance()?.userEmail),
+ );
let exploreHasTimeDimension = $derived(!!$timeRangeSummaryStore.data);
@@ -167,7 +172,7 @@
{#if $exports}
getDimensionTableExportQuery(stateManagers, isScheduled)}
exploreName={$exploreName}
diff --git a/web-common/src/features/dashboards/pivot/PivotDisplay.svelte b/web-common/src/features/dashboards/pivot/PivotDisplay.svelte
index 8602d5fd8ab7..b3bf54790bc0 100644
--- a/web-common/src/features/dashboards/pivot/PivotDisplay.svelte
+++ b/web-common/src/features/dashboards/pivot/PivotDisplay.svelte
@@ -27,6 +27,7 @@
import PivotSidebar from "./PivotSidebar.svelte";
import PivotTable from "./PivotTable.svelte";
import PivotToolbar from "./PivotToolbar.svelte";
+ import { EmbedStore } from "@rilldata/web-common/features/embeds/embed-store.ts";
export let isEmbedded: boolean = false;
@@ -42,7 +43,10 @@
timeRangeSummaryStore,
} = stateManagers;
- const { adminServer, exports } = featureFlags;
+ const { reports, exports } = featureFlags;
+ $: enableReports =
+ $reports &&
+ (!EmbedStore.getInstance() || !!EmbedStore.getInstance()?.userEmail);
const timeControlsStore = useTimeControlStore(stateManagers);
$: timeControlsForPillActions = {
@@ -229,7 +233,7 @@
{#if $exports}
getPivotExportQuery(stateManagers, isScheduled)}
exploreName={$exploreName}
diff --git a/web-common/src/features/dashboards/selectors.ts b/web-common/src/features/dashboards/selectors.ts
index fc12af9dcd59..d6dac853d609 100644
--- a/web-common/src/features/dashboards/selectors.ts
+++ b/web-common/src/features/dashboards/selectors.ts
@@ -106,13 +106,20 @@ export const useMetricsViewValidSpec = (
client: RuntimeClient,
metricsViewName: string,
selector?: (meta: V1MetricsViewSpec) => T,
+ queryClient?: QueryClient,
) => {
- return useResource(client, metricsViewName, ResourceKind.MetricsView, {
- select: (data) =>
- selector
- ? selector(data.resource?.metricsView?.state?.validSpec ?? {})
- : (data.resource?.metricsView?.state?.validSpec as T),
- });
+ return useResource(
+ client,
+ metricsViewName,
+ ResourceKind.MetricsView,
+ {
+ select: (data) =>
+ selector
+ ? selector(data.resource?.metricsView?.state?.validSpec ?? {})
+ : (data.resource?.metricsView?.state?.validSpec as T),
+ },
+ queryClient,
+ );
};
export function useMetricsViewTimeRange(
@@ -126,7 +133,7 @@ export function useMetricsViewTimeRange(
const { query: queryOptions } = options ?? {};
const fullTimeRangeQueryOptionsStore = derived(
- useMetricsViewValidSpec(client, metricsViewName),
+ useMetricsViewValidSpec(client, metricsViewName, undefined, queryClient),
(validSpecResp) => {
const metricsViewSpec = validSpecResp.data ?? {};
diff --git a/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte b/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte
index 71531e532ef2..e518e81a2f1e 100644
--- a/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte
+++ b/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte
@@ -30,6 +30,7 @@
import { getTDDExportQuery } from "./tdd-export";
import type { TDDComparison } from "./types";
import { V1TimeGrainToDateTimeUnit } from "@rilldata/web-common/lib/time/new-grains";
+ import { EmbedStore } from "@rilldata/web-common/features/embeds/embed-store.ts";
interface Props {
exploreName: string;
@@ -55,7 +56,12 @@
hideStartPivotButton = false,
}: Props = $props();
- const { adminServer, exports } = featureFlags;
+ const { reports, exports } = featureFlags;
+ let enableReports = $derived(
+ $reports &&
+ (!EmbedStore.getInstance() || !!EmbedStore.getInstance()?.userEmail),
+ );
+
const stateManagers = getStateManagers();
const {
@@ -283,7 +289,7 @@
{#if $exports}
getTDDExportQuery(stateManagers, isScheduled)}
{exploreName}
diff --git a/web-common/src/features/embeds/embed-store.ts b/web-common/src/features/embeds/embed-store.ts
index bab5f7f97962..51db913da6c7 100644
--- a/web-common/src/features/embeds/embed-store.ts
+++ b/web-common/src/features/embeds/embed-store.ts
@@ -9,6 +9,8 @@ export class EmbedStore {
public readonly instanceId: string;
public readonly runtimeHost: string;
public readonly accessToken: string;
+ public readonly adminToken: string;
+ public readonly userEmail: string;
/**
* Array of missing required params.
* instance_id, runtime_host and access_token are required params.
@@ -52,6 +54,8 @@ export class EmbedStore {
this.instanceId = url.searchParams.get("instance_id") ?? "";
this.runtimeHost = url.searchParams.get("runtime_host") ?? "";
this.accessToken = url.searchParams.get("access_token") ?? "";
+ this.adminToken = url.searchParams.get("admin_token") ?? "";
+ this.userEmail = url.searchParams.get("user_email") ?? "";
this.navigationEnabled = url.searchParams.get("navigation") === "true";
this.navigationBarEnabled =
this.navigationEnabled &&
diff --git a/web-common/src/features/exports/ExportMenu.svelte b/web-common/src/features/exports/ExportMenu.svelte
index e3dcd52499e8..2e0484a5e29a 100644
--- a/web-common/src/features/exports/ExportMenu.svelte
+++ b/web-common/src/features/exports/ExportMenu.svelte
@@ -41,7 +41,7 @@
}
const exportDash = createQueryServiceExportMutation(runtimeClient);
- const { reports, adminServer, exportHeader } = featureFlags;
+ const { adminServer, exportHeader } = featureFlags;
async function handleExport(options: {
format: V1ExportFormat;
@@ -151,7 +151,7 @@
Export as XLSX with metadata
{/if}
- {#if includeScheduledReport && $reports && exploreName}
+ {#if includeScheduledReport && exploreName}
(showScheduledReportDialog = true)}
disabled={!scheduledReportQuery}
diff --git a/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte b/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte
index 5e992674d8e1..51098b522884 100644
--- a/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte
+++ b/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte
@@ -4,7 +4,6 @@
import InputLabel from "@rilldata/web-common/components/forms/InputLabel.svelte";
import MultiInput from "@rilldata/web-common/components/forms/MultiInput.svelte";
import FormSection from "@rilldata/web-common/components/forms/FormSection.svelte";
- import { getHasSlackConnection } from "@rilldata/web-common/features/alerts/delivery-tab/notifiers-utils";
import type { TimeControls } from "@rilldata/web-common/features/dashboards/stores/TimeControls.ts";
import FiltersForm from "@rilldata/web-common/features/scheduled-reports/FiltersForm.svelte";
import RowsAndColumnsForm from "@rilldata/web-common/features/scheduled-reports/fields/RowsAndColumnsForm.svelte";
@@ -18,7 +17,7 @@
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
import InfoCircle from "@rilldata/web-common/components/icons/InfoCircle.svelte";
import type { Readable } from "svelte/store";
- import type { SuperFormErrors } from "sveltekit-superforms/client";
+ import type { SuperForm, SuperFormErrors } from "sveltekit-superforms/client";
import Input from "../../components/forms/Input.svelte";
import Select from "../../components/forms/Select.svelte";
import Checkbox from "../../components/forms/Checkbox.svelte";
@@ -35,21 +34,40 @@
import { specHasTabGroups } from "@rilldata/web-common/features/canvas/stores/tab-group";
import type { EphemeralMeasureDef } from "@rilldata/web-common/features/dashboards/ephemeral-measures/types.ts";
import type { V1Resource } from "@rilldata/web-common/runtime-client";
+ import { EmbedStore } from "@rilldata/web-common/features/embeds/embed-store.ts";
+ import type { ReportFormMetadataProvider } from "@rilldata/web-common/features/scheduled-reports/ReportFormMetadataProvider.ts";
- export let formId: string;
- export let data: Readable;
- export let errors: SuperFormErrors;
- export let submit: () => void;
- export let enhance;
- export let metricsViewName: string;
- export let exploreName: string;
- export let canvasName: string = "";
- // Canvas state (URL search string) to display instead of the page URL; set when
- // editing a report so the filter bar shows the report's captured state.
- export let canvasStateOverride: string | undefined = undefined;
- export let filters: ExpressionFilterManager | undefined = undefined;
- export let timeControls: TimeControls | undefined = undefined;
- export let ephemeralMeasures: EphemeralMeasureDef[] | undefined = undefined;
+ let {
+ formId,
+ data,
+ errors,
+ submit,
+ enhance,
+ metricsViewName,
+ exploreName,
+ canvasName = "",
+ canvasStateOverride,
+ filters,
+ timeControls,
+ ephemeralMeasures,
+ provider,
+ }: {
+ formId: string;
+ data: Readable;
+ errors: SuperFormErrors;
+ submit: () => void;
+ enhance: SuperForm["enhance"];
+ metricsViewName: string;
+ exploreName: string;
+ canvasName?: string;
+ // Canvas state (URL search string) to display instead of the page URL; set when
+ // editing a report so the filter bar shows the report's captured state.
+ canvasStateOverride?: string;
+ filters?: ExpressionFilterManager;
+ timeControls?: TimeControls;
+ ephemeralMeasures?: EphemeralMeasureDef[];
+ provider: ReportFormMetadataProvider;
+ } = $props();
const RUN_AS_OPTIONS = [
{
@@ -64,30 +82,33 @@
},
];
const runtimeClient = useRuntimeClient();
+ // Reports created from an embed always run as the creator.
+ const isEmbedded = EmbedStore.isEmbedded();
- $: selectedRunAsOption = RUN_AS_OPTIONS.find(
- (o) => o.value === $data["webOpenMode"],
+ const selectedRunAsOption = $derived(
+ RUN_AS_OPTIONS.find((o) => o.value === $data["webOpenMode"]),
);
- $: hasSlackNotifier = getHasSlackConnection(runtimeClient);
-
// Pull the time zone options from the dashboard's spec
- $: exploreSpecQuery = useExploreValidSpec(runtimeClient, exploreName);
- $: canvasQuery = useResource(
- runtimeClient,
- canvasName,
- ResourceKind.Canvas,
+ const exploreSpecQuery = $derived(
+ useExploreValidSpec(runtimeClient, exploreName),
+ );
+ const canvasQuery = $derived(
+ useResource(runtimeClient, canvasName, ResourceKind.Canvas),
+ );
+ const availableTimeZones = $derived(
+ canvasName
+ ? $canvasQuery.data?.canvas?.state?.validSpec?.timeZones
+ : $exploreSpecQuery.data?.explore?.timeZones,
);
- $: availableTimeZones = canvasName
- ? $canvasQuery.data?.canvas?.state?.validSpec?.timeZones
- : $exploreSpecQuery.data?.explore?.timeZones;
// Gates the all-tabs option in the canvas PDF variant.
- $: showTabOptions = specHasTabGroups(
- $canvasQuery.data?.canvas?.state?.validSpec?.rows,
+ const showTabOptions = $derived(
+ specHasTabGroups($canvasQuery.data?.canvas?.state?.validSpec?.rows),
+ );
+ const canvasFiltersEnabled = $derived(
+ $canvasQuery.data?.canvas?.state?.validSpec?.filtersEnabled ?? true,
);
- $: canvasFiltersEnabled =
- $canvasQuery.data?.canvas?.state?.validSpec?.filtersEnabled ?? true;
// Keyboard counterpart of the read-only filter bar's pointer-events guard:
// its controls remain focusable, so kick focus back out to keep them inoperable.
@@ -115,17 +136,20 @@
label={m.report_form_title_label()}
placeholder={m.report_form_title_placeholder()}
/>
-
- {#if selectedRunAsOption}
-
- {selectedRunAsOption.description}
-
+
+ {#if !isEmbedded}
+
+ {#if selectedRunAsOption}
+
+ {selectedRunAsOption.description}
+
+ {/if}
{/if}
{#if canvasName}
@@ -281,7 +305,7 @@
plural="emails"
placeholder={m.report_form_email_placeholder()}
/>
- {#if $hasSlackNotifier.data}
+ {#if provider.hasSlackNotifier}
= {
+ admin: () => M;
+ embed: (adminToken: string) => M;
+};
+
+/**
+ * Provides the metadata needed by alert and report forms,
+ * abstracting away whether the form is rendered in Rill Cloud or in an embed.
+ */
+export interface FormMetadataProvider {
+ mutation: M;
+
+ organization: string;
+ project: string;
+ pageBasePath: string;
+
+ userEmail: string;
+ // True until the current user has loaded.
+ isLoading: boolean;
+ projectMembersSet: Set;
+ hasSlackNotifier: boolean;
+
+ cleanup(): void;
+}
+
+export function createFormMetadataProvider(
+ client: RuntimeClient,
+ mutationFactory: MutationFactory,
+): FormMetadataProvider {
+ return EmbedStore.isEmbedded()
+ ? new EmbedFormMetadataProvider(client, mutationFactory.embed)
+ : new AdminFormMetadataProvider(client, mutationFactory.admin);
+}
+
+class AdminFormMetadataProvider implements FormMetadataProvider {
+ public mutation: M;
+
+ public organization: string;
+ public project: string;
+ public pageBasePath: string;
+
+ public userEmail = $state("");
+ public isLoading = $state(true);
+ public projectMembersSet = $state>(new Set());
+ public hasSlackNotifier = $state(false);
+
+ private readonly userUnsub: () => void;
+ private readonly projectMembersUnsub: () => void;
+ private readonly notifiersUnsub: () => void;
+
+ public constructor(
+ client: RuntimeClient,
+ createMutation: MutationFactory["admin"],
+ ) {
+ this.mutation = createMutation();
+
+ this.organization = page.params.organization;
+ this.project = page.params.project;
+ this.pageBasePath = `/${this.organization}/${this.project}/-`;
+
+ const user = createAdminServiceGetCurrentUser();
+ this.userUnsub = user.subscribe((userResp) => {
+ this.isLoading = userResp.isPending;
+ if (!userResp.data?.user?.email) return;
+ this.userEmail = userResp.data.user.email;
+ });
+
+ const listProjectMemberUsersQuery =
+ createAdminServiceListProjectMemberUsers(this.organization, this.project);
+ this.projectMembersUnsub = listProjectMemberUsersQuery.subscribe(
+ (projectMembersResp) => {
+ this.projectMembersSet = new Set(
+ projectMembersResp.data?.members?.map((m) => m.userEmail!) ?? [],
+ );
+ },
+ );
+
+ const notifierConnectorsQuery = createRuntimeServiceListNotifierConnectors(
+ client,
+ {},
+ );
+ this.notifiersUnsub = notifierConnectorsQuery.subscribe(
+ (notifierConnectorsResp) => {
+ this.hasSlackNotifier =
+ !!notifierConnectorsResp?.data?.connectors?.some(
+ (c) => c.name === "slack",
+ );
+ },
+ );
+ }
+
+ public cleanup() {
+ this.userUnsub();
+ this.projectMembersUnsub();
+ this.notifiersUnsub();
+ }
+}
+
+class EmbedFormMetadataProvider implements FormMetadataProvider {
+ public mutation: M;
+
+ public organization = "";
+ public project = "";
+ public pageBasePath = "/-/embed";
+ public userEmail = "";
+ public isLoading = false;
+ public projectMembersSet = new Set();
+ public hasSlackNotifier = false; // not support in embed
+
+ private readonly instanceUnsub: () => void;
+
+ public constructor(
+ client: RuntimeClient,
+ createMutation: MutationFactory["embed"],
+ ) {
+ const embedInstance = EmbedStore.getInstance();
+ if (!embedInstance) {
+ throw new Error("missing embed instance");
+ }
+ this.userEmail = embedInstance.userEmail;
+ this.projectMembersSet = new Set([this.userEmail]);
+
+ this.mutation = createMutation(embedInstance.adminToken);
+
+ const instanceQuery = createRuntimeServiceGetInstance(client, {});
+ this.instanceUnsub = instanceQuery.subscribe((instanceResp) => {
+ const url = instanceResp.data?.instance?.frontendUrl;
+ if (!url) return;
+ const parts = url.split("/");
+ this.organization = parts[parts.length - 2] ?? "";
+ this.project = parts[parts.length - 1] ?? "";
+ });
+ }
+
+ public cleanup() {
+ this.instanceUnsub();
+ }
+}
diff --git a/web-common/src/features/scheduled-reports/ReportFormMetadataProvider.ts b/web-common/src/features/scheduled-reports/ReportFormMetadataProvider.ts
new file mode 100644
index 000000000000..2a25a8480f5c
--- /dev/null
+++ b/web-common/src/features/scheduled-reports/ReportFormMetadataProvider.ts
@@ -0,0 +1,34 @@
+import {
+ createAdminServiceCreateReport,
+ createAdminServiceEditReport,
+} from "@rilldata/web-admin/client";
+import type {
+ FormMetadataProvider,
+ MutationFactory,
+} from "@rilldata/web-common/features/scheduled-reports/FormMetadataProvider.svelte.ts";
+import {
+ createAdminServiceCreateReportUsingToken,
+ createAdminServiceEditReportUsingToken,
+} from "@rilldata/web-common/features/scheduled-reports/report-client-using-token.ts";
+
+type ReportMutation = ReturnType<
+ typeof createAdminServiceCreateReport | typeof createAdminServiceEditReport
+>;
+
+export type ReportFormMetadataProvider = FormMetadataProvider;
+
+export function getReportMutationFactory(
+ isEdit: boolean,
+): MutationFactory {
+ return isEdit
+ ? {
+ admin: () => createAdminServiceEditReport(),
+ embed: (adminToken) =>
+ createAdminServiceEditReportUsingToken(adminToken),
+ }
+ : {
+ admin: () => createAdminServiceCreateReport(),
+ embed: (adminToken) =>
+ createAdminServiceCreateReportUsingToken(adminToken),
+ };
+}
diff --git a/web-common/src/features/scheduled-reports/ScheduledReportDialog.svelte b/web-common/src/features/scheduled-reports/ScheduledReportDialog.svelte
index 019a93b9004d..9a25e931c2ec 100644
--- a/web-common/src/features/scheduled-reports/ScheduledReportDialog.svelte
+++ b/web-common/src/features/scheduled-reports/ScheduledReportDialog.svelte
@@ -1,4 +1,4 @@
-
-
+
{m.report_form_schedule()}
{#if generalErrors}
diff --git a/web-common/src/features/scheduled-reports/report-client-using-token.ts b/web-common/src/features/scheduled-reports/report-client-using-token.ts
new file mode 100644
index 000000000000..6fbb47fe5786
--- /dev/null
+++ b/web-common/src/features/scheduled-reports/report-client-using-token.ts
@@ -0,0 +1,204 @@
+/**
+ * This file implements variants of the `CreateReport`, `EditReport` and `UnsubscribeReport` client code that authenticate using a bearer token.
+ *
+ * Modifications from the original Orval-generated code in `/web-admin/src/client/gen/default/default.ts` include:
+ * - Request functions: Authentication via `Authorization: Bearer ${token}` header, replacing cookie-based authentication.
+ * - Mutation creators: The token is bound when creating the mutation, so the mutation variables match the generated mutations.
+ */
+
+import {
+ createMutation,
+ type CreateMutationOptions,
+ type CreateMutationResult,
+ type QueryClient,
+} from "@tanstack/svelte-query";
+import {
+ type AdminServiceCreateReportBodyBody,
+ type AdminServiceUnsubscribeAlertBodyBody,
+ type RpcStatus,
+ type V1CreateReportResponse,
+ type V1EditReportResponse,
+ type V1UnsubscribeReportResponse,
+} from "@rilldata/web-admin/client";
+import httpClient from "@rilldata/web-admin/client/http-client.ts";
+
+export const adminServiceCreateReportUsingToken = (
+ org: string,
+ project: string,
+ adminServiceCreateReportBodyBody: AdminServiceCreateReportBodyBody,
+ token: string,
+) => {
+ return httpClient({
+ url: `/v1/orgs/${org}/projects/${project}/reports`,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ // We use the bearer token to authenticate the request
+ Authorization: `Bearer ${token}`,
+ },
+ data: adminServiceCreateReportBodyBody,
+ // To be explicit, we don't need to send credentials (cookies) with the request
+ withCredentials: false,
+ });
+};
+
+export const createAdminServiceCreateReportUsingToken = <
+ TError = RpcStatus,
+ TContext = unknown,
+>(
+ token: string,
+ options?: {
+ mutation?: CreateMutationOptions<
+ Awaited>,
+ TError,
+ { org: string; project: string; data: AdminServiceCreateReportBodyBody },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): CreateMutationResult<
+ Awaited>,
+ TError,
+ { org: string; project: string; data: AdminServiceCreateReportBodyBody },
+ TContext
+> => {
+ return createMutation(
+ {
+ mutationKey: ["adminServiceCreateReport"],
+ mutationFn: ({ org, project, data }) =>
+ adminServiceCreateReportUsingToken(org, project, data, token),
+ ...options?.mutation,
+ },
+ queryClient,
+ );
+};
+
+export const adminServiceEditReportUsingToken = (
+ org: string,
+ project: string,
+ name: string,
+ adminServiceCreateReportBodyBody: AdminServiceCreateReportBodyBody,
+ token: string,
+) => {
+ return httpClient({
+ url: `/v1/orgs/${org}/projects/${project}/reports/${name}`,
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ // We use the bearer token to authenticate the request
+ Authorization: `Bearer ${token}`,
+ },
+ data: adminServiceCreateReportBodyBody,
+ // To be explicit, we don't need to send credentials (cookies) with the request
+ withCredentials: false,
+ });
+};
+
+export const createAdminServiceEditReportUsingToken = <
+ TError = RpcStatus,
+ TContext = unknown,
+>(
+ token: string,
+ options?: {
+ mutation?: CreateMutationOptions<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceCreateReportBodyBody;
+ },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): CreateMutationResult<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceCreateReportBodyBody;
+ },
+ TContext
+> => {
+ return createMutation(
+ {
+ mutationKey: ["adminServiceEditReport"],
+ mutationFn: ({ org, project, name, data }) =>
+ adminServiceEditReportUsingToken(org, project, name, data, token),
+ ...options?.mutation,
+ },
+ queryClient,
+ );
+};
+
+export const adminServiceUnsubscribeReportUsingToken = (
+ org: string,
+ project: string,
+ name: string,
+ adminServiceUnsubscribeAlertBodyBody: AdminServiceUnsubscribeAlertBodyBody,
+ token: string,
+) => {
+ return httpClient({
+ url: `/v1/orgs/${org}/projects/${project}/reports/${name}/unsubscribe`,
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ // We use the bearer token to authenticate the request
+ Authorization: `Bearer ${token}`,
+ },
+ data: adminServiceUnsubscribeAlertBodyBody,
+ // To be explicit, we don't need to send credentials (cookies) with the request
+ withCredentials: false,
+ });
+};
+
+export const createAdminServiceUnsubscribeReportUsingToken = <
+ TError = RpcStatus,
+ TContext = unknown,
+>(
+ token: string,
+ options?: {
+ mutation?: CreateMutationOptions<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceUnsubscribeAlertBodyBody;
+ },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): CreateMutationResult<
+ Awaited>,
+ TError,
+ {
+ org: string;
+ project: string;
+ name: string;
+ data: AdminServiceUnsubscribeAlertBodyBody;
+ },
+ TContext
+> => {
+ return createMutation(
+ {
+ mutationKey: ["adminServiceUnsubscribeReport"],
+ mutationFn: ({ org, project, name, data }) =>
+ adminServiceUnsubscribeReportUsingToken(
+ org,
+ project,
+ name,
+ data,
+ token,
+ ),
+ ...options?.mutation,
+ },
+ queryClient,
+ );
+};
diff --git a/web-common/src/proto/gen/rill/admin/v1/api_pb.ts b/web-common/src/proto/gen/rill/admin/v1/api_pb.ts
index 7f2b0333b596..bd67c9352ed9 100644
--- a/web-common/src/proto/gen/rill/admin/v1/api_pb.ts
+++ b/web-common/src/proto/gen/rill/admin/v1/api_pb.ts
@@ -13394,6 +13394,29 @@ export class GetReportMetaRequest extends Message {
*/
webOpenMode = "";
+ /**
+ * @generated from oneof rill.admin.v1.GetReportMetaRequest.query_for
+ */
+ queryFor: {
+ /**
+ * @generated from field: string query_for_user_id = 13;
+ */
+ value: string;
+ case: "queryForUserId";
+ } | {
+ /**
+ * @generated from field: string query_for_user_email = 14;
+ */
+ value: string;
+ case: "queryForUserEmail";
+ } | {
+ /**
+ * @generated from field: google.protobuf.Struct query_for_attributes = 15;
+ */
+ value: Struct;
+ case: "queryForAttributes";
+ } | { case: undefined; value?: undefined } = { case: undefined };
+
/**
* @generated from field: string where_filter_json = 11 [deprecated = true];
* @deprecated
@@ -13422,6 +13445,9 @@ export class GetReportMetaRequest extends Message {
{ no: 8, name: "anon_recipients", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
{ no: 9, name: "resources", kind: "message", T: ResourceName, repeated: true },
{ no: 10, name: "web_open_mode", kind: "scalar", T: 9 /* ScalarType.STRING */ },
+ { no: 13, name: "query_for_user_id", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "query_for" },
+ { no: 14, name: "query_for_user_email", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "query_for" },
+ { no: 15, name: "query_for_attributes", kind: "message", T: Struct, oneof: "query_for" },
{ no: 11, name: "where_filter_json", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 12, name: "accessible_fields", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
]);
diff --git a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts
index 275218d28417..171f7f6ac559 100644
--- a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts
+++ b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts
@@ -3743,6 +3743,29 @@ export class ReportSpec extends Message {
*/
resolverProperties?: Struct;
+ /**
+ * @generated from oneof rill.runtime.v1.ReportSpec.query_for
+ */
+ queryFor: {
+ /**
+ * @generated from field: string query_for_user_id = 19;
+ */
+ value: string;
+ case: "queryForUserId";
+ } | {
+ /**
+ * @generated from field: string query_for_user_email = 20;
+ */
+ value: string;
+ case: "queryForUserEmail";
+ } | {
+ /**
+ * @generated from field: google.protobuf.Struct query_for_attributes = 21;
+ */
+ value: Struct;
+ case: "queryForAttributes";
+ } | { case: undefined; value?: undefined } = { case: undefined };
+
/**
* Legacy query-based report fields (deprecated - use resolver/resolver_properties instead)
*
@@ -3816,6 +3839,9 @@ export class ReportSpec extends Message {
{ no: 4, name: "timeout_seconds", kind: "scalar", T: 13 /* ScalarType.UINT32 */ },
{ no: 17, name: "resolver", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 18, name: "resolver_properties", kind: "message", T: Struct },
+ { no: 19, name: "query_for_user_id", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "query_for" },
+ { no: 20, name: "query_for_user_email", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "query_for" },
+ { no: 21, name: "query_for_attributes", kind: "message", T: Struct, oneof: "query_for" },
{ no: 5, name: "query_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 6, name: "query_args_json", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 7, name: "export_limit", kind: "scalar", T: 4 /* ScalarType.UINT64 */ },