mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
feat: the runs page's own header row moves into the page header
The title, the queue counters, the kind and status filters, the schedule and future-job switches, the search field and the timeframe picker all ride the band now, and the page starts at the chart — one row of vertical space back. The two toggle groups appear from 1500px: below that the bar cannot hold them next to the search field, and the same filtering is reachable from the search field's own kind and status chips. The search field is what gives width back, and the breadcrumb yields last, so a page's own name stays whole while the workspace and fork names truncate. The queue counters keep their icon form at every width, since the bar has room for two counters and never for the two labelled blocks. Their counts hang outside their icons, so the group carries the margins that keep a badge off its neighbour, and the suspended counter answers a hover at zero rather than sitting there as a dead icon. The band is 44px rather than 40, and a page can hand the breadcrumb a hint — the documentation tooltip that belongs with the page's name rather than with its buttons. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
559ffb7285
commit
4293b7683a
@@ -49,14 +49,18 @@ same weight, size and icon size, so the line reads as one control rather than fo
|
||||
type WorkspaceItem
|
||||
} from '$lib/components/workspacePicker'
|
||||
import type { PageHeaderItem, PageHeaderSection } from './pageHeaderRegistry.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
let {
|
||||
item,
|
||||
section,
|
||||
hint,
|
||||
actingWorkspaceId
|
||||
}: {
|
||||
item?: PageHeaderItem
|
||||
section?: PageHeaderSection
|
||||
/** A page's own mark next to its name, such as its documentation tooltip. */
|
||||
hint?: Snippet
|
||||
/** The workspace the page acts on, when it differs from the one the app is pointed at. */
|
||||
actingWorkspaceId?: string
|
||||
} = $props()
|
||||
@@ -296,11 +300,15 @@ same weight, size and icon size, so the line reads as one control rather than fo
|
||||
>
|
||||
{#if section.content}{@render section.content()}{:else}{section.label}{/if}
|
||||
</span>
|
||||
{#if hint}{@render hint()}{/if}
|
||||
{:else if routePage}
|
||||
{@const RouteIcon = routePage.icon}
|
||||
<span class="{SEGMENT} hover:bg-transparent hover:text-primary" aria-current="page">
|
||||
<!-- shrink-0: a page's own name is short and is the one part of the trail worth keeping
|
||||
whole, so the workspace and fork names give way first when the bar is full. -->
|
||||
<span class="{SEGMENT} shrink-0 hover:bg-transparent hover:text-primary" aria-current="page">
|
||||
{#if RouteIcon}<RouteIcon size={ICON} class="flex-shrink-0 text-tertiary" />{/if}
|
||||
<span class="truncate">{routePage.label}</span>
|
||||
</span>
|
||||
{#if hint}{@render hint()}{/if}
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
@@ -32,7 +32,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
|
||||
<div
|
||||
data-page-header
|
||||
class="flex items-center gap-1 h-10 px-2 shrink-0 min-w-0 bg-surface border-b {showBorder
|
||||
class="flex items-center gap-1 h-11 px-2 shrink-0 min-w-0 bg-surface border-b {showBorder
|
||||
? ''
|
||||
: 'border-transparent'}"
|
||||
>
|
||||
@@ -54,7 +54,17 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<NavBreadcrumb {item} {section} actingWorkspaceId={content?.actingWorkspaceId} />
|
||||
<!-- The breadcrumb yields width grudgingly (shrink-[0.1]): when a page fills the bar with
|
||||
controls, they are what should narrow, not the name of where the user is. It still gives
|
||||
way rather than pushing them off the bar once there is nothing left to take. -->
|
||||
<div class="flex min-w-0 shrink-[0.1]">
|
||||
<NavBreadcrumb
|
||||
{item}
|
||||
{section}
|
||||
hint={content?.hint}
|
||||
actingWorkspaceId={content?.actingWorkspaceId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if item && (item.summaryContent || item.summary)}
|
||||
<!-- A dot rather than a slash: the summary names the same item the path just located, it is
|
||||
@@ -72,7 +82,9 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
{/if}
|
||||
|
||||
{#if actions.length > 0}
|
||||
<div class="ml-auto flex items-center gap-2 shrink-0">
|
||||
<!-- min-w-0, not shrink-0: a page whose actions are a filter row (Runs) puts a control in
|
||||
here that can give width back, and it can only do that if this box may shrink. -->
|
||||
<div class="ml-auto flex items-center gap-2 min-w-0 pl-4">
|
||||
{#each actions as entry, i (i)}
|
||||
{#key entry.contexts}
|
||||
<ContextBridge contexts={entry.contexts}>
|
||||
|
||||
@@ -13,6 +13,7 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
let {
|
||||
item,
|
||||
section,
|
||||
hint,
|
||||
actions,
|
||||
contexts,
|
||||
barPlacement,
|
||||
@@ -25,6 +26,7 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
const id = pageHeader.register(() => ({
|
||||
item,
|
||||
section,
|
||||
hint,
|
||||
actions,
|
||||
contexts,
|
||||
barPlacement,
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import RunsQueue from '$lib/components/runs/RunsQueue.svelte'
|
||||
import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import { pageHref, RUNS_PATH } from '$lib/components/sessions/previewPaths'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { computeJobKinds, useJobsLoader } from '$lib/components/runs/useJobsLoader.svelte'
|
||||
@@ -584,6 +585,222 @@
|
||||
let resolutionNote = $state('')
|
||||
</script>
|
||||
|
||||
{#snippet runsHint()}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs"
|
||||
>
|
||||
All past and schedule executions of scripts and flows, including previews. You only see your own
|
||||
runs or runs of groups you belong to unless you are an admin.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
|
||||
{#snippet headerActions()}
|
||||
<!-- Always the icon form: the bar has room for two counters, never for the two labelled
|
||||
blocks. pr-2 on top of the row's own gap because the counts hang outside their icons,
|
||||
so without it the badge butts against the first filter. -->
|
||||
<div class="flex items-center pr-2">
|
||||
<RunsQueue
|
||||
success={filters.val.status ?? null}
|
||||
{queue_count}
|
||||
{suspended_count}
|
||||
onJobsWaiting={() => {
|
||||
jobsFilter('waiting')
|
||||
}}
|
||||
onJobsSuspended={() => {
|
||||
jobsFilter('suspended')
|
||||
}}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
<!-- The two groups are the widest thing in the bar: they appear only once the bar is wide
|
||||
enough to hold them next to the search field, below which the same filtering is reachable
|
||||
from the search field's own `kind` and `status` chips. -->
|
||||
<div class="hidden min-[1500px]:flex gap-2">
|
||||
<ToggleButtonGroup
|
||||
tabListClass="hidden min-[1500px]:flex"
|
||||
bind:selected={
|
||||
() => filters.val.job_kinds ?? 'runs',
|
||||
(v) => (v === 'runs' ? delete filters.val.job_kinds : (filters.val.job_kinds = v))
|
||||
}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton size="sm" value="all" label="All" {item} />
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value="runs"
|
||||
label="Runs"
|
||||
tooltip="Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they start), they have been triggered through the UI, a schedule or webhook"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value="dependencies"
|
||||
label="Deps"
|
||||
tooltip="Deploying a script, flow or an app launch a dependency job that create and then attach the lockfile to the deployed item. This mechanism ensure that logic is always executed with the exact same direct and indirect dependencies."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButtonMore
|
||||
small
|
||||
hideSelectedOption={innerWidth < smallScreenWidth}
|
||||
togglableItems={[
|
||||
{
|
||||
label: 'Previews',
|
||||
value: 'previews',
|
||||
tooltip: "Previews are jobs that have been started in the editor as 'Tests'"
|
||||
},
|
||||
{
|
||||
label: 'Sync',
|
||||
value: 'deploymentcallbacks',
|
||||
tooltip:
|
||||
'Sync jobs that are triggered on every script deployment to sync the workspace with the Git repository configured in the the workspace settings'
|
||||
}
|
||||
]}
|
||||
{item}
|
||||
bind:selected={filters.val.job_kinds}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => filters.val.status ?? 'all',
|
||||
(v) => (v === 'all' ? delete filters.val.status : (filters.val.status = v))
|
||||
}
|
||||
id="status"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton size="sm" value={'all'} label="All" {item} />
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value={'running'}
|
||||
tooltip="Running"
|
||||
class="whitespace-nowrap"
|
||||
icon={CirclePlay}
|
||||
iconProps={{
|
||||
class:
|
||||
'group-data-[state=on]:text-yellow-600 dark:group-data-[state=on]:text-yellow-400'
|
||||
}}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value={'success'}
|
||||
tooltip="Success"
|
||||
class="whitespace-nowrap"
|
||||
icon={CircleCheck}
|
||||
iconProps={{
|
||||
class: 'group-data-[state=on]:text-green-500 dark:group-data-[state=on]:text-green-300'
|
||||
}}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value={'failure'}
|
||||
tooltip="Failure"
|
||||
class="whitespace-nowrap"
|
||||
icon={CircleAlert}
|
||||
iconProps={{
|
||||
class: 'group-data-[state=on]:text-red-500 dark:group-data-[state=on]:text-red-300'
|
||||
}}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value={'canceled'}
|
||||
tooltip="Canceled"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
selectedColor="gray"
|
||||
{item}
|
||||
/>
|
||||
{#if filters.val.status == 'waiting'}
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value={'waiting'}
|
||||
tooltip="Waiting"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
selectedColor="blue"
|
||||
{item}
|
||||
/>
|
||||
{:else if filters.val.status == 'suspended'}
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value={'suspended'}
|
||||
tooltip="Suspended"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
selectedColor="purple"
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="hidden xl:flex gap-2 items-center">
|
||||
{#if !filters.val.job_trigger_kind || filters.val.job_trigger_kind === '!schedule'}
|
||||
<div class="flex items-center gap-1" title="Show schedules">
|
||||
<Toggle
|
||||
size="xs"
|
||||
id="show-schedules"
|
||||
bind:checked={
|
||||
() => filters.val.job_trigger_kind !== '!schedule',
|
||||
(v) =>
|
||||
v ? delete filters.val.job_trigger_kind : (filters.val.job_trigger_kind = '!schedule')
|
||||
}
|
||||
/>
|
||||
<Calendar size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-1" title="Show future jobs">
|
||||
<Toggle
|
||||
size="xs"
|
||||
id="show-future-jobs"
|
||||
bind:checked={
|
||||
() => filters.val.show_future_jobs !== false,
|
||||
(v) => (v ? delete filters.val.show_future_jobs : (filters.val.show_future_jobs = false))
|
||||
}
|
||||
/>
|
||||
<Clock size={14} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The one control in the band that gives way: everything else keeps its width and the
|
||||
search field takes what is left, down to a width that still shows a filter chip. -->
|
||||
<FilterSearchbar
|
||||
class={twMerge(
|
||||
'relative flex-1 min-w-[8rem]',
|
||||
Object.keys(filters.val).length <= 3 ? 'max-w-[20rem]' : 'max-w-[26rem]',
|
||||
ButtonType.UnifiedMinHeightClasses.sm
|
||||
)}
|
||||
schema={runsFilterSearchbarSchema}
|
||||
presets={buildRunsFilterPresets({
|
||||
isSuperAdminOrDevops: !!$superadmin || !!$devopsRole,
|
||||
isAdminsWorkspace: $workspaceStore === 'admins'
|
||||
})}
|
||||
bind:value={filters.val}
|
||||
placeholder="Filter runs..."
|
||||
autofocus
|
||||
/>
|
||||
<TimeframeSelect
|
||||
unifiedSize="sm"
|
||||
onClick={() => jobsLoader?.loadJobs(true)}
|
||||
loading={jobsLoader?.loading}
|
||||
items={runsTimeframes}
|
||||
bind:value={_timeframe.val}
|
||||
/>
|
||||
<!-- The filters are shallow-routed, so the search has to come off `window.location` at click
|
||||
time — `page.url` never sees them. Always the canonical `/runs`: only that is a recognized
|
||||
preview page, and the `/runs/<path>` route mirrors its path into `?path=` anyway. -->
|
||||
<OpenInSessionButton
|
||||
source={{
|
||||
page: () => pageHref(RUNS_PATH) + window.location.search,
|
||||
workspaceId: $workspaceStore ?? undefined
|
||||
}}
|
||||
btnProps={{ unifiedSize: 'sm' }}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<ConfirmationModal
|
||||
title={askingForConfirmation?.title ?? ''}
|
||||
confirmationText={askingForConfirmation?.confirmBtnText ?? ''}
|
||||
@@ -650,226 +867,12 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-screen flex flex-col" bind:clientWidth={innerWidth}>
|
||||
<!-- Header and filters -->
|
||||
<div id="runs-filters-bar" class="flex flex-row items-start w-full px-4 gap-3 py-4 flex-wrap">
|
||||
<div class="flex flex-row items-center gap-6">
|
||||
<h1
|
||||
class={twMerge(
|
||||
'!text-2xl font-semibold leading-6 tracking-tight',
|
||||
$userStore?.operator ? 'pl-10' : ''
|
||||
)}
|
||||
>
|
||||
Runs
|
||||
</h1>
|
||||
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs"
|
||||
>
|
||||
All past and schedule executions of scripts and flows, including previews. You only see
|
||||
your own runs or runs of groups you belong to unless you are an admin.
|
||||
</Tooltip>
|
||||
|
||||
<!-- Queue -->
|
||||
<RunsQueue
|
||||
success={filters.val.status ?? null}
|
||||
{queue_count}
|
||||
{suspended_count}
|
||||
onJobsWaiting={() => {
|
||||
jobsFilter('waiting')
|
||||
}}
|
||||
onJobsSuspended={() => {
|
||||
jobsFilter('suspended')
|
||||
}}
|
||||
small={innerWidth < smallScreenWidth}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="hidden xl:flex gap-2 ml-6">
|
||||
<ToggleButtonGroup
|
||||
tabListClass="hidden xl:flex"
|
||||
bind:selected={
|
||||
() => filters.val.job_kinds ?? 'runs',
|
||||
(v) => (v === 'runs' ? delete filters.val.job_kinds : (filters.val.job_kinds = v))
|
||||
}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="all" label="All" {item} />
|
||||
<ToggleButton
|
||||
value="runs"
|
||||
label="Runs"
|
||||
tooltip="Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they start), they have been triggered through the UI, a schedule or webhook"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="dependencies"
|
||||
label="Deps"
|
||||
tooltip="Deploying a script, flow or an app launch a dependency job that create and then attach the lockfile to the deployed item. This mechanism ensure that logic is always executed with the exact same direct and indirect dependencies."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButtonMore
|
||||
hideSelectedOption={innerWidth < smallScreenWidth}
|
||||
togglableItems={[
|
||||
{
|
||||
label: 'Previews',
|
||||
value: 'previews',
|
||||
tooltip: "Previews are jobs that have been started in the editor as 'Tests'"
|
||||
},
|
||||
{
|
||||
label: 'Sync',
|
||||
value: 'deploymentcallbacks',
|
||||
tooltip:
|
||||
'Sync jobs that are triggered on every script deployment to sync the workspace with the Git repository configured in the the workspace settings'
|
||||
}
|
||||
]}
|
||||
{item}
|
||||
bind:selected={filters.val.job_kinds}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => filters.val.status ?? 'all',
|
||||
(v) => (v === 'all' ? delete filters.val.status : (filters.val.status = v))
|
||||
}
|
||||
id="status"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value={'all'} label="All" {item} />
|
||||
<ToggleButton
|
||||
value={'running'}
|
||||
tooltip="Running"
|
||||
class="whitespace-nowrap"
|
||||
icon={CirclePlay}
|
||||
iconProps={{
|
||||
class:
|
||||
'group-data-[state=on]:text-yellow-600 dark:group-data-[state=on]:text-yellow-400'
|
||||
}}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'success'}
|
||||
tooltip="Success"
|
||||
class="whitespace-nowrap"
|
||||
icon={CircleCheck}
|
||||
iconProps={{
|
||||
class:
|
||||
'group-data-[state=on]:text-green-500 dark:group-data-[state=on]:text-green-300'
|
||||
}}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'failure'}
|
||||
tooltip="Failure"
|
||||
class="whitespace-nowrap"
|
||||
icon={CircleAlert}
|
||||
iconProps={{
|
||||
class: 'group-data-[state=on]:text-red-500 dark:group-data-[state=on]:text-red-300'
|
||||
}}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'canceled'}
|
||||
tooltip="Canceled"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
selectedColor="gray"
|
||||
{item}
|
||||
/>
|
||||
{#if filters.val.status == 'waiting'}
|
||||
<ToggleButton
|
||||
value={'waiting'}
|
||||
tooltip="Waiting"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
selectedColor="blue"
|
||||
{item}
|
||||
/>
|
||||
{:else if filters.val.status == 'suspended'}
|
||||
<ToggleButton
|
||||
value={'suspended'}
|
||||
tooltip="Suspended"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
selectedColor="purple"
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="hidden xl:flex gap-2 items-center min-h-8 ml-2">
|
||||
{#if !filters.val.job_trigger_kind || filters.val.job_trigger_kind === '!schedule'}
|
||||
<div class="flex items-center gap-1" title="Show schedules">
|
||||
<Toggle
|
||||
size="xs"
|
||||
color="nord"
|
||||
id="show-schedules"
|
||||
bind:checked={
|
||||
() => filters.val.job_trigger_kind !== '!schedule',
|
||||
(v) =>
|
||||
v
|
||||
? delete filters.val.job_trigger_kind
|
||||
: (filters.val.job_trigger_kind = '!schedule')
|
||||
}
|
||||
/>
|
||||
<Calendar size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-1" title="Show future jobs">
|
||||
<Toggle
|
||||
size="xs"
|
||||
color="nord"
|
||||
id="show-future-jobs"
|
||||
bind:checked={
|
||||
() => filters.val.show_future_jobs !== false,
|
||||
(v) =>
|
||||
v ? delete filters.val.show_future_jobs : (filters.val.show_future_jobs = false)
|
||||
}
|
||||
/>
|
||||
<Clock size={14} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TimeframeSelect
|
||||
wrapperClasses="ml-auto"
|
||||
onClick={() => jobsLoader?.loadJobs(true)}
|
||||
loading={jobsLoader?.loading}
|
||||
items={runsTimeframes}
|
||||
bind:value={_timeframe.val}
|
||||
/>
|
||||
<FilterSearchbar
|
||||
class={twMerge(
|
||||
'flex-1 relative min-w-[18rem]',
|
||||
Object.keys(filters.val).length <= 3
|
||||
? 'max-w-[20rem] 2xl:max-w-[28rem]'
|
||||
: 'max-w-[34rem]',
|
||||
ButtonType.UnifiedMinHeightClasses.md
|
||||
)}
|
||||
schema={runsFilterSearchbarSchema}
|
||||
presets={buildRunsFilterPresets({
|
||||
isSuperAdminOrDevops: !!$superadmin || !!$devopsRole,
|
||||
isAdminsWorkspace: $workspaceStore === 'admins'
|
||||
})}
|
||||
bind:value={filters.val}
|
||||
placeholder="Filter runs..."
|
||||
autofocus
|
||||
/>
|
||||
<!-- The filters are shallow-routed, so the search has to come off
|
||||
`window.location` at click time — `page.url` never sees them. Always the
|
||||
canonical `/runs`: only that is a recognized preview page, and the
|
||||
`/runs/<path>` route mirrors its path into `?path=` anyway. -->
|
||||
<OpenInSessionButton
|
||||
source={{
|
||||
page: () => pageHref(RUNS_PATH) + window.location.search,
|
||||
workspaceId: $workspaceStore ?? undefined
|
||||
}}
|
||||
btnProps={{ unifiedSize: 'md' }}
|
||||
/>
|
||||
</div>
|
||||
<!-- Everything that used to sit in a row above the graph — the page's name, its queue, the
|
||||
filters and the timeframe — is in the page header. -->
|
||||
<PageHeaderContent hint={runsHint} actions={headerActions} barBorder={false} />
|
||||
|
||||
<!-- Graph -->
|
||||
<div id="runs-chart" class="p-2 px-4 bg-surface-tertiary mx-4 border rounded-md">
|
||||
<div id="runs-chart" class="p-2 px-4 bg-surface-tertiary mx-4 mt-4 border rounded-md">
|
||||
<div class="relative z-10 mb-2 flex gap-2">
|
||||
<Tabs bind:selected={graph}>
|
||||
<Tab value="RunChart" label="Duration" id="runs-chart-duration-tab" />
|
||||
|
||||
@@ -70,7 +70,14 @@
|
||||
<div class="flex items-center">
|
||||
<!-- The tooltip fixes a bug where the other tooltips won't disappear -->
|
||||
<Tooltip disablePopup>
|
||||
<DropdownV2 {btnText} enableFlyTransition {items} size={small ? 'sm' : 'md'} />
|
||||
<!-- fixedHeight pins the wrapper at h-8, which overhangs a small group's 28px row. -->
|
||||
<DropdownV2
|
||||
{btnText}
|
||||
enableFlyTransition
|
||||
{items}
|
||||
size={small ? 'sm' : 'md'}
|
||||
fixedHeight={!small}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,9 @@ export type PageHeaderContent = {
|
||||
section?: PageHeaderSection
|
||||
/** Rendered at the right end of the bar — the page's own buttons. */
|
||||
actions?: Snippet
|
||||
/** Rendered right after the page's name in the breadcrumb, for what reads as part of that name
|
||||
* rather than as an action — a page's documentation tooltip. */
|
||||
hint?: Snippet
|
||||
/** The workspace this page acts on, when it is not the one the app is pointed at — a session
|
||||
* running in a fork. The breadcrumb then names that workspace, and its fork part shows only
|
||||
* when it really is a fork. */
|
||||
@@ -80,6 +83,7 @@ export const pageHeader = {
|
||||
const c = e.get()
|
||||
if (c.item !== undefined) merged.item = c.item
|
||||
if (c.section !== undefined) merged.section = c.section
|
||||
if (c.hint !== undefined) merged.hint = c.hint
|
||||
if (c.contexts !== undefined) merged.contexts = c.contexts
|
||||
if (c.barPlacement !== undefined) merged.barPlacement = c.barPlacement
|
||||
if (c.actingWorkspaceId !== undefined) merged.actingWorkspaceId = c.actingWorkspaceId
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
{#if small}
|
||||
<Popover contentClasses="p-4" openOnHover debounceDelay={100}>
|
||||
{#snippet trigger()}
|
||||
<div class="relative">
|
||||
<!-- mr-2: the count badge hangs half outside this box, so the margin — not the row's
|
||||
gap — is what keeps it off whatever sits to the right. -->
|
||||
<div class="relative mr-2">
|
||||
<ServerCog size={16} />
|
||||
{#if queue_count && ($queue_count ?? 0) > 0}
|
||||
<div
|
||||
@@ -49,14 +51,11 @@
|
||||
{/if}
|
||||
|
||||
{#if small}
|
||||
<Popover
|
||||
contentClasses="p-4"
|
||||
openOnHover
|
||||
debounceDelay={100}
|
||||
disablePopup={!suspended_count || ($suspended_count ?? 0) <= 0}
|
||||
>
|
||||
<!-- The icon is on the bar whatever the count, so it answers a hover whatever the count —
|
||||
a disabled trigger takes no pointer events and reads as a dead icon. -->
|
||||
<Popover contentClasses="p-4" openOnHover debounceDelay={100}>
|
||||
{#snippet trigger()}
|
||||
<div class="relative">
|
||||
<div class="relative mr-2">
|
||||
<Hourglass size={16} />
|
||||
<div
|
||||
class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 bg-surface-secondary-inverse rounded-full text-primary-inverse text-2xs h-4 min-w-4 px-1"
|
||||
@@ -69,7 +68,7 @@
|
||||
{@render suspendedContent()}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{:else}
|
||||
{:else if suspended_count && ($suspended_count ?? 0) > 0}
|
||||
{@render suspendedContent()}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -103,28 +102,28 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet suspendedContent()}
|
||||
{#if suspended_count && ($suspended_count ?? 0) > 0}
|
||||
<RunOption label="Suspended">
|
||||
{#snippet tooltip()}
|
||||
Jobs waiting for an event or approval before being resumed
|
||||
{/snippet}
|
||||
<div
|
||||
class={suspended_count && ($suspended_count ?? 0) > 0
|
||||
? 'bg-surface-secondary-inverse text-primary-inverse rounded-full min-w-6 h-6 flex center-center'
|
||||
: ''}>{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}</div
|
||||
>
|
||||
<div class="truncate text-2xs !text-secondary">
|
||||
<Button unifiedSize="md" variant="subtle" on:click={() => onJobsSuspended?.()}>
|
||||
{#if success == 'suspended'}
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
Reset filter
|
||||
<X size={12} />
|
||||
</div>
|
||||
{:else}
|
||||
<ListFilterPlus size={14} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</RunOption>
|
||||
{/if}
|
||||
<RunOption label="Suspended">
|
||||
{#snippet tooltip()}
|
||||
Jobs waiting for an event or approval before being resumed
|
||||
{/snippet}
|
||||
<div
|
||||
class={suspended_count && ($suspended_count ?? 0) > 0
|
||||
? 'bg-surface-secondary-inverse text-primary-inverse rounded-full min-w-6 h-6 flex center-center'
|
||||
: ''}
|
||||
>
|
||||
{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}
|
||||
</div>
|
||||
<div class="truncate text-2xs !text-secondary">
|
||||
<Button unifiedSize="md" variant="subtle" on:click={() => onJobsSuspended?.()}>
|
||||
{#if success == 'suspended'}
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
Reset filter
|
||||
<X size={12} />
|
||||
</div>
|
||||
{:else}
|
||||
<ListFilterPlus size={14} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</RunOption>
|
||||
{/snippet}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { CalendarIcon, RefreshCw } from 'lucide-svelte'
|
||||
import { Button } from '../common'
|
||||
import { Button, type ButtonType } from '../common'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Popover from '../meltComponents/Popover.svelte'
|
||||
@@ -94,9 +94,18 @@
|
||||
value: Timeframe
|
||||
wrapperClasses?: string
|
||||
onClick?: () => void
|
||||
/** Sizes the three buttons of the control itself; the presets inside the popover keep md. */
|
||||
unifiedSize?: ButtonType.UnifiedSize
|
||||
}
|
||||
|
||||
let { loading = false, onClick, items, value = $bindable(), wrapperClasses }: Props = $props()
|
||||
let {
|
||||
loading = false,
|
||||
onClick,
|
||||
items,
|
||||
value = $bindable(),
|
||||
wrapperClasses,
|
||||
unifiedSize = 'md'
|
||||
}: Props = $props()
|
||||
|
||||
let isOpen = $state(false)
|
||||
|
||||
@@ -141,7 +150,7 @@
|
||||
|
||||
<div class="relative flex {wrapperClasses}">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
{unifiedSize}
|
||||
wrapperClasses="flex-1"
|
||||
btnClasses="!rounded-r-none whitespace-nowrap"
|
||||
onClick={() => onClick?.()}
|
||||
@@ -154,7 +163,7 @@
|
||||
{#if value.type === 'manual'}
|
||||
<Button
|
||||
btnClasses="!rounded-none border-l-0"
|
||||
unifiedSize="md"
|
||||
{unifiedSize}
|
||||
onClick={() => (value = { ...items[0] })}
|
||||
>
|
||||
Reset
|
||||
@@ -168,7 +177,7 @@
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
{unifiedSize}
|
||||
iconOnly
|
||||
btnClasses="!rounded-l-none border-l-0"
|
||||
endIcon={{ icon: CalendarIcon }}
|
||||
|
||||
@@ -932,7 +932,7 @@
|
||||
<Pane
|
||||
bind:size={chatPaneSize}
|
||||
minSize={25}
|
||||
class="flex flex-col min-h-0 {previewCollapsed || fullscreen ? '' : 'pt-10'}"
|
||||
class="flex flex-col min-h-0 {previewCollapsed || fullscreen ? '' : 'pt-11'}"
|
||||
>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
{#each warmSessions as s (s.id)}
|
||||
|
||||
Reference in New Issue
Block a user