From 69b44f3b68e47ebeeec4acdd7a8b965bdc58445a Mon Sep 17 00:00:00 2001 From: Guilhem Date: Tue, 6 Jan 2026 18:06:42 +0000 Subject: [PATCH] fix(frontend): improve workspace page (#7502) * nit * Improve workspace view by showing a tree * implement search for workspaces * Add collapse expand button * improve unarchive button * nit * move search * nit * add max h * Add keyboard navigation * clean code * Show admin workspaces with other workspaces * Update frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * nit --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../src/lib/components/CenteredModal.svelte | 2 +- .../src/lib/components/UserSettings.svelte | 2 +- .../src/lib/components/common/button/model.ts | 2 +- .../src/lib/components/home/ItemsList.svelte | 12 +- .../components/sidebar/WorkspaceMenu.svelte | 31 +- .../components/workspace/WorkspaceCard.svelte | 226 ++++++++++ .../components/workspace/WorkspaceIcon.svelte | 31 ++ .../workspace/WorkspaceTreeView.svelte | 412 ++++++++++++++++++ frontend/src/lib/utils.ts | 2 +- .../user/(user)/accept_invite/+page.svelte | 12 +- .../user/(user)/workspaces/+page.svelte | 253 +++++------ 11 files changed, 821 insertions(+), 164 deletions(-) create mode 100644 frontend/src/lib/components/workspace/WorkspaceCard.svelte create mode 100644 frontend/src/lib/components/workspace/WorkspaceIcon.svelte create mode 100644 frontend/src/lib/components/workspace/WorkspaceTreeView.svelte diff --git a/frontend/src/lib/components/CenteredModal.svelte b/frontend/src/lib/components/CenteredModal.svelte index 94dc9dd550..e1732c931b 100644 --- a/frontend/src/lib/components/CenteredModal.svelte +++ b/frontend/src/lib/components/CenteredModal.svelte @@ -33,7 +33,7 @@ {title} {#if subtitle} -

+

{subtitle}

{/if} diff --git a/frontend/src/lib/components/UserSettings.svelte b/frontend/src/lib/components/UserSettings.svelte index 489ee865a3..6e4dc92248 100644 --- a/frontend/src/lib/components/UserSettings.svelte +++ b/frontend/src/lib/components/UserSettings.svelte @@ -50,7 +50,7 @@ - +
{#if scopes == undefined}
= { - xs: 'px-1', + xs: 'px-2', sm: 'px-2', // Regular horizontal padding md: 'px-4', lg: 'px-6' diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 53ecfdeb67..8f46ded01f 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -15,12 +15,12 @@ import { userStore, workspaceStore } from '$lib/stores' import type uFuzzy from '@leeoniya/ufuzzy' import { + ChevronsDownUp, + ChevronsUpDown, Code2, - FoldVertical, LayoutDashboard, ListFilterPlus, - SearchCode, - UnfoldVertical + SearchCode } from 'lucide-svelte' import { HOME_SEARCH_SHOW_FLOW, HOME_SEARCH_PLACEHOLDER } from '$lib/consts' @@ -502,11 +502,11 @@ {#if treeView} + {/if} + {/if} + {#if isWorkspaceDisabled(workspace)} + (user disabled in this workspace) + {/if} + {#if workspace.id === 'admins'} + Used to manage your Windmill instance + {/if} +
+
+ + + + + + + {#if children.length > 0} +
{ + onMouseClick?.() + onToggleExpand?.(workspace.id) + }} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onKeyboardNavigation?.() + onToggleExpand?.(workspace.id) + } + }} + onmouseenter={() => onMouseEnter?.(workspace.id)} + > +
+
+ + + {pluralize(children.length, 'fork', 'forks')} + +
+
+ +
+
+
+ {/if} + + + + {#if children.length > 0 && isExpanded} +
+ {#each children as child (child.id)} + + {/each} +
+ {/if} + diff --git a/frontend/src/lib/components/workspace/WorkspaceIcon.svelte b/frontend/src/lib/components/workspace/WorkspaceIcon.svelte new file mode 100644 index 0000000000..07e0010c9b --- /dev/null +++ b/frontend/src/lib/components/workspace/WorkspaceIcon.svelte @@ -0,0 +1,31 @@ + + +
+ {#if isForked} + + {#snippet text()} + {#if isForked && parentName} + Fork of {parentName} + {/if} + {/snippet} + + + {:else} + + {/if} +
\ No newline at end of file diff --git a/frontend/src/lib/components/workspace/WorkspaceTreeView.svelte b/frontend/src/lib/components/workspace/WorkspaceTreeView.svelte new file mode 100644 index 0000000000..87e94fed14 --- /dev/null +++ b/frontend/src/lib/components/workspace/WorkspaceTreeView.svelte @@ -0,0 +1,412 @@ + + + + + + workspace.name + ' (' + workspace.id + ')'} +/> + +
+ +
+ {#each rootWorkspaces as workspace (workspace.id)} + + {/each} + + {#if rootWorkspaces.length === 0} +
+ +

+ {searchFilter ? 'No workspaces match your search' : 'No workspaces available'} +

+
+ {/if} +
+
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 88309b79a1..181a4a3ef7 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -801,7 +801,7 @@ export function pluralize(quantity: number, word: string, customPlural?: string) if (quantity == 1) { return `${quantity} ${word}` } else if (customPlural) { - return `${quantity} ${customPlural}}` + return `${quantity} ${customPlural}` } else { return `${quantity} ${word}s` } diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/accept_invite/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/accept_invite/+page.svelte index 2cf363a3c9..9257f198c0 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/accept_invite/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/accept_invite/+page.svelte @@ -80,16 +80,16 @@ {/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 fbca74b36c..572992bf01 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte @@ -21,12 +21,13 @@ import CenteredModal from '$lib/components/CenteredModal.svelte' import { USER_SETTINGS_HASH } from '$lib/components/sidebar/settings' import { switchWorkspace } from '$lib/storeUtils' - import { Crown, GitFork, Settings } from 'lucide-svelte' + import { GitFork, Settings, User, Search, ChevronsDownUp, ChevronsUpDown } from 'lucide-svelte' import { isCloudHosted } from '$lib/cloud' import { emptyString } from '$lib/utils' import { getUserExt } from '$lib/user' import { refreshSuperadmin } from '$lib/refreshUser' - import { buildWorkspaceHierarchy } from '$lib/utils/workspaceHierarchy' + import WorkspaceTreeView from '$lib/components/workspace/WorkspaceTreeView.svelte' + import TextInput from '$lib/components/text_input/TextInput.svelte' import type { UserWorkspace } from '$lib/stores' let invites: WorkspaceInvite[] = [] @@ -34,6 +35,12 @@ let workspaces: UserWorkspace[] | undefined = undefined let showAllForks: boolean = false + // Workspace tree controls + let workspaceSearchFilter = '' + let workspaceAllExpanded = false + let workspaceHasForks = false + let workspaceTreeView: WorkspaceTreeView | undefined = undefined + let userSettings: UserSettings let superadminSettings: SuperadminSettings @@ -84,20 +91,8 @@ } $: list_all_as_super_admin != undefined && $userWorkspaces && handleListWorkspaces() - $: adminsInstance = workspaces?.find((x) => x.id == 'admins') || $superadmin - - // Complete workspace hierarchy with all forks - $: forkedWorkspacesHierarchy = (() => { - if (!workspaces) return [] - - // Filter out admin workspace - const nonAdminWorkspaces = workspaces.filter((x) => x.id !== 'admins') - - return buildWorkspaceHierarchy(nonAdminWorkspaces) - })() - - $: groupedNonAdminWorkspaces = forkedWorkspacesHierarchy - $: noWorkspaces = $superadmin && groupedNonAdminWorkspaces.length == 0 + $: allWorkspaces = workspaces || [] + $: noWorkspaces = $superadmin && allWorkspaces.length == 0 async function getCreateWorkspaceRequireSuperadmin() { const r = await fetch(base + '/api/workspaces/create_workspace_require_superadmin') @@ -123,6 +118,20 @@ async function speakFriendAndEnterWorkspace(workspaceId: string) { loading = true + + // Special handling for admins workspace + if (workspaceId === 'admins') { + workspaceStore.set('admins') + if (rd?.startsWith('http')) { + window.location.href = rd + return + } + await goto(rd ?? '/') + loading = false + return + } + + // Regular workspace handling workspaceStore.set(undefined) workspaceStore.set(workspaceId) $userStore = await getUserExt($workspaceStore!) @@ -154,6 +163,10 @@ } loading = false } + + function workspaceExpandCollapseAll() { + workspaceTreeView?.handleExpandCollapseAll() + } {#if $superadmin} @@ -161,38 +174,47 @@ {/if} -
-

+
+

Workspaces{#if loading}{/if}

- {#if $superadmin} -
- + {#if allWorkspaces.length > 1} +
+
+ + +
+ {#if workspaceHasForks} + + {/if}
{/if}
- {#if adminsInstance} - + {#if $superadmin} +
+ +
{/if} {#if workspaces && $usersWorkspaceStore} @@ -202,58 +224,25 @@ create your own{/if} workspace.

+ {:else} + { + if (list_all_as_super_admin) { + loadWorkspacesAsAdmin() + } else { + loadWorkspaces() + } + }} + bind:searchFilter={workspaceSearchFilter} + bind:allExpanded={workspaceAllExpanded} + bind:hasForks={workspaceHasForks} + bind:this={workspaceTreeView} + /> {/if} - {#each groupedNonAdminWorkspaces as { workspace, depth, isForked } (workspace.id)} - - {/each} {:else} - {#each new Array(3) as _} + {#each new Array(3) as _, i (i)} {/each} {/if} @@ -261,10 +250,11 @@ {#if createWorkspace}
@@ -272,10 +262,19 @@ {@const nonForkInvites = invites.filter((invite) => invite.parent_workspace_id == undefined)} -

Invites to join a Workspace

+
+

Invites to join a Workspace

+ {#if workspaces} + + {/if} +
+ +
+ {#if nonForkInvites.length == 0} -

You don't have new invites at the moment.

+

You don't have new invites at the moment.

{/if} + {#each nonForkInvites as invite}
- -
- {/if} - {#if showAllForks} {@const allWorkspacesList = workspaces || []} {@const filteredInvites = invites.filter((invite) => invite.parent_workspace_id)} -

Forks of the workspaces you're in

+
{#if filteredInvites.length == 0} -

There isn't anything here

+

There are no invites to join the forks of any workspace you're in.

+ {:else} + Forks of the workspaces you're in {/if} + {#each filteredInvites as invite} {@const inviteWorkspace = allWorkspacesList.find((w) => w.id === invite.workspace_id)}
- Accept - + - +
{/each} @@ -387,25 +388,33 @@ {#if $superadmin} + {:else} + {/if} -