mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 08:03:16 +00:00
feat: home table polish: run links, language in the path line, fork button sizing, row separators
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
42d5661573
commit
1c67ba47b2
@@ -31,11 +31,15 @@
|
||||
<!-- title on the wrapper, not on <Button>: Button renders its `title` prop only in the
|
||||
<button> branch, so the <a> branch taken here (href is set) would silently drop it.
|
||||
On the wrapper it also covers the icon and padding, not just the label text. -->
|
||||
<div title={label}>
|
||||
<!-- Like the row's Edit button: shown on hover, focus or keyboard selection. -->
|
||||
<div
|
||||
title={label}
|
||||
class="invisible group-hover/row:visible group-focus-within/row:visible group-data-[row-keyboard-selected=true]/row:visible"
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
wrapperClasses="max-w-56"
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
wrapperClasses="max-w-40"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: Pen }}
|
||||
{href}
|
||||
onClick={(e) => onEditInForkClick(e, itemType, path, { hasHref: true })}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
EMPTY_CELL_DASH,
|
||||
HOME_TABLE_BADGE_GRID,
|
||||
HOME_TABLE_GRID,
|
||||
HOME_TABLE_ROW_SEPARATOR,
|
||||
isHomeTable
|
||||
} from '$lib/components/home/homeTable'
|
||||
|
||||
@@ -78,7 +79,8 @@
|
||||
badges?: import('svelte').Snippet
|
||||
/** Badge groups the home table gives a column each; elsewhere they render
|
||||
* inline, in this order, after `badges`. */
|
||||
languageBadge?: import('svelte').Snippet
|
||||
/** Appended to the path line, e.g. a script's language. */
|
||||
pathSuffix?: string | undefined
|
||||
tagBadges?: import('svelte').Snippet
|
||||
draftBadge?: import('svelte').Snippet
|
||||
sharedBadge?: import('svelte').Snippet
|
||||
@@ -122,7 +124,7 @@
|
||||
workspaceId,
|
||||
depth = 0,
|
||||
badges,
|
||||
languageBadge,
|
||||
pathSuffix = undefined,
|
||||
tagBadges,
|
||||
draftBadge,
|
||||
sharedBadge,
|
||||
@@ -206,7 +208,8 @@
|
||||
data-row-keyboard-selected={keyboardSelected ? 'true' : undefined}
|
||||
class={twMerge(
|
||||
HOME_TABLE_GRID,
|
||||
'group/row relative w-full pl-5 pr-3 py-2.5 rounded-md',
|
||||
'group/row relative w-full pl-5 pr-3 py-2.5',
|
||||
HOME_TABLE_ROW_SEPARATOR,
|
||||
disabled ? 'opacity-25' : 'hover:bg-surface-hover',
|
||||
inSelectionMode ? 'cursor-pointer select-none' : '',
|
||||
rowSelection?.selected
|
||||
@@ -224,8 +227,12 @@
|
||||
class="absolute inset-0"
|
||||
></a>
|
||||
{/if}
|
||||
<!-- The kind icon, swapped for the checkbox on hover and in selection mode. -->
|
||||
<div class="relative z-[1] w-4 h-4">
|
||||
<!-- The kind icon, swapped for the checkbox on hover and in selection mode. In the
|
||||
tree it moves right by the same depth offset as the name, under its folder. -->
|
||||
<div
|
||||
class="relative z-[1] w-4 h-4"
|
||||
style={depth > 0 ? `transform: translateX(${depth * 16}px);` : ''}
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0',
|
||||
@@ -425,8 +432,14 @@
|
||||
'text-emphasis flex-wrap text-left text-xs min-w-0',
|
||||
homeTable ? 'font-normal' : 'font-semibold',
|
||||
titleBadge ? 'inline-flex items-center gap-2' : '',
|
||||
homeTable && !titleBadge ? 'line-clamp-2 break-words' : '',
|
||||
linked ? 'group-hover/row:underline decoration-gray-400' : ''
|
||||
)}
|
||||
title={homeTable && !customSummary
|
||||
? !summary || summary.length == 0
|
||||
? displayPath
|
||||
: summary
|
||||
: undefined}
|
||||
>
|
||||
{#if customSummary}
|
||||
{@render customSummary?.()}
|
||||
@@ -437,11 +450,6 @@
|
||||
{/if}
|
||||
{@render titleBadge?.()}
|
||||
</div>
|
||||
{#if !inlineBadges && languageBadge}
|
||||
<div class="flex items-center shrink-0">
|
||||
{@render languageBadge()}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !inlineBadges && tagBadges}
|
||||
<div class="relative z-[1] flex items-center gap-1 shrink-0">{@render tagBadges()}</div>
|
||||
{/if}
|
||||
@@ -452,10 +460,9 @@
|
||||
{#if homeTable}
|
||||
{@render inlineFavorite()}
|
||||
{/if}
|
||||
{#if inlineBadges && (badges || languageBadge || tagBadges || draftBadge || sharedBadge || labelBadges)}
|
||||
{#if inlineBadges && (badges || tagBadges || draftBadge || sharedBadge || labelBadges)}
|
||||
<div class="relative z-[1] hidden lg:flex flex-row gap-1 items-center shrink-0">
|
||||
{@render badges?.()}
|
||||
{@render languageBadge?.()}
|
||||
{@render tagBadges?.()}
|
||||
{@render draftBadge?.()}
|
||||
{@render sharedBadge?.()}
|
||||
@@ -469,6 +476,9 @@
|
||||
{:else}
|
||||
{path}
|
||||
{/if}
|
||||
{#if pathSuffix}
|
||||
<span class="opacity-75">· {pathSuffix}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
import { isDeployable } from '$lib/utils_deployable'
|
||||
|
||||
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
import { LanguageIcon } from '../languageIcons'
|
||||
import {
|
||||
Archive,
|
||||
Calendar,
|
||||
@@ -52,7 +51,7 @@
|
||||
import { Drawer, DrawerContent } from '..'
|
||||
import NoMainFuncBadge from '$lib/components/NoMainFuncBadge.svelte'
|
||||
import RowLabels from './RowLabels.svelte'
|
||||
import { isHomeTable } from '$lib/components/home/homeTable'
|
||||
import { defaultScriptLanguages } from '$lib/scripts'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
|
||||
@@ -104,7 +103,6 @@
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const iconSize = isHomeTable() ? 12 : 14
|
||||
|
||||
async function archiveScript(path: string): Promise<void> {
|
||||
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
|
||||
@@ -172,14 +170,12 @@
|
||||
workspaceId={$workspaceStore ?? ''}
|
||||
canFavorite={!script.draft_only}
|
||||
editedAt={script.created_at}
|
||||
pathSuffix={script.language
|
||||
? (defaultScriptLanguages[script.language] ?? script.language)
|
||||
: undefined}
|
||||
{depth}
|
||||
{rowSelection}
|
||||
>
|
||||
{#snippet languageBadge()}
|
||||
{#if script.language}
|
||||
<LanguageIcon lang={script.language} width={iconSize} height={iconSize} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet tagBadges()}
|
||||
{#if script.lock_error_logs}
|
||||
<Badge color="red" baseClass="border border-red-200">Deployment failed</Badge>
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
} from '$lib/gen'
|
||||
import { resource } from 'runed'
|
||||
import { getDraftItems } from '$lib/workspaceDrafts.svelte'
|
||||
import { disableHubStore, userStore, workspaceStore } from '$lib/stores'
|
||||
import { disableHubStore, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
|
||||
import { editInForkAllowed } from '$lib/utils/editInFork'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { useOperatorBuilderFlows } from '$lib/operatorWriteRights'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
import {
|
||||
@@ -67,7 +69,13 @@
|
||||
import BulkActionsBar from './BulkActionsBar.svelte'
|
||||
import { HomeSelection, setHomeSelection, toBulkItem } from './homeSelection.svelte'
|
||||
import { HomeActivity, setHomeActivity } from './homeActivity.svelte'
|
||||
import { HOME_TABLE_BADGE_GRID, HOME_TABLE_GRID, setHomeTable } from './homeTable'
|
||||
import {
|
||||
HOME_TABLE_BADGE_GRID,
|
||||
HOME_TABLE_GRID,
|
||||
HOME_TABLE_ROW_SEPARATOR,
|
||||
HOME_TABLE_WIDE_ACTIONS,
|
||||
setHomeTable
|
||||
} from './homeTable'
|
||||
import Checkbox from '../common/checkbox/Checkbox.svelte'
|
||||
|
||||
const operatorBuilderFlows = useOperatorBuilderFlows()
|
||||
@@ -1495,6 +1503,16 @@
|
||||
)
|
||||
: items
|
||||
)
|
||||
// A row the user cannot edit offers "Edit in fork", which needs a wider actions column.
|
||||
let tableStyle = $derived(
|
||||
!isCloudHosted() &&
|
||||
editInForkAllowed($workspaceStore, $userWorkspaces) &&
|
||||
((treeView ? treeSource : items) ?? []).some(
|
||||
(it) => !showEditButtons || !('canWrite' in it) || !it.canWrite
|
||||
)
|
||||
? HOME_TABLE_WIDE_ACTIONS
|
||||
: undefined
|
||||
)
|
||||
// Remount identity: only a *mode* change (workspace, selected owner, entering/leaving
|
||||
// search, label filter) restructures the tree, so only those key the {#key} remount.
|
||||
// A sort/archive/library/kind change keeps the same lazy tree and is applied in place
|
||||
@@ -2111,7 +2129,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if treeView}
|
||||
<div class="border rounded-md bg-surface-tertiary">
|
||||
<div class="border rounded-md bg-surface-tertiary" style={tableStyle}>
|
||||
{@render tableHeader()}
|
||||
<!-- Remount the tree on a MODE change only (treeKey = view/owner/search/label):
|
||||
expanded folders (their `opened` state is local to each TreeView) collapse
|
||||
@@ -2147,13 +2165,17 @@
|
||||
{/key}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md bg-surface-tertiary" class:wm-imported={justImported}>
|
||||
<div
|
||||
class="border rounded-md bg-surface-tertiary"
|
||||
class:wm-imported={justImported}
|
||||
style={tableStyle}
|
||||
>
|
||||
{@render tableHeader()}
|
||||
{#if filter === ''}
|
||||
{#each [...visiblePipelineFolders].sort() as folder (folder)}
|
||||
<a
|
||||
href="{base}/pipeline/{encodeURIComponent(folder)}"
|
||||
class="w-full inline-flex items-center gap-4 pl-5 pr-3 py-2 rounded-md hover:bg-surface-hover transition-colors text-sm"
|
||||
class="w-full inline-flex items-center gap-4 pl-5 pr-3 py-2 {HOME_TABLE_ROW_SEPARATOR} hover:bg-surface-hover transition-colors text-sm"
|
||||
>
|
||||
<NetworkIcon size={16} class="text-emerald-600 dark:text-emerald-400" />
|
||||
<span class="text-xs font-medium text-emphasis truncate">Pipeline · f/{folder}</span>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import { displayDate, formatCron } from '$lib/utils'
|
||||
import { describeSchedule } from '$lib/utils/describeCron'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getHomeActivity, type ActivityKind } from './homeActivity.svelte'
|
||||
|
||||
interface Props {
|
||||
@@ -88,10 +91,11 @@
|
||||
{#if homeActivity}
|
||||
<div class="relative z-[1] hidden lg:flex items-center w-24 min-w-0 shrink-0 text-xs">
|
||||
{#if lastRun}
|
||||
<Tooltip class="min-w-0">
|
||||
<a
|
||||
href="{base}/runs/{path}"
|
||||
class="flex items-center gap-1.5 min-w-0 text-secondary hover:text-primary"
|
||||
<!-- The close delay lets the pointer cross into the popup to click a run. -->
|
||||
<Tooltip class="min-w-0" closeDelay={150}>
|
||||
<!-- Not a link: the popover's "View runs" and run entries are the ways in. -->
|
||||
<span
|
||||
class="flex items-center gap-1.5 min-w-0 text-secondary cursor-default"
|
||||
aria-label="Latest runs"
|
||||
>
|
||||
<span class={twMerge('w-1.5 h-1.5 rounded-full shrink-0', statusClass[lastRun.status])}
|
||||
@@ -103,16 +107,29 @@
|
||||
<TimeAgo date={lastRun.created_at} compact /> ago
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
{#snippet text()}
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold">Latest {runs.length} runs</span>
|
||||
<div class="flex flex-col gap-1 min-w-56">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<span class="font-semibold">Latest {runs.length} runs</span>
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="xs"
|
||||
href="{base}/runs/{path}?workspace={$workspaceStore}"
|
||||
>
|
||||
View runs
|
||||
</Button>
|
||||
</div>
|
||||
{#each runs as run (run.id)}
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="{base}/run/{run.id}?workspace={$workspaceStore}"
|
||||
class="group/run -mx-1.5 px-1.5 py-0.5 rounded flex items-center gap-2 text-primary hover:bg-surface-hover"
|
||||
>
|
||||
<div class={twMerge('w-2 h-2 rounded-full', statusClass[run.status])}></div>
|
||||
<span class="w-16">{statusLabel[run.status]}</span>
|
||||
<span class="text-hint">{displayDate(run.created_at, true)}</span>
|
||||
</div>
|
||||
<span class="text-hint grow">{displayDate(run.created_at, true)}</span>
|
||||
<ExternalLink size={12} class="text-hint group-hover/run:text-primary shrink-0" />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import Item from './Item.svelte'
|
||||
import { countLeaves, type FolderItem, type ItemType, type UserItem } from './treeViewUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { isHomeTable } from './homeTable'
|
||||
import { HOME_TABLE_ROW_SEPARATOR, isHomeTable } from './homeTable'
|
||||
import { pluralize } from '$lib/utils'
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '$lib/components/common'
|
||||
@@ -74,7 +74,9 @@
|
||||
}: Props = $props()
|
||||
|
||||
// The home table draws no separators between its rows.
|
||||
const separator = isHomeTable() ? 'pl-5 rounded-md hover:bg-surface-hover' : 'border-b'
|
||||
const separator = isHomeTable()
|
||||
? `pl-5 hover:bg-surface-hover ${HOME_TABLE_ROW_SEPARATOR}`
|
||||
: 'border-b'
|
||||
|
||||
let visualDepth = $derived(depth + indent)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { HOME_TABLE_ROW_SEPARATOR } from './homeTable'
|
||||
import { untrack, type Snippet } from 'svelte'
|
||||
import TreeView from './TreeView.svelte'
|
||||
import { countLeaves, groupItems, type ItemType, type UserItem } from './treeViewUtils'
|
||||
@@ -287,7 +288,7 @@
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
onclick={toggleOtherUsers}
|
||||
class="pl-5 pr-3 py-2 rounded-md hover:bg-surface-hover w-full flex flex-row items-center justify-between cursor-pointer"
|
||||
class="pl-5 pr-3 py-2 hover:bg-surface-hover {HOME_TABLE_ROW_SEPARATOR} w-full flex flex-row items-center justify-between cursor-pointer"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<Users size={16} class="text-secondary" />
|
||||
|
||||
@@ -7,12 +7,19 @@ import { getContext, setContext } from 'svelte'
|
||||
* triggers and runs cells are hidden, and the template drops them too.
|
||||
*/
|
||||
export const HOME_TABLE_GRID =
|
||||
'grid items-center gap-x-5 grid-cols-[1rem_minmax(0,1fr)_6.5rem] lg:grid-cols-[1rem_minmax(0,2fr)_minmax(0,1.5fr)_6rem_13rem_5rem_6.5rem]'
|
||||
'grid items-center gap-x-5 grid-cols-[1rem_minmax(0,1fr)_var(--home-actions-w,6.5rem)] lg:grid-cols-[1rem_minmax(0,3fr)_minmax(0,1fr)_6rem_13rem_5rem_var(--home-actions-w,6.5rem)]'
|
||||
|
||||
/** Actions column width when some row shows "Edit in fork" instead of Edit. */
|
||||
export const HOME_TABLE_WIDE_ACTIONS = '--home-actions-w: 12.5rem'
|
||||
|
||||
/** Sub-columns of the badges area: labels · shared. */
|
||||
export const HOME_TABLE_BADGE_GRID =
|
||||
'grid-cols-[minmax(0,1fr)_1.25rem] gap-x-2 items-center min-w-0'
|
||||
|
||||
/** The faint line between home table rows; the last row in the box draws none. */
|
||||
export const HOME_TABLE_ROW_SEPARATOR =
|
||||
'border-b border-gray-100 dark:border-gray-700/40 last:border-b-0'
|
||||
|
||||
/** Shows a muted dash in a table cell that renders no element. */
|
||||
export const EMPTY_CELL_DASH =
|
||||
"[&:not(:has(*))]:after:content-['–'] [&:not(:has(*))]:after:text-hint [&:not(:has(*))]:after:text-2xs [&:not(:has(*))]:after:opacity-40"
|
||||
|
||||
Reference in New Issue
Block a user