mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
fix(frontend): improve workspace picker menu UI (#7491)
* Improve sidebar workspace picker * nit * nit
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
export const sidebarClasses = {
|
||||
text: 'text-primary-inverse dark:text-primary data-[light-mode=true]:text-primary text-xs font-normal',
|
||||
selectedText: 'text-emphasis-inverse dark:text-emphasis text-xs font-normal',
|
||||
sublabelText: 'text-secondary-inverse dark:text-secondary text-2xs font-normal',
|
||||
hoverBg:
|
||||
'transition-colors hover:bg-surface-hover-inverse dark:hover:bg-surface-hover data-[light-mode=true]:hover:bg-surface-hover'
|
||||
}
|
||||
@@ -20,6 +21,7 @@
|
||||
aiId?: string | undefined
|
||||
aiDescription?: string | undefined
|
||||
label?: string | undefined
|
||||
sublabel?: string | undefined
|
||||
icon?: any | undefined
|
||||
iconClasses?: string | null
|
||||
iconProps?: any | null
|
||||
@@ -39,6 +41,7 @@
|
||||
aiId = undefined,
|
||||
aiDescription = undefined,
|
||||
label = undefined,
|
||||
sublabel = undefined,
|
||||
icon = undefined,
|
||||
iconClasses = null,
|
||||
iconProps = null,
|
||||
@@ -91,41 +94,62 @@
|
||||
{href}
|
||||
data-light-mode={lightMode}
|
||||
class={twMerge(
|
||||
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
|
||||
'group flex items-center px-2 py-2 font-light rounded-md gap-2 w-full',
|
||||
sidebarClasses.hoverBg,
|
||||
color ? 'border-4' : '',
|
||||
'transition-all relative',
|
||||
sublabel ? 'h-10' : 'h-8',
|
||||
classNames
|
||||
)}
|
||||
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
|
||||
use:conditionalMelt={trigger}
|
||||
title={isCollapsed ? undefined : label}
|
||||
{...$trigger}
|
||||
>
|
||||
{#if icon}
|
||||
{@const SvelteComponent = icon}
|
||||
<SvelteComponent
|
||||
size={16}
|
||||
class={twMerge('flex-shrink-0', sidebarClasses.text, 'transition-colors', iconClasses)}
|
||||
{...iconProps}
|
||||
/>
|
||||
<div
|
||||
style="background-color: {color}"
|
||||
class={twMerge('rounded-full center-center', color ? 'p-1 -ml-1' : '')}
|
||||
>
|
||||
<SvelteComponent
|
||||
size={16}
|
||||
class={twMerge('flex-shrink-0', sidebarClasses.text, 'transition-colors', iconClasses)}
|
||||
{...iconProps}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isCollapsed && label}
|
||||
<span
|
||||
class={twMerge(
|
||||
'whitespace-pre truncate',
|
||||
sidebarClasses.text,
|
||||
'transition-all',
|
||||
classNames
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold">
|
||||
{shortcut}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
<div class="flex flex-col text-left grow min-w-0">
|
||||
{#if !isCollapsed && label}
|
||||
<div
|
||||
class={twMerge(
|
||||
'whitespace-pre truncate w-full',
|
||||
sidebarClasses.text,
|
||||
'transition-all',
|
||||
classNames
|
||||
)}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
<span
|
||||
class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold"
|
||||
>
|
||||
{shortcut}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if sublabel}
|
||||
<div
|
||||
class={twMerge(
|
||||
'whitespace-pre truncate w-full',
|
||||
sidebarClasses.sublabelText,
|
||||
'transition-all',
|
||||
classNames
|
||||
)}
|
||||
title={sublabel}>{sublabel}</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isCollapsed && notificationsCount > 0}
|
||||
<div class="absolute translate-x-1/2 translate-y-1/2 -top-2 right-1 flex h-fit w-fit">
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
{#if connected}
|
||||
<div class="divide-gray-100 border-t" role="none">
|
||||
<div class="px-2.5 text-xs font-semibold mt-1">Live activity</div>
|
||||
<div class="px-2 text-xs text-secondary font-normal mt-1">Live activity</div>
|
||||
<div class="py-1 flex flex-col gap-y-1 max-h-48 overflow-auto" transition:slide>
|
||||
{#each Object.entries($awarenessStore ?? {}) as [user, url]}
|
||||
<div class="inline-flex gap-2 px-2 items-center">
|
||||
@@ -97,8 +97,8 @@
|
||||
>
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-primary truncate">{user}</span>
|
||||
<span class="text-xs text-primary truncate">{showActivity(url)}</span>
|
||||
<span class="text-xs text-primary truncate">{user}</span>
|
||||
<span class="text-2xs text-secondary truncate">{showActivity(url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
} from '$lib/stores'
|
||||
import { Building, Plus, Settings, GitFork } from 'lucide-svelte'
|
||||
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
|
||||
import { Menu, MenuItem } from '$lib/components/meltComponents'
|
||||
import { Menu, MenuItem, Tooltip } from '$lib/components/meltComponents'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { base } from '$lib/base'
|
||||
import { page } from '$app/stores'
|
||||
@@ -23,6 +23,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { MenubarBuilders } from '@melt-ui/svelte'
|
||||
import { buildWorkspaceHierarchy } from '$lib/utils/workspaceHierarchy'
|
||||
import { getContrastTextColor } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
isCollapsed?: boolean
|
||||
@@ -66,12 +67,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a workspace is forked
|
||||
function isForkedWorkspace(workspaceId: string): boolean {
|
||||
if (!$userWorkspaces) return false
|
||||
return $userWorkspaces.some((w) => w.id === workspaceId && w.parent_workspace_id != null)
|
||||
}
|
||||
|
||||
function getForkedWorkspace(workspaceId: string) {
|
||||
if (!$userWorkspaces) return undefined
|
||||
return $userWorkspaces.find((w) => w.id === workspaceId && w.parent_workspace_id != null)
|
||||
@@ -87,43 +82,56 @@
|
||||
if (!$userWorkspaces) return []
|
||||
return buildWorkspaceHierarchy($userWorkspaces)
|
||||
})
|
||||
|
||||
const itemClass =
|
||||
'text-primary flex flex-row gap-2 px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
</script>
|
||||
|
||||
{#if isForkedWorkspace($workspaceStore ?? '') && !isCollapsed}
|
||||
{@const forkedWorkspace = getForkedWorkspace($workspaceStore ?? '')}
|
||||
{@const parentWorkspace = forkedWorkspace
|
||||
? getParentWorkspace(forkedWorkspace.parent_workspace_id!)
|
||||
: null}
|
||||
<Menu {createMenu} usePointerDownOutside>
|
||||
{#snippet triggr({ trigger })}
|
||||
<div class="group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full text-xs">
|
||||
<Building size={12} class="text-primary" />
|
||||
<span class="text-xs text-primary"> {parentWorkspace?.name ?? ''} </span>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Menu>
|
||||
{/if}
|
||||
{#snippet workspaceIcon(
|
||||
workspaceColor: string | undefined,
|
||||
isForked: boolean,
|
||||
parentName: string | undefined
|
||||
)}
|
||||
{@const iconColor = getContrastTextColor(workspaceColor)}
|
||||
<div style="background-color: {workspaceColor}" class="rounded-full p-1.5 center-center">
|
||||
{#if isForked}
|
||||
<Tooltip>
|
||||
{#snippet text()}
|
||||
{#if isForked && parentName}
|
||||
Fork of {parentName}
|
||||
{/if}
|
||||
{/snippet}
|
||||
<GitFork size={14} class="flex-shrink-0" style="color: {iconColor}" />
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<Building size={14} style="color: {iconColor}" />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Menu {createMenu} usePointerDownOutside>
|
||||
{#snippet triggr({ trigger })}
|
||||
{@const forkedWorkspace = getForkedWorkspace($workspaceStore ?? '')}
|
||||
{@const parentWorkspace = forkedWorkspace
|
||||
? getParentWorkspace(forkedWorkspace.parent_workspace_id!)
|
||||
: null}
|
||||
{@const iconColor = getContrastTextColor($workspaceColor)}
|
||||
{#if forkedWorkspace && parentWorkspace}
|
||||
<div class={isCollapsed ? '' : 'pl-6'}>
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={GitFork}
|
||||
label={removePrefix($workspaceStore ?? '', 'wm-fork-')}
|
||||
{isCollapsed}
|
||||
color={$workspaceColor}
|
||||
{trigger}
|
||||
/>
|
||||
</div>
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={GitFork}
|
||||
iconProps={iconColor ? { style: `color: ${iconColor}` } : undefined}
|
||||
label={removePrefix($workspaceStore ?? '', 'wm-fork-')}
|
||||
sublabel={parentWorkspace?.name ? `Fork of ${parentWorkspace.name}` : undefined}
|
||||
{isCollapsed}
|
||||
color={$workspaceColor}
|
||||
{trigger}
|
||||
/>
|
||||
{:else}
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={Building}
|
||||
iconProps={iconColor ? { style: `color: ${iconColor}` } : undefined}
|
||||
label={$workspaceStore ?? ''}
|
||||
{isCollapsed}
|
||||
color={$workspaceColor}
|
||||
@@ -136,12 +144,13 @@
|
||||
<div class="divide-y" role="none">
|
||||
<div class="py-1">
|
||||
{#each groupedWorkspaces() as { workspace, depth, isForked, parentName }}
|
||||
{@const isSelected = $workspaceStore === workspace.id}
|
||||
<MenuItem
|
||||
class={twMerge(
|
||||
'text-xs min-w-0 w-full overflow-hidden flex flex-col py-1.5',
|
||||
'text-xs min-w-0 w-full overflow-hidden flex flex-col py-2 px-3',
|
||||
workspace.disabled && 'opacity-50 cursor-not-allowed',
|
||||
$workspaceStore === workspace.id
|
||||
? 'cursor-default bg-surface-selected'
|
||||
isSelected
|
||||
? 'cursor-default bg-surface-accent-selected'
|
||||
: workspace.disabled
|
||||
? ''
|
||||
: 'cursor-pointer hover:bg-surface-hover data-[highlighted]:bg-surface-hover'
|
||||
@@ -154,73 +163,47 @@
|
||||
{item}
|
||||
>
|
||||
<div class="flex items-center justify-between min-w-0 w-full">
|
||||
<div
|
||||
class={twMerge('flex items-center gap-2 min-w-0', 'pl-4')}
|
||||
style:padding-left={`${4 + depth * 12}px`}
|
||||
>
|
||||
{#if isForked}
|
||||
<GitFork size={12} class="text-primary flex-shrink-0" />
|
||||
{:else}
|
||||
<Building size={12} />
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 min-w-0" style:padding-left={`${depth * 16}px`}>
|
||||
{@render workspaceIcon(workspace.color, isForked, parentName)}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
class={twMerge(
|
||||
'truncate text-left text-[1.2em]',
|
||||
isForked ? 'text-secondary' : 'text-primary'
|
||||
'truncate text-left text-xs font-normal',
|
||||
isSelected ? 'text-accent' : 'text-primary'
|
||||
)}
|
||||
title={workspace.name}
|
||||
>
|
||||
{workspace.name}{workspace.disabled ? ' (user disabled)' : ''}
|
||||
</div>
|
||||
<div
|
||||
class={twMerge(
|
||||
'font-mono text-2xs whitespace-nowrap truncate text-left',
|
||||
isForked ? 'text-primary opacity-75' : 'text-primary'
|
||||
'font-mono text-2xs whitespace-nowrap truncate text-left font-normal',
|
||||
isSelected ? 'text-accent/80' : 'text-hint'
|
||||
)}
|
||||
title={workspace.id}
|
||||
>
|
||||
{workspace.id}
|
||||
</div>
|
||||
{#if isForked && parentName}
|
||||
<div class="text-primary text-2xs truncate text-left pl-2 min-h-[1rem]">
|
||||
Fork of {parentName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if workspace.color}
|
||||
<div
|
||||
class="w-5 h-5 mr-2 rounded border border-gray-300 dark:border-gray-600"
|
||||
style="background-color: {workspace.color}"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</div>
|
||||
{#if (isCloudHosted() || $superadmin) && !strictWorkspaceSelect}
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="{base}/user/create_workspace"
|
||||
class="text-primary px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary flex flex-flow gap-2"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<MenuItem href="{base}/user/create_workspace" class={itemClass} {item}>
|
||||
<Plus size={16} />
|
||||
Workspace
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !strictWorkspaceSelect && !isCloudHosted()}
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="{base}/user/fork_workspace"
|
||||
class="text-primary px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary flex flex-flow gap-2"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<MenuItem href="{base}/user/fork_workspace" class={itemClass} {item}>
|
||||
<GitFork size={16} />
|
||||
Fork current workspace
|
||||
</a>
|
||||
</MenuItem>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !strictWorkspaceSelect}
|
||||
@@ -228,10 +211,7 @@
|
||||
<MenuItem
|
||||
href="{base}/user/workspaces"
|
||||
onClick={() => clearWorkspaceFromStorage()}
|
||||
class={twMerge(
|
||||
'text-primary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)}
|
||||
class={itemClass}
|
||||
{item}
|
||||
>
|
||||
All workspaces
|
||||
@@ -240,14 +220,7 @@
|
||||
{/if}
|
||||
{#if ($userStore?.is_admin || $superadmin) && !strictWorkspaceSelect}
|
||||
<div class="py-1" role="none">
|
||||
<MenuItem
|
||||
href="{base}/workspace_settings"
|
||||
class={twMerge(
|
||||
'text-secondary px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary flex flex-flow gap-2',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)}
|
||||
{item}
|
||||
>
|
||||
<MenuItem href="{base}/workspace_settings" class={itemClass} {item}>
|
||||
<Settings size={16} />
|
||||
Workspace settings
|
||||
</MenuItem>
|
||||
|
||||
@@ -862,6 +862,150 @@ export function isValidHexColor(color: string): boolean {
|
||||
return /^#(([A-F0-9]{2}){3,4}|[A-F0-9]{3})$/i.test(color)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the relative luminance of a color according to WCAG 2.1
|
||||
* @param r Red component (0-255)
|
||||
* @param g Green component (0-255)
|
||||
* @param b Blue component (0-255)
|
||||
* @returns Relative luminance value (0-1)
|
||||
*/
|
||||
function getRelativeLuminance(r: number, g: number, b: number): number {
|
||||
const [rs, gs, bs] = [r, g, b].map((val) => {
|
||||
val = val / 255
|
||||
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4)
|
||||
})
|
||||
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts hex color to HSL
|
||||
* @param hex Hex color string (e.g., "#FF0000")
|
||||
* @returns Array of [hue (0-360), saturation (0-100), lightness (0-100)]
|
||||
*/
|
||||
function hexToHsl(hex: string): [number, number, number] {
|
||||
// Normalize hex color
|
||||
let normalizedHex = hex.replace('#', '')
|
||||
if (normalizedHex.length === 3) {
|
||||
normalizedHex = normalizedHex
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('')
|
||||
}
|
||||
|
||||
const r = parseInt(normalizedHex.substring(0, 2), 16) / 255
|
||||
const g = parseInt(normalizedHex.substring(2, 4), 16) / 255
|
||||
const b = parseInt(normalizedHex.substring(4, 6), 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
let h = 0
|
||||
let s = 0
|
||||
const l = (max + min) / 2
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
||||
break
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6
|
||||
break
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, l * 100]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts HSL to hex color
|
||||
* @param h Hue (0-360)
|
||||
* @param s Saturation (0-100)
|
||||
* @param l Lightness (0-100)
|
||||
* @returns Hex color string
|
||||
*/
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
h = h / 360
|
||||
s = s / 100
|
||||
l = l / 100
|
||||
|
||||
let r: number, g: number, b: number
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l // Achromatic
|
||||
} else {
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t
|
||||
if (t < 1 / 2) return q
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
|
||||
return p
|
||||
}
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
const p = 2 * l - q
|
||||
r = hue2rgb(p, q, h + 1 / 3)
|
||||
g = hue2rgb(p, q, h)
|
||||
b = hue2rgb(p, q, h - 1 / 3)
|
||||
}
|
||||
|
||||
const toHex = (c: number) => {
|
||||
const hex = Math.round(c * 255).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a text color with the same hue as the background but adjusted lightness for good contrast
|
||||
* @param backgroundColor Hex color string (e.g., "#FF0000" or "#F00")
|
||||
* @returns Hex color string with same hue but good contrast, or undefined if invalid
|
||||
*/
|
||||
export function getContrastTextColor(
|
||||
backgroundColor: string | null | undefined
|
||||
): string | undefined {
|
||||
if (!backgroundColor || !isValidHexColor(backgroundColor)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Normalize hex color
|
||||
let hex = backgroundColor.replace('#', '')
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('')
|
||||
}
|
||||
|
||||
// Parse RGB components and calculate background luminance
|
||||
const r = parseInt(hex.substring(0, 2), 16)
|
||||
const g = parseInt(hex.substring(2, 4), 16)
|
||||
const b = parseInt(hex.substring(4, 6), 16)
|
||||
const bgLuminance = getRelativeLuminance(r, g, b)
|
||||
|
||||
// Convert to HSL to extract hue
|
||||
const [hue] = hexToHsl(backgroundColor)
|
||||
|
||||
// Determine if background is light or dark
|
||||
const isLightBackground = bgLuminance > 0.5
|
||||
|
||||
// Use fixed saturation and lightness based on background lightness
|
||||
// For light backgrounds: use dark text (low lightness, high saturation)
|
||||
// For dark backgrounds: use light text (high lightness, high saturation)
|
||||
const saturation = 70 // Fixed saturation for good readability
|
||||
const lightness = isLightBackground ? 25 : 85 // Dark for light bg, light for dark bg
|
||||
|
||||
// Generate the color with the same hue but adjusted saturation and lightness
|
||||
return hslToHex(hue, saturation, lightness)
|
||||
}
|
||||
|
||||
export function sortObject<T>(o: T & object): T {
|
||||
return Object.keys(o)
|
||||
.sort()
|
||||
|
||||
@@ -161,18 +161,20 @@
|
||||
{/if}
|
||||
|
||||
<CenteredModal title="Select a workspace" subtitle="Logged in as {$usersWorkspaceStore?.email}">
|
||||
<h2 class="mb-4 inline-flex gap-2 text-sm font-semibold text-emphasis">
|
||||
Workspaces{#if loading}<WindmillIcon spin="fast" />{/if}
|
||||
</h2>
|
||||
<div class="flex flex-row items-center gap-2 justify-between">
|
||||
<h2 class="mb-4 inline-flex gap-2 text-sm font-semibold text-emphasis">
|
||||
Workspaces{#if loading}<WindmillIcon spin="fast" />{/if}
|
||||
</h2>
|
||||
|
||||
{#if $superadmin}
|
||||
<div class="flex flex-row-reverse pb-2">
|
||||
<Toggle
|
||||
bind:checked={list_all_as_super_admin}
|
||||
options={{ right: 'List all workspaces as superadmin' }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $superadmin}
|
||||
<div class="flex flex-row-reverse pb-2">
|
||||
<Toggle
|
||||
bind:checked={list_all_as_super_admin}
|
||||
options={{ right: 'List all workspaces as superadmin' }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if adminsInstance}
|
||||
<Button
|
||||
@@ -201,7 +203,7 @@
|
||||
workspace.
|
||||
</p>
|
||||
{/if}
|
||||
{#each groupedNonAdminWorkspaces as { workspace, depth, isForked, parentName } (workspace.id)}
|
||||
{#each groupedNonAdminWorkspaces as { workspace, depth, isForked } (workspace.id)}
|
||||
<label class="block pb-2" style:padding-left={`${depth * 24}px`}>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -216,14 +218,14 @@
|
||||
{#if isForked}
|
||||
<GitFork size={12} class="text-primary mr-2 flex-shrink-0" />
|
||||
{/if}
|
||||
<span class="flex-1">
|
||||
<span class="flex-1 items-center">
|
||||
{#if workspace.color}
|
||||
<span
|
||||
class="inline-block w-3 h-3 mr-2 rounded-full border border-gray-400"
|
||||
<div
|
||||
class="inline-block w-4 h-4 mr-2 rounded-full border"
|
||||
style="background-color: {workspace.color}"
|
||||
></span>
|
||||
></div>
|
||||
{/if}
|
||||
<span class="font-mono" class:text-secondary={isForked}>{workspace.id}</span> -
|
||||
<span class="font-mono text-secondary">{workspace.id}</span> -
|
||||
<span class:text-secondary={isForked}>{workspace.name}</span>
|
||||
as
|
||||
<span class="font-mono" class:text-secondary={isForked}>{workspace.username}</span>
|
||||
@@ -233,12 +235,6 @@
|
||||
{#if workspace.disabled}
|
||||
<span class="text-red-500"> (user disabled in this workspace)</span>
|
||||
{/if}
|
||||
{#if isForked && parentName}
|
||||
<span class="text-secondary text-2xs mt-1">
|
||||
<br />
|
||||
Fork of {parentName}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
{#if $superadmin && workspace['deleted']}
|
||||
|
||||
Reference in New Issue
Block a user