Files
windmill/frontend/src/lib/utils/workspaceHierarchy.ts
T
3dadcbe865 feat: forkables workspaces v0 (#6479)
* Add create_ephemeral workspace endpoint

* Add cli devShell

* List ephemeral workspaces + improve endpoint

* Add postgres function to clone a workspace (to be revisited)

* Clone workspace using the postgres function

* Add first iteration of ephemeral workspaces command

* Update display of forked workspaces

* Remove SQLX_OFFLINE

* Add UI to create ephemeral workspace

* Add option to exclude repository from being inherited to forks

* WIP: reworking cloning logic

* Fix cloning

* Fix redirect after creating fork

* Clean up cloning behaviour

* Rename ephemeral to fork

* emove ephemeral_workspaces table in favour of columns in  workspaces

* Fix display of forked workspaces

* Fix skip inherit git sync repo setting

* Fix fork invite display + creating fork as user

* Fix SideMenu bug

* Fix alignment

* Simplify migrations

* Update deletion of workspaces

* Delete forked workspace from cli

* Deleting fork workspaces from the UI as non-admin

* Update cli sync and fork creation to adapt to branches and forks

* Update fork prefix

* Remove skip tracking toggle

* Fix npm check warnings

* Fix last npm check

* fix: force stdin to Stdio::null for all user code execution (#6575)

Set stdin to Stdio::null for all Commands that execute user code across all supported languages to prevent unwanted input consumption. This affects Python, Deno, Bash, PowerShell, Go, Rust, PHP, Ruby, Java, C#, Ansible, Nu, and Bun executors.

The dedicated worker handler was intentionally left unchanged as it requires stdin for inter-process communication.

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* Update ee-repo ref

* Update SQLx metadata

* Fix typos

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2025-09-10 17:42:45 +00:00

129 lines
3.8 KiB
TypeScript

import type { UserWorkspace } from '../stores'
export interface WorkspaceHierarchyItem {
workspace: UserWorkspace
depth: number
isForked: boolean
parentName?: string
hasChildren: boolean
}
/**
* Builds a hierarchical structure from a flat array of workspaces.
* Supports unlimited nesting levels (fork of fork of fork...).
* Returns a flattened array with hierarchy metadata for easy rendering.
*/
export function buildWorkspaceHierarchy(workspaces: UserWorkspace[]): WorkspaceHierarchyItem[] {
if (!workspaces || workspaces.length === 0) {
return []
}
// Create maps for quick lookups
const workspaceMap = new Map(workspaces.map(w => [w.id, w]))
const childrenMap = new Map<string, UserWorkspace[]>()
const hasChildrenSet = new Set<string>()
// Build children mapping and track which workspaces have children
for (const workspace of workspaces) {
if (workspace.parent_workspace_id) {
if (!childrenMap.has(workspace.parent_workspace_id)) {
childrenMap.set(workspace.parent_workspace_id, [])
}
childrenMap.get(workspace.parent_workspace_id)!.push(workspace)
hasChildrenSet.add(workspace.parent_workspace_id)
}
}
// Find root workspaces (those without a parent or whose parent is not in the current list)
const rootWorkspaces = workspaces.filter(w => {
if (!w.parent_workspace_id) {
return true // Definitely a root
}
// Check if parent exists in the current workspace list
return !workspaceMap.has(w.parent_workspace_id)
})
const result: WorkspaceHierarchyItem[] = []
// Recursively build the hierarchy
function addWorkspaceAndChildren(workspace: UserWorkspace, depth: number, isForked: boolean, parentName?: string) {
// Add the current workspace
result.push({
workspace,
depth,
isForked,
parentName,
hasChildren: hasChildrenSet.has(workspace.id)
})
// Add its children (sorted by name for consistency)
const children = childrenMap.get(workspace.id) || []
children
.sort((a, b) => a.name.localeCompare(b.name))
.forEach(child => {
addWorkspaceAndChildren(child, depth + 1, true, workspace.name)
})
}
// Process root workspaces (sorted by name for consistency)
rootWorkspaces
.sort((a, b) => a.name.localeCompare(b.name))
.forEach(workspace => {
const isRootForked = workspace.parent_workspace_id != null
const parentName = isRootForked && workspace.parent_workspace_id
? workspace.parent_workspace_id // Use parent ID as fallback if parent not in list
: undefined
addWorkspaceAndChildren(workspace, 0, isRootForked, parentName)
})
return result
}
/**
* Helper function to get the indentation padding based on depth.
* Each level adds 24px of left padding.
*/
export function getWorkspaceIndentation(depth: number): string {
return `${depth * 24}px`
}
/**
* Helper function to check if a workspace is a root workspace
*/
export function isRootWorkspace(workspace: UserWorkspace): boolean {
return workspace.parent_workspace_id == null
}
/**
* Helper function to find all descendants of a workspace
*/
export function findWorkspaceDescendants(
workspaceId: string,
allWorkspaces: UserWorkspace[]
): UserWorkspace[] {
const descendants: UserWorkspace[] = []
const childrenMap = new Map<string, UserWorkspace[]>()
// Build children mapping
for (const workspace of allWorkspaces) {
if (workspace.parent_workspace_id) {
if (!childrenMap.has(workspace.parent_workspace_id)) {
childrenMap.set(workspace.parent_workspace_id, [])
}
childrenMap.get(workspace.parent_workspace_id)!.push(workspace)
}
}
// Recursively find descendants
function collectDescendants(id: string) {
const children = childrenMap.get(id) || []
for (const child of children) {
descendants.push(child)
collectDescendants(child.id)
}
}
collectDescendants(workspaceId)
return descendants
}