RunsPage redesign v0

This commit is contained in:
Diego Imbert
2026-02-05 15:54:25 +01:00
parent 35c9a09052
commit 7962b74ba0
9 changed files with 96 additions and 305 deletions
@@ -260,6 +260,6 @@
} as any)
</script>
<div class="relative max-h-40">
<div class="relative h-44">
<Line {data} {options} />
</div>
@@ -1,38 +0,0 @@
<script lang="ts">
import DropdownV2 from '$lib/components/DropdownV2.svelte'
import { ChevronDown } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import type { Item } from '$lib/utils'
interface Props {
items?: Item[]
extraLabel?: import('svelte').Snippet
selected: string
selectedDisplayName?: string
btnClasses?: string
}
let { items = [], extraLabel, selected, selectedDisplayName, btnClasses }: Props = $props()
const filteredItems = $derived(items.filter((item) => item.id !== selected))
</script>
<DropdownV2 items={filteredItems}>
{#snippet buttonReplacement()}
<div
class={twMerge(
'p-2 h-8 flex flex-row items-center gap-2 border hover:bg-surface-hover cursor-pointer rounded-md',
btnClasses
)}
>
<div class="flex flex-row items-center gap-1 pr-2 justify-between w-full">
<span class="text-xs whitespace-nowrap">
{selectedDisplayName ?? items.find((item) => item.id === selected)?.displayName ?? ''}
</span>
{@render extraLabel?.()}
</div>
<ChevronDown size={12} />
</div>
{/snippet}
</DropdownV2>
+1 -26
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import 'chartjs-adapter-date-fns'
import zoomPlugin from 'chartjs-plugin-zoom'
import Tooltip2 from '$lib/components/Tooltip.svelte'
import {
Chart as ChartJS,
Title,
@@ -17,7 +16,6 @@
} from 'chart.js'
import type { CompletedJob } from '$lib/gen'
import { getDbClockNow } from '$lib/forLater'
import Button from './common/button/Button.svelte'
import { Scatter } from '$lib/components/chartjs-wrappers/chartJs'
import DarkModeObserver from './DarkModeObserver.svelte'
@@ -28,10 +26,7 @@
maxTimeSet?: string | null
selectedIds?: string[]
canSelect?: boolean
lastFetchWentToEnd?: boolean
totalRowsFetched: number
onPointClicked: (ids: string[]) => void
onLoadExtra: () => void
onZoom: (zoom: { min: Date; max: Date }) => void
}
@@ -42,10 +37,7 @@
maxTimeSet = null,
selectedIds = $bindable([]),
canSelect = true,
lastFetchWentToEnd = false,
totalRowsFetched,
onPointClicked,
onLoadExtra,
onZoom
}: Props = $props()
@@ -299,23 +291,6 @@
<DarkModeObserver bind:darkMode />
<!-- {JSON.stringify(minTime)}
{JSON.stringify(maxTime)}
{JSON.stringify(jobs?.map((x) => x.started_at))} -->
<!-- {minTime}
{maxTime} -->
<!-- {JSON.stringify(jobs?.map((x) => x.started_at))} -->
<div class="relative max-h-40">
{#if !lastFetchWentToEnd}
<div class="absolute top-[-28px] left-[220px]">
<Button size="xs" color="transparent" variant="contained" on:click={() => onLoadExtra()}>
Load more
<Tooltip2>
There are more jobs to load but only the first {totalRowsFetched} were fetched
</Tooltip2>
</Button>
</div>
{/if}
<div class="relative h-44">
<Scatter {data} options={scatterOptions} />
</div>
+57 -183
View File
@@ -11,7 +11,7 @@
import { sendUserToast } from '$lib/toast'
import { userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
import { Button, Drawer, DrawerContent, Skeleton, Tab, Tabs } from '$lib/components/common'
import RunChart from '$lib/components/RunChart.svelte'
import JobRunsPreview from '$lib/components/runs/JobRunsPreview.svelte'
@@ -27,25 +27,20 @@
import { twMerge } from 'tailwind-merge'
import ManuelDatePicker from '$lib/components/runs/ManuelDatePicker.svelte'
import { computeJobKinds, useJobsLoader } from '$lib/components/runs/useJobsLoader.svelte'
import { Calendar, Clock, TriangleAlert } from 'lucide-svelte'
import ConcurrentJobsChart from '$lib/components/ConcurrentJobsChart.svelte'
import { isJobSelectable, type RunsSelectionMode } from '$lib/utils'
import { isJobSelectable, pluralize, type RunsSelectionMode } from '$lib/utils'
import BatchReRunOptionsPane, {
type BatchReRunOptions
} from '$lib/components/runs/BatchReRunOptionsPane.svelte'
import { untrack } from 'svelte'
import { page } from '$app/state'
import RunOption from '$lib/components/runs/RunOption.svelte'
import TooltipV2 from '$lib/components/meltComponents/Tooltip.svelte'
import DropdownSelect from '$lib/components/DropdownSelect.svelte'
import RunsBatchActionsDropdown from '$lib/components/runs/RunsBatchActionsDropdown.svelte'
import { createBubbler } from 'svelte/legacy'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import Select from '$lib/components/select/Select.svelte'
import AnimatedPane from '$lib/components/splitPanes/AnimatedPane.svelte'
import { useSearchParams } from '$lib/svelte5UtilsKit.svelte'
import { StaleWhileLoading } from '$lib/svelte5Utils.svelte'
import { TriangleAlertIcon } from 'lucide-svelte'
interface Props {
/** Initial path from route params (e.g., /runs/u/user/script) */
@@ -130,7 +125,6 @@
let graph: 'RunChart' | 'ConcurrencyChart' = $state(
typeOfChart(page.url.searchParams.get('graph'))
)
let graphIsRunsChart: boolean = $state(untrack(() => graph) === 'RunChart')
let innerWidth = $state(window.innerWidth)
let jobsLoader = useJobsLoader(() => ({
filters,
@@ -139,7 +133,7 @@
autoRefresh,
argError,
resultError,
lookback: graphIsRunsChart ? 0 : lookback,
lookback: graph === 'RunChart' ? 0 : lookback,
onSetPerPage: (p) => (filters.per_page = p),
onSetMinMaxTs: (minTs, maxTs) => ((filters.min_ts = minTs), (filters.max_ts = maxTs)),
currentWorkspace: $workspaceStore ?? ''
@@ -456,10 +450,6 @@
}
}
function setLookback(lookbackInDays: number) {
lookback = lookbackInDays
}
async function loadExtra() {
await jobsLoader?.loadExtraJobs()
}
@@ -524,8 +514,6 @@
return jobs?.filter(isJobSelectable(selectionMode)).length ?? 0
})
let tableTopBarWidth = $state(0)
const smallScreenWidth = 1920
const verySmallScreenWidth = 1300
@@ -603,7 +591,7 @@
{:else}
<div class="w-full h-screen flex flex-col" bind:clientWidth={innerWidth}>
<!-- Header and filters -->
<div class="flex flex-row items-start w-full border-b px-4 gap-8">
<div class="flex flex-row items-start w-full px-4 gap-8">
<div class="flex flex-row items-center h-full gap-6">
<div class="flex flex-row items-center gap-1">
<h1
@@ -645,7 +633,7 @@
{#if filters.min_ts || filters.max_ts}
<input
type="text"
class="!text-sm text-primary !bg-surface-secondary h-9 !border-none"
class="!text-sm text-primary !bg-surface-sunken h-9 !border-none"
value={filters.min_ts
? new Date(filters.min_ts).toLocaleString()
: 'zoom x axis to set min'}
@@ -733,78 +721,41 @@
</div>
<!-- Graph -->
<div class="p-2 px-4 pt-8 w-full border-b">
<div class="relative z-10">
<div class="absolute left-0 -top-7 flex flex-row gap-2 items-center min-w-24">
<ToggleButtonGroup
selected={graph}
on:selected={({ detail }) => {
graph = detail
graphIsRunsChart = graph === 'RunChart'
}}
>
{#snippet children({ item })}
<ToggleButton value="RunChart" label="Duration" {item} />
<ToggleButton
{item}
value="ConcurrencyChart"
label="Concurrency"
icon={warnJobLimit ? TriangleAlert : undefined}
tooltip={warnJobLimit ? warnJobLimitMsg : undefined}
/>
<div class="p-2 px-4 bg-surface-tertiary mx-4 mt-2 drop-shadow-base rounded-md">
<div class="relative z-10 mb-2 flex gap-2">
<Tabs bind:selected={graph}>
<Tab value="RunChart" label="Duration" />
<Tab value="ConcurrencyChart" label="Concurrency">
{#snippet extra()}
{#if warnJobLimit}
<Tooltip Icon={TriangleAlertIcon}>{warnJobLimitMsg}</Tooltip>
{/if}
{/snippet}
</ToggleButtonGroup>
</Tab>
</Tabs>
{#if !graphIsRunsChart}
<DropdownSelect
items={[
{
displayName: 'None',
action: () => setLookback(0),
id: '0'
},
{
displayName: '1 day',
action: () => setLookback(1),
id: '1'
},
{
displayName: '3 days',
action: () => setLookback(3),
id: '3'
},
{
displayName: '7 days',
action: () => setLookback(7),
id: '7'
}
]}
selected={lookback.toString()}
selectedDisplayName={`${lookback} days lookback`}
>
{#snippet extraLabel()}
<TooltipV2>
{#snippet text()}
How far behind the min datetime to start considering jobs for the concurrency
graph. Change this value to include jobs started before the set time window for
the computation of the graph
{/snippet}
</TooltipV2>
{/snippet}
</DropdownSelect>
{/if}
</div>
{#if graph !== 'RunChart'}
<Select
class="ml-auto"
bind:value={lookback}
items={[
{ label: 'None', value: 0 },
{ label: '1 day', value: 1 },
{ label: '3 days', value: 3 },
{ label: '7 days', value: 7 }
]}
transformInputSelectedText={(_, v) => `${pluralize(v, 'day')} lookback`}
tooltip={'How far behind the min datetime to start considering jobs for the concurrency graph. Change this value to include jobs started before the set time window for the computation of the graph'}
/>
{/if}
</div>
{#if graph === 'RunChart'}
<RunChart
{lastFetchWentToEnd}
bind:selectedIds
canSelect={!selectionMode}
minTimeSet={filters.min_ts}
maxTimeSet={filters.max_ts}
totalRowsFetched={jobs?.length ?? 0}
maxIsNow={filters.max_ts == undefined}
onLoadExtra={loadExtra}
jobs={completedJobs}
onZoom={async (zoom) => {
filters.min_ts = zoom.min.toISOString()
@@ -831,106 +782,10 @@
{/if}
</div>
<div class="grow min-h-0">
<div class="grow min-h-0 mt-4">
<Splitpanes>
<Pane minSize={40}>
<div class="flex flex-col h-full">
<!-- Runs table top bar -->
<div
class="flex flex-row gap-4 items-center px-2 py-1 grow-0 justify-between"
bind:clientWidth={tableTopBarWidth}
>
<div class="flex flex-row gap-4 items-center">
{#if selectionMode && selectableJobCount}
<div class="flex flex-row items-center font-semibold text-sm">
<div class="px-2">
<input
onfocus={bubble('focus')}
type="checkbox"
checked={allSelected}
id="select-all"
class={twMerge(
'cursor-pointer',
allSelected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
'flex flex-row items-center p-2 pr-4 top-0 font-semibold text-sm'
)}
onclick={selectAll}
/>
</div>
<label
class="cursor-pointer whitespace-nowrap text-xs text-emphasis font-semibold"
for="select-all">Select all</label
>
</div>
{/if}
<RunsBatchActionsDropdown
isLoading={loadingSelectedIds}
{selectionMode}
selectionCount={selectedIds.length}
{onSetSelectionMode}
{onCancelFilteredJobs}
{onCancelSelectedJobs}
{onReRunFilteredJobs}
{onReRunSelectedJobs}
small={tableTopBarWidth < 800}
/>
</div>
<div class="flex flex-row gap-4 items-center">
{#if !filters.job_trigger_kind}
<div class="flex flex-row gap-1 items-center">
<Toggle
id="cron-schedules"
bind:checked={filters.show_schedules}
options={tableTopBarWidth < 800 || selectionMode
? {}
: { right: 'Schedules' }}
/>
<span title="Schedules">
<Calendar size="14" />
</span>
</div>
{/if}
<div class="flex flex-row gap-1 items-center">
<Toggle
size="sm"
bind:checked={filters.show_future_jobs}
id="planned-later"
options={tableTopBarWidth < 800 || selectionMode
? {}
: { right: 'Planned later' }}
/>
<span title="Planned later">
<Clock size={14} />
</span>
</div>
<div class="flex flex-row gap-2 items-center">
<ManuelDatePicker
on:loadJobs={() => {
jobsLoader?.loadJobs(true)
}}
bind:minTs={filters.min_ts}
bind:maxTs={filters.max_ts}
bind:selectedManualDate
{loading}
bind:this={manualDatePicker}
numberOfLastJobsToFetch={filters.per_page}
/>
<Toggle
size="sm"
bind:checked={autoRefresh}
on:change={() => {
localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false')
}}
options={{ right: 'Auto-refresh' }}
textClass="whitespace-nowrap"
/>
</div>
</div>
</div>
<!-- Runs table. Add overflow-hidden because scroll is handled inside the runs table based on this wrapper height -->
<div class="grow min-h-0 overflow-y-hidden overflow-x-auto">
{#if jobs}
@@ -938,7 +793,7 @@
{jobs}
externalJobs={externalJobs ?? []}
omittedObscuredJobs={extendedJobs?.omitted_obscured_jobs ?? false}
showExternalJobs={!graphIsRunsChart}
showExternalJobs={graph !== 'RunChart'}
activeLabel={filters.label}
{selectionMode}
{lastFetchWentToEnd}
@@ -964,12 +819,30 @@
</div>
{/if}
</div>
<div
class="bg-surface-secondary border-t flex text-xs px-2 py-1 items-center justify-end gap-2"
>
Per page:
<div class="bg-surface border-t flex text-xs px-2 py-1 items-center gap-4">
<ManuelDatePicker
on:loadJobs={() => {
jobsLoader?.loadJobs(true)
}}
bind:minTs={filters.min_ts}
bind:maxTs={filters.max_ts}
bind:selectedManualDate
{loading}
bind:this={manualDatePicker}
numberOfLastJobsToFetch={filters.per_page}
/>
<Toggle
size="xs"
bind:checked={autoRefresh}
on:change={() => {
localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false')
}}
options={{ right: 'Auto-refresh' }}
textClass="whitespace-nowrap"
/>
<div class="flex-1"></div>
<Select
class="w-20"
class="w-24"
bind:value={
() => filters.per_page,
(newPerPage) => {
@@ -984,6 +857,7 @@
{ value: 1000, label: '1000' },
{ value: 10000, label: '10000' }
]}
transformInputSelectedText={(_, v) => `${v} / page`}
/>
</div>
</div>
+3 -1
View File
@@ -19,6 +19,7 @@
markdownTooltip?: string | undefined
customSize?: string
class?: string
Icon?: typeof InfoIcon
children?: import('svelte').Snippet
}
@@ -31,6 +32,7 @@
markdownTooltip = undefined,
customSize = '100%',
class: classNames = '',
Icon = InfoIcon,
children
}: Props = $props()
const plugins = [gfmPlugin()]
@@ -53,7 +55,7 @@
? 'text-primary-inverse'
: 'text-primary'} {classNames} relative"
>
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
<Icon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
</div>
{#snippet text()}
{#if markdownTooltip}
@@ -48,7 +48,6 @@
import Select from '../select/Select.svelte'
import { safeSelectItems } from '../select/utils.svelte'
import RunOption from './RunOption.svelte'
import DropdownSelect from '../DropdownSelect.svelte'
import TooltipV2 from '$lib/components/meltComponents/Tooltip.svelte'
import TextInput from '../text_input/TextInput.svelte'
import { jobTriggerKinds, triggerDisplayNamesMap } from '../triggers/utils'
@@ -528,50 +527,15 @@
<!-- Kind -->
<RunOption label="Kind" for="kind">
{#if small && !calendarSmall}
<DropdownSelect
btnClasses="min-w-24 h-9 bg-surface-secondary font-normal"
<Select
items={[
{
displayName: 'All',
action: () => {
jobKindsCat = 'all'
},
id: 'all'
},
{
displayName: 'Runs',
action: () => {
jobKindsCat = 'runs'
},
id: 'runs',
extra: runsTooltip
},
{
displayName: 'Previews',
action: () => {
jobKindsCat = 'previews'
},
id: 'previews',
extra: previewsTooltip
},
{
displayName: 'Deps',
action: () => {
jobKindsCat = 'dependencies'
},
id: 'dependencies',
extra: dependenciesTooltip
},
{
displayName: 'Sync',
action: () => {
jobKindsCat = 'deploymentcallbacks'
},
id: 'deploymentcallbacks',
extra: syncTooltip
}
{ label: 'All', value: 'all' },
{ label: 'Runs', value: 'runs', extra: runsTooltip }, // TODO fix tooltip
{ label: 'Previews', value: 'previews', extra: previewsTooltip }, // TODO fix tooltip
{ label: 'Deps', value: 'dependencies', extra: dependenciesTooltip }, // TODO fix tooltip
{ label: 'Sync', value: 'deploymentcallbacks', extra: syncTooltip } // TODO fix tooltip
]}
selected={jobKindsCat}
bind:value={jobKindsCat}
/>
{:else}
<ToggleButtonGroup bind:selected={jobKindsCat}>
@@ -199,13 +199,13 @@
<svelte:window onresize={() => computeHeight()} />
<div
class="divide-y h-full border min-w-[650px]"
class="divide-y h-full min-w-[650px]"
id="runs-table-wrapper"
bind:clientWidth={containerWidth}
>
<div bind:clientHeight={headerHeight}>
<div
class="grid bg-surface-secondary sticky top-0 w-full py-2 pr-4"
class="grid sticky top-0 w-full min-h-6 mb-2 pr-4 items-end"
class:grid-runs-table={!containsLabel && !selectionMode && showTag}
class:grid-runs-table-with-labels={containsLabel && !selectionMode && showTag}
class:grid-runs-table-selection={!containsLabel && selectionMode && showTag}
@@ -220,7 +220,7 @@
{#if selectionMode}
<div class="text-xs font-semibold pl-4"></div>
{/if}
<div class="text-2xs px-2 flex flex-row items-center gap-2">
<div class="text-2xs px-2 flex flex-row items-center gap-2 leading-3">
{#if showExternalJobs && externalJobs.length > 0}
<div class="flex flex-row">
{jobs
@@ -242,15 +242,15 @@
{jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''}
{/if}
</div>
<div class="text-xs font-semibold">Started</div>
<div class="text-xs font-semibold">Duration</div>
<div class="text-xs font-semibold">Path</div>
<div class="text-xs font-semibold leading-3">Started</div>
<div class="text-xs font-semibold leading-3">Duration</div>
<div class="text-xs font-semibold leading-3">Path</div>
{#if containsLabel}
<div class="text-xs font-semibold">Label</div>
<div class="text-xs font-semibold leading-3">Label</div>
{/if}
<div class="text-xs font-semibold">Triggered by</div>
<div class="text-xs font-semibold leading-3">Triggered by</div>
{#if showTag}
<div class="text-xs font-semibold">Tag</div>
<div class="text-xs font-semibold leading-3">Tag</div>
{/if}
<div class=""></div>
</div>
@@ -532,6 +532,8 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
})
$effect(() => {
Object.keys(filters ?? {}).map((k) => filters?.[k as keyof RunsFilters])
currentWorkspace
lookback
let p = untrack(() => onParamChanges())
return () => p.cancel()
})
@@ -16,6 +16,7 @@
inputSizeClasses
} from '../text_input/TextInput.svelte'
import { ButtonType } from '../common/button/model'
import Tooltip from '../Tooltip.svelte'
type Value = Item['value']
@@ -37,6 +38,7 @@
RightIcon,
createText,
noItemsMsg,
tooltip,
open = $bindable(false),
id,
itemLabelWrapperClasses,
@@ -72,12 +74,13 @@
createText?: string
noItemsMsg?: string
open?: boolean
tooltip?: string
id?: string
itemLabelWrapperClasses?: string
itemButtonWrapperClasses?: string
size?: 'sm' | 'md' | 'lg'
showPlaceholderOnOpen?: boolean
transformInputSelectedText?: (text: string) => string
transformInputSelectedText?: (text: string, value: Value) => string
groupBy?: (item: Item) => string
sortBy?: (a: Item, b: Item) => number
onFocus?: () => void
@@ -106,7 +109,9 @@
if (!open) filterText = ''
})
let valueEntry = $derived(value && processedItems?.find((item) => deepEqual(item.value, value)))
let valueEntry = $derived(
value != null ? processedItems?.find((item) => deepEqual(item.value, value)) : undefined
)
function setValue(item: ProcessedItem<Value>) {
if (item.__is_create && onCreateItem) {
@@ -126,12 +131,12 @@
let inputText = $derived.by(() => {
let text = valueEntry?.label ?? getLabel({ value }) ?? ''
return transformInputSelectedText?.(text) ?? text
return transformInputSelectedText?.(text, value) ?? text
})
</script>
<div
class={`relative ${className}`}
class={`relative h-fit ${className}`}
use:clickOutside={{ onClickOutside: () => (open = false) }}
onpointerdown={() => onFocus?.()}
onfocus={() => onFocus?.()}
@@ -155,6 +160,13 @@
<RightIcon size={iconSize} class="text-secondary" />
</div>
{/if}
{#if tooltip}
<div class="absolute z-10 right-2 h-full flex items-center">
<Tooltip>{tooltip}</Tooltip>
</div>
{/if}
<!-- svelte-ignore a11y_autofocus -->
<input
{autofocus}