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 @@ - -
-
-
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/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..2af193fa7a5 --- /dev/null +++ b/web-admin/src/features/projects/project-runtime.spec.ts @@ -0,0 +1,105 @@ +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, 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, 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, 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, + { + credentials: fakeMockedCredentials, + permissions: 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, + { + credentials: fakeMockedCredentials, + permissions: fakeMockedPermissions, + }, + true, + ); + expect(result.authContext).toBe("mock"); + }); + + it("falls back to project permissions when mock permissions are undefined", () => { + const result = resolveRuntimeConnection( + fakeProjectData, + { credentials: fakeMockedCredentials }, + false, + ); + expect(result.projectPermissions).toEqual( + fakeProjectData.projectPermissions, + ); + }); +}); 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..7ad3c84fb93 --- /dev/null +++ b/web-admin/src/features/projects/project-runtime.ts @@ -0,0 +1,50 @@ +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, + mockUser: + | { + credentials: V1GetDeploymentCredentialsResponse; + permissions?: V1ProjectPermissions; + } + | undefined, + onPublicURLPage: boolean, +): { + authContext: AuthContext; + host: string | undefined; + instanceId: string | undefined; + jwt: string | undefined; + projectPermissions: V1ProjectPermissions | undefined; +} { + if (mockUser) { + return { + authContext: "mock", + host: mockUser.credentials.runtimeHost, + instanceId: mockUser.credentials.instanceId, + jwt: mockUser.credentials.accessToken, + projectPermissions: + mockUser.permissions ?? 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/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; 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 diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 1679a13e62e..7af6f0565a0 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}