mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
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:
@@ -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 _}
|
||||
|
||||
Reference in New Issue
Block a user