replace popup with melt popover in settings pages

This commit is contained in:
Guilhem
2025-02-18 13:18:42 +00:00
parent f793080828
commit a6bad016bc
5 changed files with 745 additions and 724 deletions
@@ -7,7 +7,8 @@
import FolderEditor from '$lib/components/FolderEditor.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import { userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import { Button, Drawer, DrawerContent, Popup, Skeleton } from '$lib/components/common'
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import FolderInfo from '$lib/components/FolderInfo.svelte'
import FolderUsageInfo from '$lib/components/FolderUsageInfo.svelte'
import { sendUserToast } from '$lib/utils'
@@ -78,139 +79,137 @@
</DrawerContent>
</Drawer>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.folders}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.folders}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{:else}
<CenteredPage>
<PageHeader
title="Folders"
tooltip="Folders allow to group items such as scripts/flows/resources/schedule together and to grant homogenous RBAC permissions to groups and individual users towards them."
documentationLink="https://www.windmill.dev/docs/core_concepts/groups_and_folders"
>
<div class="flex flex-row">
<Popup
let:close
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New folder</Button>
</svelte:fragment>
<div class="flex flex-col gap-2">
<input
class="mr-2"
on:keyup={(e) => handleKeyUp(e, () => close(null))}
placeholder="New folder name"
bind:value={newFolderName}
/>
<CenteredPage>
<PageHeader
title="Folders"
tooltip="Folders allow to group items such as scripts/flows/resources/schedule together and to grant homogenous RBAC permissions to groups and individual users towards them."
documentationLink="https://www.windmill.dev/docs/core_concepts/groups_and_folders"
>
<div class="flex flex-row">
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}>
<svelte:fragment slot="trigger">
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New folder</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
<div class="flex flex-col gap-2 p-4">
<input
class="mr-2"
on:keyup={(e) => handleKeyUp(e, () => close())}
placeholder="New folder name"
bind:value={newFolderName}
/>
<div>
<Button
size="md"
startIcon={{ icon: Plus }}
disabled={!newFolderName}
on:click={() => {
addFolder()
close(null)
}}
>
Create
</Button>
</div>
</div>
</Popup>
</div>
</PageHeader>
<div>
<Button
size="md"
startIcon={{ icon: Plus }}
disabled={!newFolderName}
on:click={() => {
addFolder()
close()
}}
>
Create
</Button>
</div>
</div>
</svelte:fragment>
</Popover>
</div>
</PageHeader>
<div class="relative mb-20 pt-8">
<DataTable>
<Head>
<tr>
<Cell head first>Name</Cell>
<Cell head class="w-20">Scripts</Cell>
<Cell head class="w-20">Flows</Cell>
<Cell head class="w-20">Apps</Cell>
<Cell head class="w-20">Schedules</Cell>
<Cell head class="w-20">Variables</Cell>
<Cell head class="w-20">Resources</Cell>
<Cell head class="w-20">Participants</Cell>
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#if folders === undefined}
{#each new Array(4) as _}
<tr>
<td colspan="9">
<Skeleton layout={[[2]]} />
</td>
</tr>
{/each}
{:else}
{#if folders.length === 0}
<tr>
<td colspan="4" class="text-tertiary mt-2">No folders yet, create one!</td>
</tr>
{/if}
<div class="relative mb-20 pt-8">
<DataTable>
<Head>
<tr>
<Cell head first>Name</Cell>
<Cell head class="w-20">Scripts</Cell>
<Cell head class="w-20">Flows</Cell>
<Cell head class="w-20">Apps</Cell>
<Cell head class="w-20">Schedules</Cell>
<Cell head class="w-20">Variables</Cell>
<Cell head class="w-20">Resources</Cell>
<Cell head class="w-20">Participants</Cell>
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#if folders === undefined}
{#each new Array(4) as _}
<tr>
<td colspan="9">
<Skeleton layout={[[2]]} />
</td>
</tr>
{/each}
{:else}
{#if folders.length === 0}
<tr>
<td colspan="4" class="text-tertiary mt-2">No folders yet, create one!</td>
</tr>
{/if}
{#each folders as { name, extra_perms, owners, canWrite, summary } (name)}
<Row
hoverable
on:click={() => {
editFolderName = name
folderDrawer.openDrawer()
}}
>
<Cell first>
<span class="text-blue-500">{name}</span>
{#if summary}
<br />
<span class="text-gray-500">{summary}</span>
{/if}
</Cell>
<FolderUsageInfo {name} tabular />
{#each folders as { name, extra_perms, owners, canWrite, summary } (name)}
<Row
hoverable
on:click={() => {
editFolderName = name
folderDrawer.openDrawer()
}}
>
<Cell first>
<span class="text-blue-500">{name}</span>
{#if summary}
<br />
<span class="text-gray-500">{summary}</span>
{/if}
</Cell>
<FolderUsageInfo {name} tabular />
<Cell><FolderInfo members={computeMembers(owners, extra_perms)} /></Cell>
<Cell shouldStopPropagation>
<Dropdown
items={[
{
displayName: 'Manage folder',
icon: Pen,
disabled: !canWrite,
action: () => {
editFolderName = name
folderDrawer.openDrawer()
}
},
{
displayName: `Delete${canWrite ? '' : ' (require owner permissions)'}`,
icon: Trash,
type: 'delete',
disabled: !canWrite,
action: async () => {
try {
await FolderService.deleteFolder({
workspace: $workspaceStore ?? '',
name
})
} catch (e) {
sendUserToast(e.body, true)
loadFolders()
<Cell><FolderInfo members={computeMembers(owners, extra_perms)} /></Cell>
<Cell shouldStopPropagation>
<Dropdown
items={[
{
displayName: 'Manage folder',
icon: Pen,
disabled: !canWrite,
action: () => {
editFolderName = name
folderDrawer.openDrawer()
}
},
{
displayName: `Delete${canWrite ? '' : ' (require owner permissions)'}`,
icon: Trash,
type: 'delete',
disabled: !canWrite,
action: async () => {
try {
await FolderService.deleteFolder({
workspace: $workspaceStore ?? '',
name
})
} catch (e) {
sendUserToast(e.body, true)
loadFolders()
}
}
}
}
]}
/>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
</div>
</CenteredPage>
]}
/>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
</div>
</CenteredPage>
{/if}
@@ -4,7 +4,8 @@
import { GroupService } from '$lib/gen'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Drawer, DrawerContent, Popup, Skeleton } from '$lib/components/common'
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
import GroupEditor from '$lib/components/GroupEditor.svelte'
import GroupInfo from '$lib/components/GroupInfo.svelte'
@@ -40,11 +41,12 @@
}
}
function handleKeyUp(event: KeyboardEvent) {
function handleKeyUp(event: KeyboardEvent, close: () => void) {
const key = event.key
if (key === 'Enter') {
event.preventDefault()
addGroup()
close()
}
}
async function addGroup() {
@@ -73,160 +75,162 @@
</DrawerContent>
</Drawer>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.groups}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{:else}
<CenteredPage>
<PageHeader
title="Groups"
tooltip="Group users together to grant roles and homegenous permissions. Same users can be in many groups at the same time."
documentationLink="https://www.windmill.dev/docs/core_concepts/groups_and_folders"
>
<div class="flex flex-row">
<div>
<Popup
let:close
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New&nbsp;group</Button>
</svelte:fragment>
<div class="flex-col flex gap-2">
<input
class="mr-2"
on:keyup={handleKeyUp}
placeholder="New group name"
bind:value={newGroupName}
/>
<Button
size="md"
startIcon={{ icon: Plus }}
disabled={!newGroupName}
on:click={() => {
addGroup()
close(null)
}}
>
Create
</Button>
</div>
</Popup>
</div>
</div>
</PageHeader>
<div class="relative mb-20 pt-8">
<DataTable>
<Head>
<tr>
<Cell head first>Name</Cell>
<Cell head>Members</Cell>
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#if groups === undefined}
{#each new Array(4) as _}
<tr>
<td colspan="5">
<Skeleton layout={[[2]]} />
</td>
</tr>
{/each}
{:else}
{#each groups as { name, summary, extra_perms, canWrite } (name)}
<Row
hoverable
on:click={() => {
editGroupName = name
groupDrawer.openDrawer()
}}
>
<Cell first>
<div class="flex flex-row gap-2 justify-between">
<div>
<span class="text-blue-500">{name}</span>
{#if summary}
<br />
<span class="text-gray-500">{summary}</span>
{/if}
</div>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
</Cell>
<Cell>
<GroupInfo {name} />
</Cell>
<Cell>
<Dropdown
items={[
{
displayName: 'Manage group',
icon: Pen,
disabled: !canWrite,
action: () => {
editGroupName = name
groupDrawer.openDrawer()
}
},
{
displayName: 'Delete',
icon: Trash,
type: 'delete',
disabled: !canWrite,
action: async () => {
await GroupService.deleteGroup({ workspace: $workspaceStore ?? '', name })
loadGroups()
}
}
]}
/>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.groups}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{#if instanceGroups && instanceGroups.length > 0}
{:else}
<CenteredPage>
<PageHeader
title="Instance Groups"
tooltip="Instance Groups are managed by SCIM and are groups shared by every workspaces"
documentationLink="https://www.windmill.dev/docs/misc/saml_and_scim#scim"
/>
<div class="relative mb-20 pt-8">
<TableCustom>
<tr slot="header-row">
<th>Name</th>
<th>Members</th>
</tr>
<tbody slot="body">
{#each instanceGroups as { name, emails }}
<tr>
<td>
<a
href="#{name}"
title="Groups"
tooltip="Group users together to grant roles and homegenous permissions. Same users can be in many groups at the same time."
documentationLink="https://www.windmill.dev/docs/core_concepts/groups_and_folders"
>
<div class="flex flex-row">
<div>
<Popover
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="trigger">
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New&nbsp;group</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
<div class="flex-col flex gap-2 p-4">
<input
class="mr-2"
on:keyup={(e) => handleKeyUp(e, close)}
placeholder="New group name"
bind:value={newGroupName}
/>
<Button
size="md"
startIcon={{ icon: Plus }}
disabled={!newGroupName}
on:click={() => {
editGroupName = name
groupDrawer.openDrawer()
addGroup()
close()
}}
>{name}
</a>
</td>
<td>{emails?.length ?? 0} members</td>
</tr>
{/each}
>
Create
</Button>
</div>
</svelte:fragment>
</Popover>
</div>
</div>
</PageHeader>
<div class="relative mb-20 pt-8">
<DataTable>
<Head>
<tr>
<Cell head first>Name</Cell>
<Cell head>Members</Cell>
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#if groups === undefined}
{#each new Array(4) as _}
<tr>
<td colspan="5">
<Skeleton layout={[[2]]} />
</td>
</tr>
{/each}
{:else}
{#each groups as { name, summary, extra_perms, canWrite } (name)}
<Row
hoverable
on:click={() => {
editGroupName = name
groupDrawer.openDrawer()
}}
>
<Cell first>
<div class="flex flex-row gap-2 justify-between">
<div>
<span class="text-blue-500">{name}</span>
{#if summary}
<br />
<span class="text-gray-500">{summary}</span>
{/if}
</div>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
</Cell>
<Cell>
<GroupInfo {name} />
</Cell>
<Cell>
<Dropdown
items={[
{
displayName: 'Manage group',
icon: Pen,
disabled: !canWrite,
action: (e) => {
e?.stopPropagation()
editGroupName = name
groupDrawer.openDrawer()
}
},
{
displayName: 'Delete',
icon: Trash,
type: 'delete',
disabled: !canWrite,
action: async () => {
await GroupService.deleteGroup({ workspace: $workspaceStore ?? '', name })
loadGroups()
}
}
]}
/>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</TableCustom>
</DataTable>
</div>
{/if}
</CenteredPage>
{#if instanceGroups && instanceGroups.length > 0}
<PageHeader
title="Instance Groups"
tooltip="Instance Groups are managed by SCIM and are groups shared by every workspaces"
documentationLink="https://www.windmill.dev/docs/misc/saml_and_scim#scim"
/>
<div class="relative mb-20 pt-8">
<TableCustom>
<tr slot="header-row">
<th>Name</th>
<th>Members</th>
</tr>
<tbody slot="body">
{#each instanceGroups as { name, emails }}
<tr>
<td>
<a
href="#{name}"
on:click={() => {
editGroupName = name
groupDrawer.openDrawer()
}}
>{name}
</a>
</td>
<td>{emails?.length ?? 0} members</td>
</tr>
{/each}
</tbody>
</TableCustom>
</div>
{/if}
</CenteredPage>
{/if}
<style>
@@ -3,7 +3,8 @@
import { GroupService } from '$lib/gen'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Drawer, DrawerContent, Popup } from '$lib/components/common'
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import InstanceGroupEditor from '$lib/components/InstanceGroupEditor.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import TableCustom from '$lib/components/TableCustom.svelte'
@@ -48,29 +49,30 @@
<PageHeader title="Instance Groups">
<div class="flex flex-row">
<div>
<Popup
let:close
<Popover
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<svelte:fragment slot="trigger">
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New&nbsp;group</Button>
</svelte:fragment>
<div class="flex-col flex gap-2">
<input class="mr-2" placeholder="New instance group name" bind:value={newGroupName} />
<Button
size="md"
startIcon={{ icon: Plus }}
disabled={!newGroupName}
on:click={() => {
addInstanceGroup()
close(null)
}}
>
Create
</Button>
</div>
</Popup>
<svelte:fragment slot="content" let:close>
<div class="flex-col flex gap-2 p-4">
<input class="mr-2" placeholder="New instance group name" bind:value={newGroupName} />
<Button
size="md"
startIcon={{ icon: Plus }}
disabled={!newGroupName}
on:click={() => {
addInstanceGroup()
close()
}}
>
Create
</Button>
</div>
</svelte:fragment>
</Popover>
</div>
</div>
</PageHeader>
@@ -59,22 +59,14 @@
import HighlightCode from '$lib/components/HighlightCode.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
import {
ActionRow,
Button,
Popup,
Skeleton,
Tab,
Alert,
DrawerContent
} from '$lib/components/common'
import { ActionRow, Button, Skeleton, Tab, Alert, DrawerContent } from '$lib/components/common'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import FlowMetadata from '$lib/components/FlowMetadata.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import JobProgressBar from '$lib/components/jobs/JobProgressBar.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { goto } from '$lib/navigation'
import { sendUserToast } from '$lib/toast'
import { forLater } from '$lib/forLater'
@@ -89,7 +81,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import WorkflowTimeline from '$lib/components/WorkflowTimeline.svelte'
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
import Popover from '$lib/components/Popover.svelte'
import Tooltip from '$lib/components/meltComponents/Tooltip.svelte'
import HighlightTheme from '$lib/components/HighlightTheme.svelte'
import PreprocessedArgsDisplay from '$lib/components/runs/PreprocessedArgsDisplay.svelte'
import ExecutionDuration from '$lib/components/ExecutionDuration.svelte'
@@ -589,8 +581,8 @@
</Badge>
</Button>
{:else}
<Popup floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}>
<svelte:fragment slot="button">
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}>
<svelte:fragment slot="trigger">
<Button
title={`Re-start this flow from step ${selectedJobStep} (included). ${
!$enterpriseLicense
@@ -609,46 +601,48 @@
</Badge>
</Button>
</svelte:fragment>
<label class="block text-primary">
<div class="pb-1 text-sm text-secondary"
>{selectedJobStepType == 'forloop' ? 'From iteration #:' : 'From branch:'}</div
>
<div class="flex w-full">
{#if selectedJobStepType === 'forloop'}
<input
type="number"
min="0"
bind:value={branchOrIterationN}
class="!w-32 grow"
on:click|stopPropagation={() => {}}
/>
{:else}
<select
bind:value={branchOrIterationN}
class="!w-32 grow"
on:click|stopPropagation={() => {}}
>
{#each restartBranchNames as [branchIdx, branchName]}
<option value={branchIdx}>{branchName}</option>
{/each}
</select>
{/if}
<Button
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Restart flow"
on:click|once={() => {
restartFlow(job?.id, selectedJobStep, branchOrIterationN)
}}
<svelte:fragment slot="content">
<label class="block text-primary">
<div class="pb-1 text-sm text-secondary"
>{selectedJobStepType == 'forloop' ? 'From iteration #:' : 'From branch:'}</div
>
<ArrowRight size={18} />
</Button>
</div>
</label>
</Popup>
<div class="flex w-full">
{#if selectedJobStepType === 'forloop'}
<input
type="number"
min="0"
bind:value={branchOrIterationN}
class="!w-32 grow"
on:click|stopPropagation={() => {}}
/>
{:else}
<select
bind:value={branchOrIterationN}
class="!w-32 grow"
on:click|stopPropagation={() => {}}
>
{#each restartBranchNames as [branchIdx, branchName]}
<option value={branchIdx}>{branchName}</option>
{/each}
</select>
{/if}
<Button
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Restart flow"
on:click|once={() => {
restartFlow(job?.id, selectedJobStep, branchOrIterationN)
}}
>
<ArrowRight size={18} />
</Button>
</div>
</label>
</svelte:fragment>
</Popover>
{/if}
{/if}
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
@@ -755,14 +749,18 @@
<Badge color="indigo">Tag: {job.tag}</Badge>
</div>
{/if}
{#if !job.visible_to_owner}
<div
><Badge color="red"
>only visible to you <Tooltip
>The option to hide this run from the owner of this script or flow was activated</Tooltip
>
</Badge></div
>
{#if !job.visible_to_owner || true}
<div>
<Badge color="red">
only visible to you
<Tooltip useDefaultIcon>
<svelte:fragment slot="text">
The option to hide this run from the owner of this script or flow was
activated
</svelte:fragment>
</Tooltip>
</Badge>
</div>
{/if}
{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0}
{#each job?.['labels'] as label}
@@ -773,7 +771,7 @@
{/if}
{#if concurrencyKey}
<div>
<Popover notClickable>
<Tooltip notClickable>
<svelte:fragment slot="text">
This job has concurrency limits enabled with the key
<a
@@ -787,7 +785,7 @@
>
<Badge>Concurrency: {truncateRev(concurrencyKey, 20)}</Badge></a
>
</Popover>
</Tooltip>
</div>
{/if}
</div>
@@ -1,7 +1,8 @@
<script lang="ts">
import AssignableTags from '$lib/components/AssignableTags.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, Popup, Skeleton, Tab, Tabs } from '$lib/components/common'
import { Button, Skeleton, Tab, Tabs } from '$lib/components/common'
import Popover from '$lib/components/meltComponents/Popover.svelte'
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'
@@ -15,7 +16,13 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import WorkspaceGroup from '$lib/components/WorkerGroup.svelte'
import { WorkerService, type WorkerPing, ConfigService, SettingService } from '$lib/gen'
import { enterpriseLicense, superadmin, userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import {
enterpriseLicense,
superadmin,
userStore,
workspaceStore,
userWorkspaces
} from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { displayDate, groupBy, pluralize, truncate } from '$lib/utils'
import { AlertTriangle, LineChart, List, Plus, Search } from 'lucide-svelte'
@@ -272,167 +279,173 @@
</DrawerContent>
</Drawer>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.workers}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.workers}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
<p class="font-bold">Unauthorized</p>
<p>Page not available for operators</p>
</div>
{:else}
<CenteredPage>
<PageHeader
title="Workers"
tooltip="The workers are the dutiful servants that execute the jobs."
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>
{#if $superadmin}
<div class="flex flex-row-reverse w-full pb-2 items-center gap-4">
<div>
<AssignableTags
showWorkspaceRestriction
on:refresh={() => {
loadCustomTags()
}}
/>
</div>
<div>
<DefaultTags bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
</div>
<div>
<Button
size="xs"
color="dark"
startIcon={{
icon: LineChart
}}
on:click={() => {
queueMetricsDrawer?.openDrawer()
}}
>
Queue metrics
</Button>
</div>
<div>
<Button
size="xs"
color="dark"
startIcon={{
icon: List
}}
on:click={() => {
openSearchWithPrefilledText('!')
}}
>
Service logs
</Button>
</div>
</div>
{/if}
</PageHeader>
{#if workers != undefined}
{#if groupedWorkers.length == 0}
<p>No workers seem to be available</p>
{/if}
<div class="pt-4 pb-8 w-full flex justify-between items-center"
><h4
>{groupWorkers?.length} Worker Groups <Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>Worker groups are groups of workers that share a config and are meant to be identical.
Worker groups are meant to be used with tags. Tags can be assigned to scripts and flows
and can be seen as dedicated queues. Only the corresponding
</Tooltip></h4
>
<div />
{#if $superadmin}
<div class="flex flex-row items-center">
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<div class="flex items-center gap-2">
<Button
size="sm"
startIcon={{ icon: Plus }}
nonCaptureEvent
disabled={!$enterpriseLicense}
dropdownItems={$enterpriseLicense
? [
{
label: 'Copy groups config as YAML',
onClick: () => {
if (!workerGroups) {
return sendUserToast('No worker groups found', true)
}
const workersConfig = Object.entries(workerGroups).map(
([name, config]) => ({
name,
...config
})
)
navigator.clipboard.writeText(YAML.stringify(workersConfig))
sendUserToast('Worker groups config copied to clipboard as YAML')
}
},
{
label: 'Import groups config from YAML',
onClick: () => {
importConfigDrawer?.toggleDrawer?.()
}
}
]
: undefined}
>
<span class="hidden md:block"
>New group config {!$enterpriseLicense ? '(EE)' : ''}</span
>
<Tooltip light>
Worker Group configs are propagated to every workers in the worker group
</Tooltip>
</Button>
</div>
</svelte:fragment>
<div class="flex flex-col gap-2">
<input class="mr-2 h-full" placeholder="New group name" bind:value={newConfigName} />
{#if !$enterpriseLicense}
<div class="flex items-center whitespace-nowrap text-yellow-600 gap-2">
<AlertTriangle size={16} />
EE only
</div>
{/if}
<Button
size="sm"
startIcon={{ icon: Plus }}
disabled={!newConfigName || !$enterpriseLicense}
on:click={addConfig}
>
Create
</Button>
</div>
</Popup>
</div>
{/if}</div
<CenteredPage>
<PageHeader
title="Workers"
tooltip="The workers are the dutiful servants that execute the jobs."
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>
{#if $superadmin}
<div class="flex flex-row-reverse w-full pb-2 items-center gap-4">
<div>
<AssignableTags
showWorkspaceRestriction
on:refresh={() => {
loadCustomTags()
}}
/>
</div>
<div>
<DefaultTags bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
</div>
<div>
<Button
size="xs"
color="dark"
startIcon={{
icon: LineChart
}}
on:click={() => {
queueMetricsDrawer?.openDrawer()
}}
>
Queue metrics
</Button>
</div>
<div>
<Button
size="xs"
color="dark"
startIcon={{
icon: List
}}
on:click={() => {
openSearchWithPrefilledText('!')
}}
>
Service logs
</Button>
</div>
</div>
{/if}
</PageHeader>
{#if (groupedWorkers ?? []).length > 5}
<div class="flex gap-2 items-center">
<div class="text-secondary text-sm">Worker group:</div>
<AutoComplete
noInputStyles
items={groupedWorkers.map((x) => x[0])}
bind:selectedItem={selectedTab}
hideArrow={true}
inputClassName={'flex !font-gray-600 !font-primary !bg-surface-primary"'}
dropdownClassName="!text-sm !py-2 !rounded-sm !border-gray-200 !border !shadow-md"
className="!font-gray-600 !font-primary !bg-surface-primary"
/>
{#if workers != undefined}
{#if groupedWorkers.length == 0}
<p>No workers seem to be available</p>
{/if}
<!-- <select
<div class="pt-4 pb-8 w-full flex justify-between items-center"
><h4
>{groupWorkers?.length} Worker Groups <Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>Worker groups are groups of workers that share a config and are meant to be identical.
Worker groups are meant to be used with tags. Tags can be assigned to scripts and flows
and can be seen as dedicated queues. Only the corresponding
</Tooltip></h4
>
<div />
{#if $superadmin}
<div class="flex flex-row items-center">
<Popover
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="trigger">
<div class="flex items-center gap-2">
<Button
size="sm"
startIcon={{ icon: Plus }}
nonCaptureEvent
disabled={!$enterpriseLicense}
dropdownItems={$enterpriseLicense
? [
{
label: 'Copy groups config as YAML',
onClick: () => {
if (!workerGroups) {
return sendUserToast('No worker groups found', true)
}
const workersConfig = Object.entries(workerGroups).map(
([name, config]) => ({
name,
...config
})
)
navigator.clipboard.writeText(YAML.stringify(workersConfig))
sendUserToast('Worker groups config copied to clipboard as YAML')
}
},
{
label: 'Import groups config from YAML',
onClick: () => {
importConfigDrawer?.toggleDrawer?.()
}
}
]
: undefined}
>
<span class="hidden md:block"
>New group config {!$enterpriseLicense ? '(EE)' : ''}</span
>
<Tooltip light>
Worker Group configs are propagated to every workers in the worker group
</Tooltip>
</Button>
</div>
</svelte:fragment>
<svelte:fragment slot="content">
<div class="flex flex-col gap-2 p-4">
<input
class="mr-2 h-full"
placeholder="New group name"
bind:value={newConfigName}
/>
{#if !$enterpriseLicense}
<div class="flex items-center whitespace-nowrap text-yellow-600 gap-2">
<AlertTriangle size={16} />
EE only
</div>
{/if}
<Button
size="sm"
startIcon={{ icon: Plus }}
disabled={!newConfigName || !$enterpriseLicense}
on:click={addConfig}
>
Create
</Button>
</div>
</svelte:fragment>
</Popover>
</div>
{/if}</div
>
{#if (groupedWorkers ?? []).length > 5}
<div class="flex gap-2 items-center">
<div class="text-secondary text-sm">Worker group:</div>
<AutoComplete
noInputStyles
items={groupedWorkers.map((x) => x[0])}
bind:selectedItem={selectedTab}
hideArrow={true}
inputClassName={'flex !font-gray-600 !font-primary !bg-surface-primary"'}
dropdownClassName="!text-sm !py-2 !rounded-sm !border-gray-200 !border !shadow-md"
className="!font-gray-600 !font-primary !bg-surface-primary"
/>
<!-- <select
class="max-w-64"
bind:value={selectedTab}
on:change={() => {
@@ -448,221 +461,226 @@
</option>
{/each}
</select> -->
</div>
{:else}
<Tabs bind:selected={selectedTab}>
{#each groupedWorkers.map((x) => x[0]) 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]}
workers={worker_group[1]}
{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..."
autocomplete="off"
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>Last job</Cell>
<Cell head>Occupancy rate<br />(15s/5m/30m/ever)</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
first
colspan={(!config || config?.dedicated_worker == undefined) && $superadmin
? 11
: 9}
scope="colgroup"
class="bg-surface-secondary/30 !py-1 border-b !text-xs"
>
<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}
<span class="font-semibold ml-8">{workers?.length} Workers</span>
{/if}
</div>
</Cell>
</tr>
{#if workers}
{#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed, last_job_id, last_job_workspace_id, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, 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 last_job_id}
<a href={`/run/${last_job_id}?workspace=${last_job_workspace_id}`}>
View last job
</a>
<br />
(workspace {last_job_workspace_id})
{/if}
</Cell>
<Cell>
{displayOccupancyRate(occupancy_rate_15s)}/{displayOccupancyRate(
occupancy_rate_5m
)}/{displayOccupancyRate(occupancy_rate_30m)}/{displayOccupancyRate(
occupancy_rate
)}
</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(2) + ' 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}
</tbody>
</DataTable>
{/if}
{:else}
{@const worker_group = Object.entries(workerGroups ?? {})
.filter((x) => !groupedWorkers.some((y) => y[0] == x[0]))
.find((x) => x[0] == selectedTab)}
<Tabs bind:selected={selectedTab}>
{#each groupedWorkers.map((x) => x[0]) 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]}
workers={worker_group[1]}
{config}
on:reload={() => {
loadWorkerGroups()
}}
name={worker_group[0]}
config={worker_group[1]}
activeWorkers={0}
activeWorkers={activeWorkers?.length ?? 0}
{defaultTagPerWorkspace}
/>
<div class="text-xs text-tertiary"> No workers currently in this worker group </div>
<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..."
autocomplete="off"
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>Last job</Cell>
<Cell head>Occupancy rate<br />(15s/5m/30m/ever)</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
first
colspan={(!config || config?.dedicated_worker == undefined) && $superadmin
? 11
: 9}
scope="colgroup"
class="bg-surface-secondary/30 !py-1 border-b !text-xs"
>
<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}
<span class="font-semibold ml-8">{workers?.length} Workers</span>
{/if}
</div>
</Cell>
</tr>
{#if workers}
{#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed, last_job_id, last_job_workspace_id, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, 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 last_job_id}
<a href={`/run/${last_job_id}?workspace=${last_job_workspace_id}`}>
View last job
</a>
<br />
(workspace {last_job_workspace_id})
{/if}
</Cell>
<Cell>
{displayOccupancyRate(occupancy_rate_15s)}/{displayOccupancyRate(
occupancy_rate_5m
)}/{displayOccupancyRate(occupancy_rate_30m)}/{displayOccupancyRate(
occupancy_rate
)}
</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(2) + ' 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}
</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}
workers={worker_group[1]}
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}
{/if}
</div>
<div class="pb-20" />
<AutoscalingEvents worker_group={selectedTab} />
{:else}
<div class="flex flex-col">
{#each new Array(4) as _}
<Skeleton layout={[[8], 1]} />
{/each}
</div>
{/if}
</CenteredPage>
</div>
<div class="pb-20" />
<AutoscalingEvents worker_group={selectedTab} />
{:else}
<div class="flex flex-col">
{#each new Array(4) as _}
<Skeleton layout={[[8], 1]} />
{/each}
</div>
{/if}
</CenteredPage>
{/if}