diff --git a/frontend/src/lib/instanceRoutes.ts b/frontend/src/lib/instanceRoutes.ts new file mode 100644 index 0000000000..7d57dc1b81 --- /dev/null +++ b/frontend/src/lib/instanceRoutes.ts @@ -0,0 +1,10 @@ +/** + * Pages a devops user can open without belonging to any workspace. Devops grants no workspace + * access, so such a page renders with `$workspaceStore` and `$userStore` unset and must hide + * whatever needs them (a workspace-scoped call or a link into a workspace) rather than break. + */ +const INSTANCE_ROUTES = ['/workers', '/service_logs'] + +export function isInstanceRoute(pathname: string): boolean { + return INSTANCE_ROUTES.some((r) => pathname === r || pathname.startsWith(r + '/')) +} diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index 918b5cd4d7..3fe47a74fc 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -79,8 +79,11 @@ Home, Play, Search, - WandSparkles + WandSparkles, + ArrowLeft } from 'lucide-svelte' + import { Button } from '$lib/components/common' + import { isInstanceRoute } from '$lib/instanceRoutes' import { getUserExt } from '$lib/user' import { confirmPendingLoginMethod } from '$lib/lastLoginMethod' import { deepEqual } from 'fast-equals' @@ -796,6 +799,11 @@ } let devOnly = $derived(page.url.pathname.startsWith('/scripts/dev')) + // A devops user who belongs to no workspace has no `$userStore`, and would otherwise wait on + // "Loading user..." forever on the instance pages their role exists for. + let instanceOnlyView = $derived( + !$workspaceStore && !!$devopsRole && isInstanceRoute(page.url.pathname) + ) async function loadDefaultScripts(workspace: string, user: UserExt | undefined) { if (!user?.operator) { @@ -1566,6 +1574,27 @@ outside this layout, and their shortcut must reopen the URL that is their creden /> +{:else if instanceOnlyView} + +
+
+ +
+ +
+
+
+ {@render children?.()} +
+
{:else} {/if} diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte index 2c5e626578..046f723e85 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte @@ -9,6 +9,7 @@ import { UserService, type WorkspaceInvite, WorkspaceService } from '$lib/gen' import { superadmin, + devopsRole, usersWorkspaceStore, userWorkspaces, workspaceStore, @@ -30,7 +31,9 @@ Search, ChevronsDownUp, ChevronsUpDown, - LogOut + LogOut, + Logs, + ServerCog } from 'lucide-svelte' import { isCloudHosted } from '$lib/cloud' import { isValidLogoutRedirect, toSameOriginRelativePath } from '$lib/logoutRedirect' @@ -169,17 +172,17 @@ let workspacesLoaded = $derived.by( () => workspaces !== undefined && $usersWorkspaceStore !== undefined ) - // Not for a superadmin: this page is also where they reach the instance settings and the - // list-all toggle, and standing the picker down takes both away — a superadmin with no - // membership of their own has business here besides creating a workspace. Waiting for the - // store to answer rather than reading `!$superadmin`, which is true while `globalWhoami` - // is still in flight. + // Not for a superadmin or devops user: this page is also where they reach the instance + // settings, the list-all toggle and the instance pages, and standing the picker down takes + // those away — they have business here besides creating a workspace. `$devopsRole` is truthy + // for a superadmin too. Waiting for the store to answer rather than reading `!$devopsRole`, + // which is true while `globalWhoami` is still in flight. let nothingToChoose = $derived( workspacesLoaded && invitesLoaded && createWorkspace && - $superadmin !== undefined && - !$superadmin && + $devopsRole !== undefined && + !$devopsRole && !list_all_as_super_admin && allWorkspaces.length === 0 && invites.length === 0 @@ -206,11 +209,11 @@ let showCreate = $state(false) $effect(() => { if (nothingToChoose) showCreate = true - // Except for a superadmin, whom `nothingToChoose` excludes — so this only ever undoes a - // latch that should not have happened: one taken on a stale `false` before the forced - // `refreshSuperadmin` above answered. Without it that superadmin would be stuck on the - // create form, instance settings and the list-all toggle gone with it, until a reload. - else if ($superadmin) showCreate = false + // Except for a superadmin or devops user, whom `nothingToChoose` excludes — so this only + // ever undoes a latch that should not have happened: one taken on a stale `false` before + // the forced `refreshSuperadmin` above answered. Without it they would be stuck on the + // create form, with what they came here for gone with it, until a reload. + else if ($devopsRole) showCreate = false }) async function speakFriendAndEnterWorkspace(workspaceId: string) { @@ -518,6 +521,20 @@ {/if} {/if} + + {#if !showCreate && $devopsRole && !$superadmin} +

Instance

+
+ + +
+ {/if} + diff --git a/frontend/src/routes/(root)/(logged)/workers/+page.svelte b/frontend/src/routes/(root)/(logged)/workers/+page.svelte index 78caf47cf1..5fa5ef7f56 100644 --- a/frontend/src/routes/(root)/(logged)/workers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workers/+page.svelte @@ -602,9 +602,12 @@ if (!config || !config.worker_tags || config.worker_tags.length === 0) cols += 1 // Worker tags if ((!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)) cols += 2 // Last job, Occupancy rate - if ($superadmin || $devopsRole) cols += 1 // Repl + if (replAvailable) cols += 1 // Repl return cols }) + // The REPL runs its commands as jobs in the selected workspace, which a devops user who + // belongs to no workspace does not have (see `isInstanceRoute`). + let replAvailable = $derived(($superadmin || $devopsRole) && !!$workspaceStore) {#if $superadmin || $devopsRole} @@ -1022,7 +1025,7 @@ Limits Version Status - {#if $superadmin || $devopsRole} + {#if replAvailable} Repl @@ -1152,13 +1155,17 @@ {jobs_executed} {#if (!config || config?.dedicated_worker == undefined) && ($superadmin || $devopsRole)} - {#if last_job_id} + + {#if last_job_id && $workspaceStore} View last job
{last_job_workspace_id} + {:else if last_job_id} + {last_job_workspace_id} {/if}
@@ -1277,7 +1284,7 @@
{pingText}
- {#if $superadmin || $devopsRole} + {#if replAvailable}