Files
windmill/frontend/src/lib/components/common/table/AppRow.svelte
T
Guilhem 484fdfa2bc fix(frontend): update workers page ui (#7264)
* Improve workers page

* Update group config drawer

* improve dirty workergroup config

* Make layout reactive

* fix section animation

* prevent opening dropdown while clicking New group config

* migrate workers page to svelte 5

* Open drawer upon adding a worker group

* nit critical alert table

* improve queue metrics drawer

* improve agent worker drawer

* harmonize copy icon

* improve agent worker doc

* improve layout

* Improve autoscaling event list

* Improve tags managment

* Remove default tags

* fix npm check

* Add info for agent workers

* improve agent worker jwt token creation

* Improve token display

* nit

* improve tag display

* create EE component

* nit

* harmonize tag overflow

* handle permission better

* improve env var presets

* handle permission for config

* nit alerts

* nit

* Improve custom tag creation in tag select

* optimistic tag addition

* nit

* nit

* fix typo

* improve workers table

* Group config tags

* show mismatch

* fix typo

* optimistic update when adding tag

* do not allow to create tag when picking a tag to watch in alerts
2025-12-04 22:09:25 +00:00

292 lines
7.9 KiB
Svelte

<script lang="ts">
import { base } from '$lib/base'
import Dropdown from '$lib/components/DropdownV2.svelte'
import type MoveDrawer from '$lib/components/MoveDrawer.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import type ShareModal from '$lib/components/ShareModal.svelte'
import { AppService, DraftService, type ListableApp } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
import DraftBadge from '$lib/components/DraftBadge.svelte'
import Badge from '../badge/Badge.svelte'
import {
ExternalLink,
Eye,
File,
FileJson,
FolderOpen,
GitFork,
ChevronUpSquare,
History,
Pen,
Share,
Trash,
Copy
} from 'lucide-svelte'
import { goto as gotoUrl } from '$app/navigation'
import { page } from '$app/stores'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import { DELETE, copyToClipboard } from '$lib/utils'
import AppDeploymentHistory from '$lib/components/apps/editor/AppDeploymentHistory.svelte'
import { isDeployable } from '$lib/utils_deployable'
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
interface Props {
app: ListableApp & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean }
marked: string | undefined
starred: boolean
shareModal: ShareModal
moveDrawer: MoveDrawer
deploymentDrawer: DeployWorkspaceDrawer
deleteConfirmedCallback: (() => void) | undefined
depth?: number
menuOpen?: boolean
}
let {
app,
marked,
starred,
shareModal,
moveDrawer,
deploymentDrawer,
deleteConfirmedCallback = $bindable(),
depth = 0,
menuOpen = $bindable(false)
}: Props = $props()
const dispatch = createEventDispatcher()
let appExport: { open: (path: string) => void } | undefined = $state(undefined)
let appDeploymentHistory: AppDeploymentHistory | undefined = $state(undefined)
async function loadAppJson() {
appExport?.open(app.path)
}
</script>
{#if menuOpen}
{#await import('$lib/components/apps/editor/AppJsonEditor.svelte') then Module}
<Module.default on:change bind:this={appExport} />
{/await}
<AppDeploymentHistory bind:this={appDeploymentHistory} appPath={app.path} />
{/if}
<Row
href="{base}/apps{app.raw_app ? '_raw' : ''}/get/{app.path}"
kind="app"
{marked}
path={app.path}
summary={app.summary}
workspaceId={app.workspace_id ?? $workspaceStore ?? ''}
{starred}
on:change
canFavorite={!app.draft_only}
{depth}
>
{#snippet badges()}
{#if app.execution_mode == 'anonymous'}
<Badge small icon={{ icon: Eye }}>Public</Badge>
{/if}
{#if app.raw_app}
<Badge small icon={{ icon: FileJson }}>Raw</Badge>
{/if}
<SharedBadge canWrite={app.canWrite} extraPerms={app.extra_perms} />
<DraftBadge has_draft={app.has_draft} draft_only={app.draft_only} />
<div class="w-8 center-center"></div>
{/snippet}
{#snippet actions()}
<span class="hidden md:inline-flex gap-x-1">
{#if !$userStore?.operator}
{#if app.canWrite}
<div>
<Button
aiId={`edit-app-button-${app.summary?.length > 0 ? app.summary : app.path}`}
aiDescription={`Edits the app ${app.summary?.length > 0 ? app.summary : app.path}`}
variant="subtle"
wrapperClasses="w-20"
startIcon={{ icon: Pen }}
href="{base}/apps{app.raw_app ? '_raw' : ''}/edit/{app.path}?nodraft=true"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
aiId={`fork-app-button-${app.summary?.length > 0 ? app.summary : app.path}`}
aiDescription={`Fork the app ${app.summary?.length > 0 ? app.summary : app.path}`}
variant="subtle"
wrapperClasses="w-20"
unifiedSize="md"
startIcon={{ icon: GitFork }}
href="{base}/apps{app.raw_app ? '_raw' : ''}/add?template={app.path}"
>
Fork
</Button>
</div>
{/if}
{/if}
</span>
<Dropdown
aiId={`app-row-dropdown-${app.summary?.length > 0 ? app.summary : app.path}`}
aiDescription={`Open dropdown for app ${app.summary?.length > 0 ? app.summary : app.path} options`}
items={async () => {
let { draft_only, canWrite, summary, execution_mode, path, has_draft } = app
if (draft_only) {
return [
{
displayName: 'Delete',
icon: Trash,
action: async (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
await AppService.deleteApp({ workspace: $workspaceStore ?? '', path })
dispatch('change')
} else {
deleteConfirmedCallback = async () => {
await AppService.deleteApp({ workspace: $workspaceStore ?? '', path })
dispatch('change')
}
}
},
type: 'delete',
disabled: !canWrite,
hide: $userStore?.operator
},
{
displayName: $userStore?.operator ? 'View JSON' : 'View/Edit JSON',
icon: File,
action: () => {
loadAppJson()
}
}
]
}
return [
{
displayName: 'Duplicate/Fork',
icon: GitFork,
href: `${base}/apps${app.raw_app ? '_raw' : ''}/add?template=${path}`,
hide: $userStore?.operator
},
{
displayName: 'Move/Rename',
icon: FolderOpen,
action: () => {
moveDrawer.openDrawer(path, summary, 'app')
},
disabled: !canWrite,
hide: $userStore?.operator
},
...(isDeployable('app', path, await getDeployUiSettings())
? [
{
displayName: 'Deploy to staging/prod',
icon: ChevronUpSquare,
action: () => {
deploymentDrawer.openDrawer(path, 'app')
},
hide: $userStore?.operator
}
]
: []),
{
displayName: $userStore?.operator ? 'View JSON' : 'View/Edit JSON',
icon: FileJson,
action: () => {
loadAppJson()
}
},
{
displayName: 'Deployments',
icon: History,
action: () => appDeploymentHistory?.open(),
hide: $userStore?.operator
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: Share,
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'app')
},
hide: $userStore?.operator
},
{
displayName: 'Copy path',
icon: Copy,
action: () => {
copyToClipboard(path)
}
},
...(execution_mode == 'anonymous'
? [
{
displayName: 'Go to public page',
icon: ExternalLink,
action: async () => {
let secretUrl = await AppService.getPublicSecretOfApp({
workspace: $workspaceStore ?? '',
path
})
let url =
$page.url.protocol +
'//' +
`${$page.url.hostname}/public/${$workspaceStore}/${secretUrl}`
gotoUrl(url)
}
}
]
: []),
...(has_draft
? [
{
displayName: 'Delete Draft',
icon: Trash,
action: async () => {
await DraftService.deleteDraft({
workspace: $workspaceStore ?? '',
path,
kind: 'app'
})
dispatch('change')
},
type: DELETE,
disabled: !canWrite,
hide: $userStore?.operator
}
]
: []),
{
displayName: 'Delete',
icon: Trash,
action: async (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
await AppService.deleteApp({ workspace: $workspaceStore ?? '', path })
dispatch('change')
} else {
deleteConfirmedCallback = async () => {
await AppService.deleteApp({ workspace: $workspaceStore ?? '', path })
dispatch('change')
}
}
},
type: 'delete',
disabled: !canWrite,
hide: $userStore?.operator
}
]
}}
on:open={() => {
menuOpen = true
}}
/>
{/snippet}
</Row>