feat(frontend): worker page improvements (#3921)

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): add search

* feat(frontend): fix style

* feat(frontend): display a select when the number of worker groups is above 5

* feat(frontend): add number of workers in the select

* fix(frontend): flatten header and rows

* feat(frontend): improve worker header design

* feat(frontend): improve worker header design

* feat(frontend): fix search

* feat(frontend): fix dark theme
This commit is contained in:
Faton Ramadani
2024-06-18 17:18:16 +02:00
committed by GitHub
parent 1baf50f6c3
commit ccc3308b4c
4 changed files with 269 additions and 141 deletions
@@ -24,7 +24,7 @@
<div class="flex flex-col w-80 p-2 gap-2">
{#if !$enterpriseLicense}
<div class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap ml-8">
<div class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap justify-end">
<AlertTriangle size={16} />
EE only <Tooltip>Enterprise Edition only feature</Tooltip>
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Plus, X } from 'lucide-svelte'
import { Copy, Plus, RefreshCcwIcon, Settings, Trash, X } from 'lucide-svelte'
import { Alert, Button, Drawer } from './common'
import Multiselect from 'svelte-multiselect'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
@@ -801,8 +801,7 @@
</DrawerContent>
</Drawer>
<div class="flex gap-2 items-center"
><h4 class="py-4 truncate w-40 text-primary">{name}</h4>
<div class="flex gap-2 items-center justify-end flex-row my-2">
{#if $superadmin}
<Button
color="light"
@@ -812,9 +811,10 @@
loadNConfig()
drawer.openDrawer()
}}
startIcon={{ icon: config == undefined ? Plus : Settings }}
>
<div class="flex flex-row gap-1 items-center">
{config == undefined ? 'create' : 'edit'} config
{config == undefined ? 'Create' : 'Edit'} config
</div>
</Button>
@@ -830,8 +830,9 @@
)
sendUserToast('Worker config copied to clipboard as YAML')
}}
startIcon={{ icon: Copy }}
>
copy config
Copy config
</Button>
{#if config}
@@ -845,9 +846,10 @@
openDelete = true
}
}}
startIcon={{ icon: Trash }}
btnClasses="text-red-400"
>
delete config
Delete config
</Button>
{/if}
@@ -860,8 +862,9 @@
openClean = true
}}
btnClasses="text-red-400"
startIcon={{ icon: RefreshCcwIcon }}
>
clean cache
Clean cache
</Button>
{:else if config}
<Button
@@ -875,9 +878,4 @@
<div class="flex flex-row gap-1 items-center"> config </div>
</Button>
{/if}
{#if activeWorkers > 1}
<span class="ml-4 text-xs"
>{activeWorkers} Workers <Tooltip>Number of workers active in the last 10s</Tooltip></span
>
{/if}
</div>
@@ -28,6 +28,7 @@
<span use:popperRef>
<MenuButton
class={twMerge('h-full w-full flex flex-row gap-2 items-center', hasPadding ? 'px-2' : '')}
>
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
@@ -1,7 +1,7 @@
<script lang="ts">
import AssignableTags from '$lib/components/AssignableTags.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Popup, Skeleton } from '$lib/components/common'
import { Button, Popup, Skeleton, Tab, Tabs } from '$lib/components/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
@@ -17,8 +17,8 @@
import { WorkerService, type WorkerPing, ConfigService } from '$lib/gen'
import { enterpriseLicense, superadmin } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { displayDate, groupBy, truncate } from '$lib/utils'
import { AlertTriangle, FileJson, LineChart, Plus } from 'lucide-svelte'
import { displayDate, groupBy, pluralize, truncate } from '$lib/utils'
import { AlertTriangle, FileJson, LineChart, Plus, Search } from 'lucide-svelte'
import { onDestroy, onMount } from 'svelte'
import YAML from 'yaml'
@@ -163,6 +163,47 @@
}
let queueMetricsDrawer: Drawer
let selectedTab: string = groupedWorkers?.[0]?.[0] ?? 'default'
let search: string = ''
$: worker_group = filterWorkerGroupByNames(
groupedWorkers?.find((x) => x?.[0] == selectedTab),
search
)
function filterWorkerGroupByNames(
worker_group: [string, [string, WorkerPing[]][]] | undefined,
search: string
): [string, [string, WorkerPing[]][]] | undefined {
if (!worker_group) {
return undefined
}
if (!search) {
return worker_group
}
if (search === '') {
return worker_group
}
const filteredWorkerGroup: [string, WorkerPing[]][] = worker_group[1]
.map(
([section, workers]) =>
[
section,
workers.filter(
(worker) =>
worker.worker.toLowerCase().includes(search.toLowerCase()) ||
worker.worker_instance.toLowerCase().includes(search.toLowerCase()) ||
worker.ip.toLowerCase().includes(search.toLowerCase())
)
] as [string, WorkerPing[]]
)
.filter(([section, workers]) => workers.length > 0)
return [worker_group[0], filteredWorkerGroup]
}
</script>
{#if $superadmin}
@@ -280,7 +321,8 @@
]
: undefined}
>
New group config
<span class="hidden md:block">New group config</span>
<Tooltip light>
Worker Group configs are propagated to every workers in the worker group
</Tooltip>
@@ -309,140 +351,227 @@
</div>
{/if}</div
>
{#each groupedWorkers as worker_group (worker_group[0])}
{@const config = (workerGroups ?? {})[worker_group[0]]}
{@const activeWorkers = worker_group?.[1].flatMap((x) =>
x[1]?.filter((y) => (y.last_ping ?? 0) < 15)
)}
<WorkspaceGroup
{customTags}
name={worker_group[0]}
{config}
on:reload={() => {
loadWorkerGroups()
}}
activeWorkers={activeWorkers?.length ?? 0}
{defaultTagPerWorkspace}
/>
<DataTable>
<Head>
<tr>
<Cell head first>Worker</Cell>
<Cell head>
<div class="flex flex-row items-center gap-1">
Worker Tags
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups#assign-custom-worker-groups"
>
If defined, the workers only pull jobs with the same corresponding tag
</Tooltip>
</div>
</Cell>
<Cell head>Last ping</Cell>
<Cell head>Worker start</Cell>
<Cell head>Jobs ran</Cell>
{#if (!config || config?.dedicated_worker == undefined) && $superadmin}
<Cell head>Current job</Cell>
<Cell head>Occupancy rate</Cell>
{/if}
<Cell head>Memory usage<br />(Windmill)</Cell>
<Cell head>Limits</Cell>
<Cell head>Version</Cell>
<Cell head last>Liveness</Cell>
</tr>
</Head>
<tbody>
{#each worker_group[1] as [section, workers]}
<tr class="border-t">
<Cell
first
colspan={(!config || config?.dedicated_worker == undefined) && $superadmin ? 11 : 9}
scope="colgroup"
class="bg-surface-secondary/60 py-2 border-b"
>
Instance: <Badge color="gray">{section?.split(splitter)?.[0]}</Badge>
IP: <Badge color="gray">{workers[0].ip}</Badge>
{#if workers?.length > 1}
{workers?.length} Workers
{#if Object.keys(workerGroups ?? {}).length > 5}
<div>
<select
class="max-w-64"
bind:value={selectedTab}
on:change={() => {
search = ''
}}
>
{#each Object.keys(workerGroups ?? {}) as name (name)}
<option value={name}
>{name} ({pluralize(
groupedWorkers.find((x) => x[0] == name)?.[1].length ?? 0,
'worker'
)})
</option>
{/each}
</select>
</div>
{:else}
<Tabs bind:selected={selectedTab}>
{#each Object.keys(workerGroups ?? {}) as name (name)}
{@const worker_group = groupedWorkers.find((x) => x[0] == name)}
{#if worker_group}
{@const activeWorkers = worker_group?.[1].flatMap((x) =>
x[1]?.filter((y) => (y.last_ping ?? 0) < 15)
)}
<Tab value={worker_group[0]}>
{`${worker_group[0]} - ${pluralize(activeWorkers?.length, 'worker')}`}
<Tooltip>Number of workers active in the last 15s</Tooltip>
</Tab>
{:else}
<Tab value={name}>
{name} (0 worker)
</Tab>
{/if}
{/each}
</Tabs>
{/if}
<div>
{#if worker_group}
{@const config = (workerGroups ?? {})[worker_group[0]]}
{@const activeWorkers = worker_group?.[1].flatMap((x) =>
x[1]?.filter((y) => (y.last_ping ?? 0) < 15)
)}
<WorkspaceGroup
{customTags}
name={worker_group[0]}
{config}
on:reload={() => {
loadWorkerGroups()
}}
activeWorkers={activeWorkers?.length ?? 0}
{defaultTagPerWorkspace}
/>
<div class="flex flex-row items-center gap-2 relative my-2">
<input
class="max-w-80 border rounded-md !pl-8"
placeholder="Search workers by name..."
bind:value={search}
/>
<Search class="absolute left-2 " size={14} />
</div>
{#if worker_group?.[1].length == 0 && search}
<div class="text-xs text-tertiary">
No workers found. Reset the search to see all workers.
</div>
{:else}
<DataTable>
<Head>
<tr>
<Cell head first>Worker</Cell>
<Cell head>
<div class="flex flex-row items-center gap-1">
Worker Tags
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups#assign-custom-worker-groups"
>
If defined, the workers only pull jobs with the same corresponding tag
</Tooltip>
</div>
</Cell>
<Cell head>Last ping</Cell>
<Cell head>Worker start</Cell>
<Cell head>Jobs ran</Cell>
{#if (!config || config?.dedicated_worker == undefined) && $superadmin}
<Cell head>Current job</Cell>
<Cell head>Occupancy rate</Cell>
{/if}
</Cell>
</tr>
{#if workers}
{#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed, current_job_id, current_job_workspace_id, occupancy_rate, wm_version, vcpus, memory, memory_usage, wm_memory_usage }}
<tr>
<Cell first>{worker}</Cell>
<Cell>
{#if custom_tags && custom_tags?.length > 2}
{truncate(custom_tags?.join(', ') ?? '', 10)}
<Tooltip>{custom_tags?.join(', ')}</Tooltip>
{:else}
{custom_tags?.join(', ') ?? ''}
{/if}
</Cell>
<Cell>{last_ping != undefined ? last_ping + timeSinceLastPing : -1}s ago</Cell>
<Cell>{displayDate(started_at)}</Cell>
<Cell>{jobs_executed}</Cell>
{#if (!config || config?.dedicated_worker == undefined) && $superadmin}
<Cell>
{#if current_job_id}
<a href={`/run/${current_job_id}?workspace=${current_job_workspace_id}`}>
View job
</a>
<br />
(workspace {current_job_workspace_id})
{/if}
</Cell>
<Cell>
{Math.ceil(occupancy_rate ?? 0 * 100)}%
</Cell>
{/if}
<Cell head>Memory usage<br />(Windmill)</Cell>
<Cell head>Limits</Cell>
<Cell head>Version</Cell>
<Cell head last>Liveness</Cell>
</tr>
</Head>
<tbody class="divide-y">
{#each worker_group[1] as [section, workers]}
<tr class="border-t">
<Cell
>{memory_usage ? Math.round(memory_usage / 1024 / 1024) + 'MB' : '--'}<br
/>({wm_memory_usage
? Math.round(wm_memory_usage / 1024 / 1024) + 'MB'
: '--'})</Cell
first
colspan={(!config || config?.dedicated_worker == undefined) && $superadmin
? 11
: 9}
scope="colgroup"
class="bg-surface-secondary/30 !py-1 border-b !text-xs"
>
<Cell>
{vcpus ? (vcpus / 100000).toFixed(1) + ' vCPUs' : '--'}
/ {memory ? Math.round(memory / 1024 / 1024) + 'MB' : '--'}
</Cell>
<Cell>
<div class="!text-2xs">
{wm_version.split('-')[0]}<Tooltip>{wm_version}</Tooltip>
<div class="flex flex-row w-full">
<div class="min-w-64">
Host:
<span class="font-semibold">{section?.split(splitter)?.[0]}</span>
</div>
<span class="ml-4">IP: </span>
<span class="font-semibold">{workers[0].ip}</span>
{#if workers?.length > 1}
{workers?.length} Workers
{/if}
</div>
</Cell>
<Cell last>
<Badge
color={last_ping != undefined ? (last_ping < 60 ? 'green' : 'red') : 'gray'}
>
{last_ping != undefined ? (last_ping < 60 ? 'Alive' : 'Dead') : 'Unknown'}
</Badge>
</Cell>
</tr>
{#if workers}
{#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed, current_job_id, current_job_workspace_id, occupancy_rate, wm_version, vcpus, memory, memory_usage, wm_memory_usage }}
<tr>
<Cell first>{worker}</Cell>
<Cell>
{#if custom_tags && custom_tags?.length > 2}
{truncate(custom_tags?.join(', ') ?? '', 10)}
<Tooltip>{custom_tags?.join(', ')}</Tooltip>
{:else}
{custom_tags?.join(', ') ?? ''}
{/if}
</Cell>
<Cell>{last_ping != undefined ? last_ping + timeSinceLastPing : -1}s ago</Cell
>
<Cell>{displayDate(started_at)}</Cell>
<Cell>{jobs_executed}</Cell>
{#if (!config || config?.dedicated_worker == undefined) && $superadmin}
<Cell>
{#if current_job_id}
<a
href={`/run/${current_job_id}?workspace=${current_job_workspace_id}`}
>
View job
</a>
<br />
(workspace {current_job_workspace_id})
{/if}
</Cell>
<Cell>
{Math.ceil(occupancy_rate ?? 0 * 100)}%
</Cell>
{/if}
<Cell>
<div class="flex flex-col gap-1">
<div>
{memory_usage ? Math.round(memory_usage / 1024 / 1024) + 'MB' : '--'}
</div>
<div>
({wm_memory_usage
? Math.round(wm_memory_usage / 1024 / 1024) + 'MB'
: '--'})
</div>
</div>
</Cell>
<Cell>
<div class="flex flex-col gap-1">
<div>
{vcpus ? (vcpus / 100000).toFixed(1) + ' vCPUs' : '--'}
</div>
<div>
{memory ? Math.round(memory / 1024 / 1024) + 'MB' : '--'}
</div>
</div>
</Cell>
<Cell>
<div class="!text-2xs">
{wm_version.split('-')[0]}<Tooltip>{wm_version}</Tooltip>
</div>
</Cell>
<Cell last>
<Badge
color={last_ping != undefined
? last_ping < 60
? 'green'
: 'red'
: 'gray'}
>
{last_ping != undefined ? (last_ping < 60 ? 'Alive' : 'Dead') : 'Unknown'}
</Badge>
</Cell>
</tr>
{/each}
{/if}
{/each}
{/if}
{/each}
</tbody>
</DataTable>
<div class="pb-4" />
{/each}
</tbody>
</DataTable>
{/if}
{:else}
{@const worker_group = Object.entries(workerGroups ?? {})
.filter((x) => !groupedWorkers.some((y) => y[0] == x[0]))
.find((x) => x[0] == selectedTab)}
{#if worker_group}
<WorkspaceGroup
{customTags}
on:reload={() => {
loadWorkerGroups()
}}
name={worker_group[0]}
config={worker_group[1]}
activeWorkers={0}
/>
<div class="text-xs text-tertiary"> No workers currently in this worker group </div>
{/if}
{/if}
</div>
<div class="pb-4" />
{#each Object.entries(workerGroups ?? {}).filter((x) => !groupedWorkers.some((y) => y[0] == x[0])) as worker_group (worker_group[0])}
<WorkspaceGroup
{customTags}
on:reload={() => {
loadWorkerGroups()
}}
name={worker_group[0]}
config={worker_group[1]}
activeWorkers={0}
/>
<div class="text-xs text-tertiary"> No workers currently in this worker group </div>
{/each}
{:else}
<div class="flex flex-col">
{#each new Array(4) as _}