From 01dd092fb1c60a2e45e1686a1b46bd17f7f526dc Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 30 Mar 2026 14:23:30 +0300 Subject: [PATCH 1/7] refactor: convert project layout to Svelte 5 runes and extract shared utilities - Migrate `+layout.svelte` from Svelte 4 to Svelte 5 runes (`$derived`, `$effect`, `$props`) - Extract `baseGetProjectQueryOptions` polling config to `project-query-options.ts` - Extract `resolveRuntimeConnection` to `project-runtime.ts` with unit tests - Remove duplicate mock query chain from `ProjectHeader` (layout already passes mock-aware permissions) - Add section comments and component-level doc block to layout --- .../features/projects/ProjectHeader.svelte | 46 +-- .../projects/project-query-options.ts | 35 ++ .../features/projects/project-runtime.spec.ts | 143 ++++++++ .../src/features/projects/project-runtime.ts | 49 +++ .../[organization]/[project]/+layout.svelte | 324 ++++++++---------- 5 files changed, 376 insertions(+), 221 deletions(-) create mode 100644 web-admin/src/features/projects/project-query-options.ts create mode 100644 web-admin/src/features/projects/project-runtime.spec.ts create mode 100644 web-admin/src/features/projects/project-runtime.ts diff --git a/web-admin/src/features/projects/ProjectHeader.svelte b/web-admin/src/features/projects/ProjectHeader.svelte index b8cefac25b2..5723e4e733c 100644 --- a/web-admin/src/features/projects/ProjectHeader.svelte +++ b/web-admin/src/features/projects/ProjectHeader.svelte @@ -4,7 +4,6 @@ import ExploreBookmarks from "@rilldata/web-admin/features/bookmarks/ExploreBookmarks.svelte"; import ShareDashboardPopover from "@rilldata/web-admin/features/dashboards/share/ShareDashboardPopover.svelte"; import ShareProjectPopover from "@rilldata/web-admin/features/projects/user-management/ShareProjectPopover.svelte"; - import { createAdminServiceGetProjectWithBearerToken } from "@rilldata/web-admin/features/public-urls/get-project-with-bearer-token"; import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte"; import type { PathOption } from "@rilldata/web-common/components/navigation/breadcrumbs/types"; import { useCanvas } from "@rilldata/web-common/features/canvas/selector"; @@ -18,10 +17,7 @@ import HeaderLogo from "@rilldata/web-common/layout/header/HeaderLogo.svelte"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import type { V1ProjectPermissions } from "../../client"; - import { - createAdminServiceGetCurrentUser, - createAdminServiceGetDeploymentCredentials, - } from "../../client"; + import { createAdminServiceGetCurrentUser } from "../../client"; import ViewAsUserChip from "../../features/view-as-user/ViewAsUserChip.svelte"; import { viewAsUserStore } from "../../features/view-as-user/viewAsUserStore"; import CreateAlert from "../alerts/CreateAlert.svelte"; @@ -72,40 +68,6 @@ $: onCanvasDashboardPage = isCanvasDashboardPage($page); $: onPublicURLPage = isPublicURLPage($page); - // When "View As" is active, fetch deployment credentials for the mocked user. - $: mockedUserId = $viewAsUserStore?.id; - - $: mockedCredentialsQuery = createAdminServiceGetDeploymentCredentials( - organization, - project, - { userId: mockedUserId }, - { - query: { - enabled: !!mockedUserId && !!organization && !!project, - }, - }, - ); - - $: mockedProjectQuery = createAdminServiceGetProjectWithBearerToken( - organization, - project, - $mockedCredentialsQuery.data?.accessToken ?? "", - undefined, - { - query: { - enabled: !!$mockedCredentialsQuery.data?.accessToken, - }, - }, - ); - - // Use effective permissions when "View As" is active (from server) - $: effectiveManageProjectMembers = - $mockedProjectQuery.data?.projectPermissions?.manageProjectMembers ?? - projectPermissions.manageProjectMembers; - $: effectiveCreateMagicAuthTokens = - $mockedProjectQuery.data?.projectPermissions?.createMagicAuthTokens ?? - projectPermissions.createMagicAuthTokens; - $: loggedIn = !!$user.data?.user; $: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/"; @@ -213,7 +175,7 @@ {#if $viewAsUserStore} {/if} - {#if onProjectPage && effectiveManageProjectMembers} + {#if onProjectPage && projectPermissions.manageProjectMembers} {/if} {/if} @@ -263,7 +225,7 @@ {/if} {/if} diff --git a/web-admin/src/features/projects/project-query-options.ts b/web-admin/src/features/projects/project-query-options.ts new file mode 100644 index 00000000000..a7461b6b9af --- /dev/null +++ b/web-admin/src/features/projects/project-query-options.ts @@ -0,0 +1,35 @@ +import { + V1DeploymentStatus, + type RpcStatus, + type V1GetProjectResponse, +} from "@rilldata/web-admin/client"; +import { RUNTIME_ACCESS_TOKEN_DEFAULT_TTL } from "@rilldata/web-common/runtime-client/constants"; +import type { CreateQueryOptions } from "@tanstack/svelte-query"; + +const PollTimeWhenProjectDeploymentPending = 1000; +const PollTimeWhenProjectDeploymentError = 5000; +const PollTimeWhenProjectDeploymentOk = RUNTIME_ACCESS_TOKEN_DEFAULT_TTL / 2; // Proactively refetch the JWT before it expires + +export const baseGetProjectQueryOptions: Partial< + CreateQueryOptions +> = { + gcTime: Math.min(RUNTIME_ACCESS_TOKEN_DEFAULT_TTL, 1000 * 60 * 5), // Make sure we don't keep a stale JWT in the cache + refetchInterval: (query) => { + const status = query.state.data?.deployment?.status; + switch (status) { + case V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING: + case V1DeploymentStatus.DEPLOYMENT_STATUS_UPDATING: + return PollTimeWhenProjectDeploymentPending; + case V1DeploymentStatus.DEPLOYMENT_STATUS_ERRORED: + return PollTimeWhenProjectDeploymentError; + case V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING: + return PollTimeWhenProjectDeploymentOk; + default: + return false; + } + }, + refetchIntervalInBackground: true, // Keep polling while the tab is hidden (e.g. deploy loader) + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, +}; diff --git a/web-admin/src/features/projects/project-runtime.spec.ts b/web-admin/src/features/projects/project-runtime.spec.ts new file mode 100644 index 00000000000..06548be4e01 --- /dev/null +++ b/web-admin/src/features/projects/project-runtime.spec.ts @@ -0,0 +1,143 @@ +import type { + V1GetDeploymentCredentialsResponse, + V1GetProjectResponse, + V1ProjectPermissions, +} from "@rilldata/web-admin/client"; +import { describe, expect, it } from "vitest"; +import { resolveRuntimeConnection } from "./project-runtime"; + +const fakeProjectData: V1GetProjectResponse = { + deployment: { + runtimeHost: "https://runtime.example.com", + runtimeInstanceId: "inst-123", + }, + jwt: "project-jwt", + projectPermissions: { + readDev: true, + manageDev: false, + manageProjectMembers: true, + createMagicAuthTokens: true, + }, +}; + +const fakeMockedCredentials: V1GetDeploymentCredentialsResponse = { + runtimeHost: "https://mock-runtime.example.com", + instanceId: "mock-inst-456", + accessToken: "mock-jwt", +}; + +const fakeMockedPermissions: V1ProjectPermissions = { + readDev: false, + manageDev: false, + manageProjectMembers: false, + createMagicAuthTokens: false, +}; + +describe("resolveRuntimeConnection", () => { + it("returns cookie auth (user) by default", () => { + const result = resolveRuntimeConnection( + fakeProjectData, + undefined, + undefined, + undefined, + false, + ); + expect(result).toEqual({ + authContext: "user", + host: "https://runtime.example.com", + instanceId: "inst-123", + jwt: "project-jwt", + projectPermissions: fakeProjectData.projectPermissions, + }); + }); + + it("returns undefined fields when projectData is undefined", () => { + const result = resolveRuntimeConnection( + undefined, + undefined, + undefined, + undefined, + false, + ); + expect(result).toEqual({ + authContext: "user", + host: undefined, + instanceId: undefined, + jwt: undefined, + projectPermissions: undefined, + }); + }); + + it("returns magic auth on public URL pages", () => { + const result = resolveRuntimeConnection( + fakeProjectData, + undefined, + undefined, + undefined, + true, + ); + expect(result.authContext).toBe("magic"); + expect(result.host).toBe("https://runtime.example.com"); + }); + + it("returns mock auth when View As is active", () => { + const result = resolveRuntimeConnection( + fakeProjectData, + "user-789", + fakeMockedCredentials, + fakeMockedPermissions, + false, + ); + expect(result).toEqual({ + authContext: "mock", + host: "https://mock-runtime.example.com", + instanceId: "mock-inst-456", + jwt: "mock-jwt", + projectPermissions: fakeMockedPermissions, + }); + }); + + it("mock auth takes priority over public URL", () => { + const result = resolveRuntimeConnection( + fakeProjectData, + "user-789", + fakeMockedCredentials, + fakeMockedPermissions, + true, + ); + expect(result.authContext).toBe("mock"); + }); + + it("falls back to project permissions when mock permissions are undefined", () => { + const result = resolveRuntimeConnection( + fakeProjectData, + "user-789", + fakeMockedCredentials, + undefined, + false, + ); + expect(result.projectPermissions).toEqual( + fakeProjectData.projectPermissions, + ); + }); + + it("requires both mockedUserId and credentials for mock mode", () => { + const resultWithIdOnly = resolveRuntimeConnection( + fakeProjectData, + "user-789", + undefined, + undefined, + false, + ); + expect(resultWithIdOnly.authContext).toBe("user"); + + const resultWithCredsOnly = resolveRuntimeConnection( + fakeProjectData, + undefined, + fakeMockedCredentials, + undefined, + false, + ); + expect(resultWithCredsOnly.authContext).toBe("user"); + }); +}); diff --git a/web-admin/src/features/projects/project-runtime.ts b/web-admin/src/features/projects/project-runtime.ts new file mode 100644 index 00000000000..a38ba8f22a2 --- /dev/null +++ b/web-admin/src/features/projects/project-runtime.ts @@ -0,0 +1,49 @@ +import type { + V1GetDeploymentCredentialsResponse, + V1GetProjectResponse, + V1ProjectPermissions, +} from "@rilldata/web-admin/client"; +import type { AuthContext } from "@rilldata/web-common/runtime-client/v2/runtime-client"; + +/** + * Resolves the effective runtime connection based on which auth mode is active. + * + * Three modes, in priority order: + * 1. Mock (View As): use mocked user's credentials and permissions + * 2. Magic (Public URL): bearer-token auth, project's runtime host/jwt + * 3. User (default): cookie auth, project's runtime host/jwt + */ +export function resolveRuntimeConnection( + projectData: V1GetProjectResponse | undefined, + mockedUserId: string | undefined, + mockedCredentials: V1GetDeploymentCredentialsResponse | undefined, + mockedProjectPermissions: V1ProjectPermissions | undefined, + onPublicURLPage: boolean, +): { + authContext: AuthContext; + host: string | undefined; + instanceId: string | undefined; + jwt: string | undefined; + projectPermissions: V1ProjectPermissions | undefined; +} { + const isMocked = !!(mockedUserId && mockedCredentials); + + if (isMocked) { + return { + authContext: "mock", + host: mockedCredentials.runtimeHost, + instanceId: mockedCredentials.instanceId, + jwt: mockedCredentials.accessToken, + projectPermissions: + mockedProjectPermissions ?? projectData?.projectPermissions, + }; + } + + return { + authContext: onPublicURLPage ? "magic" : "user", + host: projectData?.deployment?.runtimeHost, + instanceId: projectData?.deployment?.runtimeInstanceId, + jwt: projectData?.jwt, + projectPermissions: projectData?.projectPermissions, + }; +} diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 1679a13e62e..718901e81c6 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -1,44 +1,23 @@ - + {#if error} @@ -261,18 +225,20 @@ body={error.response.data?.message} /> {:else if projectData} - {#if isProjectAvailable && effectiveHost != null && effectiveInstanceId} - {#key `${effectiveHost}::${effectiveInstanceId}`} + {#if isProjectAvailable && runtime.host != null && runtime.instanceId} + + {#key `${runtime.host}::${runtime.instanceId}`} {#if onProjectPage && deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING} {/if} - + {@render children()} {/key} {:else} From 5368c81366bc94235ed17173e7da444158c4a8a4 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 30 Mar 2026 14:23:37 +0300 Subject: [PATCH 2/7] chore: clean up `features/projects` directory - Delete `invalidations.ts` (dead code, zero consumers) - Delete `ResourceError.svelte` (dead code, shadowed by `web-common` version) - Move `constants.ts` to `user-management/constants.ts` (closer to its only consumer) --- .../dialogs/ConvertGuestToMemberDialog.svelte | 2 +- .../features/projects/ResourceError.svelte | 19 -------------- .../src/features/projects/invalidations.ts | 25 ------------------- .../{ => user-management}/constants.ts | 0 4 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 web-admin/src/features/projects/ResourceError.svelte delete mode 100644 web-admin/src/features/projects/invalidations.ts rename web-admin/src/features/projects/{ => user-management}/constants.ts (100%) diff --git a/web-admin/src/features/organizations/user-management/dialogs/ConvertGuestToMemberDialog.svelte b/web-admin/src/features/organizations/user-management/dialogs/ConvertGuestToMemberDialog.svelte index 337a1b802f6..dccd2535897 100644 --- a/web-admin/src/features/organizations/user-management/dialogs/ConvertGuestToMemberDialog.svelte +++ b/web-admin/src/features/organizations/user-management/dialogs/ConvertGuestToMemberDialog.svelte @@ -9,7 +9,7 @@ import { PROJECT_ROLES_DESCRIPTION_MAP, PROJECT_ROLES_OPTIONS, - } from "@rilldata/web-admin/features/projects/constants.ts"; + } from "@rilldata/web-admin/features/projects/user-management/constants.ts"; import { Button } from "@rilldata/web-common/components/button"; import Select from "@rilldata/web-common/components/forms/Select.svelte"; import * as Dialog from "@rilldata/web-common/components/dialog"; diff --git a/web-admin/src/features/projects/ResourceError.svelte b/web-admin/src/features/projects/ResourceError.svelte deleted file mode 100644 index 5e420fd8cb3..00000000000 --- a/web-admin/src/features/projects/ResourceError.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
-
-
Error loading {kind}s
-
- If this error persists, please contact support. -
-
-
diff --git a/web-admin/src/features/projects/invalidations.ts b/web-admin/src/features/projects/invalidations.ts deleted file mode 100644 index 56abbf6d3f9..00000000000 --- a/web-admin/src/features/projects/invalidations.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - getAdminServiceGetGithubUserStatusQueryKey, - getAdminServiceGetProjectQueryKey, -} from "@rilldata/web-admin/client"; -import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient"; -import { invalidateRuntimeQueries } from "@rilldata/web-common/runtime-client/invalidation"; - -export function invalidateProjectQueries( - instanceId: string, - organization: string, - project: string, -) { - return Promise.all([ - queryClient.refetchQueries({ - queryKey: getAdminServiceGetProjectQueryKey(organization, project), - - // avoid refetching createAdminServiceGetProjectWithBearerToken - exact: true, - }), - queryClient.refetchQueries({ - queryKey: getAdminServiceGetGithubUserStatusQueryKey(), - }), - invalidateRuntimeQueries(queryClient, instanceId), - ]); -} diff --git a/web-admin/src/features/projects/constants.ts b/web-admin/src/features/projects/user-management/constants.ts similarity index 100% rename from web-admin/src/features/projects/constants.ts rename to web-admin/src/features/projects/user-management/constants.ts From fb407db692b660a326092349690392bd5b480967 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 30 Mar 2026 15:02:16 +0300 Subject: [PATCH 3/7] fix: update remaining `constants` import paths after file move --- .../projects/user-management/OrgUserGroupSetRole.svelte | 2 +- .../projects/user-management/ProjectUserGroupSetRole.svelte | 2 +- .../src/features/projects/user-management/UserRoleSelect.svelte | 2 +- .../src/features/projects/user-management/UserSetRole.svelte | 2 +- .../features/projects/user-management/UsergroupSetRole.svelte | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web-admin/src/features/projects/user-management/OrgUserGroupSetRole.svelte b/web-admin/src/features/projects/user-management/OrgUserGroupSetRole.svelte index 545a6e661b7..4d63e72c5d5 100644 --- a/web-admin/src/features/projects/user-management/OrgUserGroupSetRole.svelte +++ b/web-admin/src/features/projects/user-management/OrgUserGroupSetRole.svelte @@ -13,7 +13,7 @@ import { useQueryClient } from "@tanstack/svelte-query"; import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import { PROJECT_ROLES_DESCRIPTION_MAP } from "../constants"; + import { PROJECT_ROLES_DESCRIPTION_MAP } from "./constants"; export let organization: string; export let group: V1MemberUsergroup; diff --git a/web-admin/src/features/projects/user-management/ProjectUserGroupSetRole.svelte b/web-admin/src/features/projects/user-management/ProjectUserGroupSetRole.svelte index 5d2f6cf7cec..9900d7b2d0c 100644 --- a/web-admin/src/features/projects/user-management/ProjectUserGroupSetRole.svelte +++ b/web-admin/src/features/projects/user-management/ProjectUserGroupSetRole.svelte @@ -13,7 +13,7 @@ import { useQueryClient } from "@tanstack/svelte-query"; import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import { PROJECT_ROLES_DESCRIPTION_MAP } from "../constants"; + import { PROJECT_ROLES_DESCRIPTION_MAP } from "./constants"; export let organization: string; export let group: V1MemberUsergroup; diff --git a/web-admin/src/features/projects/user-management/UserRoleSelect.svelte b/web-admin/src/features/projects/user-management/UserRoleSelect.svelte index cdc9aa1e676..6f80dfef459 100644 --- a/web-admin/src/features/projects/user-management/UserRoleSelect.svelte +++ b/web-admin/src/features/projects/user-management/UserRoleSelect.svelte @@ -7,7 +7,7 @@ } from "@rilldata/web-common/components/dropdown-menu"; import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import { PROJECT_ROLES_OPTIONS } from "../constants"; + import { PROJECT_ROLES_OPTIONS } from "./constants"; export let value: string; export let width = "w-18"; diff --git a/web-admin/src/features/projects/user-management/UserSetRole.svelte b/web-admin/src/features/projects/user-management/UserSetRole.svelte index ef6d6f895e5..04bc5c18f6c 100644 --- a/web-admin/src/features/projects/user-management/UserSetRole.svelte +++ b/web-admin/src/features/projects/user-management/UserSetRole.svelte @@ -16,7 +16,7 @@ import { useQueryClient } from "@tanstack/svelte-query"; import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import { PROJECT_ROLES_DESCRIPTION_MAP } from "../constants"; + import { PROJECT_ROLES_DESCRIPTION_MAP } from "./constants"; type User = V1ProjectMemberUser | V1ProjectInvite; diff --git a/web-admin/src/features/projects/user-management/UsergroupSetRole.svelte b/web-admin/src/features/projects/user-management/UsergroupSetRole.svelte index 00128e223bb..04972dbc3aa 100644 --- a/web-admin/src/features/projects/user-management/UsergroupSetRole.svelte +++ b/web-admin/src/features/projects/user-management/UsergroupSetRole.svelte @@ -12,7 +12,7 @@ import { useQueryClient } from "@tanstack/svelte-query"; import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import { PROJECT_ROLES_DESCRIPTION_MAP } from "../constants"; + import { PROJECT_ROLES_DESCRIPTION_MAP } from "./constants"; export let organization: string; export let project: string; From a0f9672da57f5458bdbfb51b9c081c3fa2d0bc65 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 30 Mar 2026 15:13:50 +0300 Subject: [PATCH 4/7] fix: update `ResourceError` import to use `web-common` version --- .../src/features/dashboards/listing/DashboardsTable.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-admin/src/features/dashboards/listing/DashboardsTable.svelte b/web-admin/src/features/dashboards/listing/DashboardsTable.svelte index 4cdadbf90a5..b73ca548396 100644 --- a/web-admin/src/features/dashboards/listing/DashboardsTable.svelte +++ b/web-admin/src/features/dashboards/listing/DashboardsTable.svelte @@ -1,6 +1,6 @@