Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from "$app/stores";
import ResourceError from "@rilldata/web-admin/features/projects/ResourceError.svelte";
import ResourceError from "@rilldata/web-common/features/resources/ResourceError.svelte";
import ResourceList from "@rilldata/web-admin/features/resources/ResourceList.svelte";
import ResourceListEmptyState from "@rilldata/web-admin/features/resources/ResourceListEmptyState.svelte";
import ExploreIcon from "@rilldata/web-common/components/icons/ExploreIcon.svelte";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
46 changes: 4 additions & 42 deletions web-admin/src/features/projects/ProjectHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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" : "/";

Expand Down Expand Up @@ -213,7 +175,7 @@
{#if $viewAsUserStore}
<ViewAsUserChip />
{/if}
{#if onProjectPage && effectiveManageProjectMembers}
{#if onProjectPage && projectPermissions.manageProjectMembers}
<ShareProjectPopover
{organization}
{project}
Expand Down Expand Up @@ -249,7 +211,7 @@
<CreateAlert />
{/if}
<ShareDashboardPopover
createMagicAuthTokens={effectiveCreateMagicAuthTokens}
createMagicAuthTokens={projectPermissions.createMagicAuthTokens}
/>
{/if}
</StateManagersProvider>
Expand All @@ -263,7 +225,7 @@
{/if}
<CanvasBookmarks {organization} {project} canvasName={dashboard} />
<ShareDashboardPopover
createMagicAuthTokens={effectiveCreateMagicAuthTokens}
createMagicAuthTokens={projectPermissions.createMagicAuthTokens}
/>
{/if}

Expand Down
19 changes: 0 additions & 19 deletions web-admin/src/features/projects/ResourceError.svelte

This file was deleted.

25 changes: 0 additions & 25 deletions web-admin/src/features/projects/invalidations.ts

This file was deleted.

35 changes: 35 additions & 0 deletions web-admin/src/features/projects/project-query-options.ts
Original file line number Diff line number Diff line change
@@ -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<V1GetProjectResponse, RpcStatus>
> = {
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,
};
105 changes: 105 additions & 0 deletions web-admin/src/features/projects/project-runtime.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
50 changes: 50 additions & 0 deletions web-admin/src/features/projects/project-runtime.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading