New Timeframe system

This commit is contained in:
Diego Imbert
2026-02-09 21:48:45 +01:00
parent bbb74b1d2d
commit 151fc9a951
8 changed files with 378 additions and 294 deletions
@@ -184,13 +184,13 @@
unifiedSize="md"
wrapperClasses="h-full"
{disabled}
iconOnly
endIcon={{ icon: X }}
on:click={() => {
value = null
dispatch('clear')
}}
>
<X size={14} />
</Button>
></Button>
{/if}
<!-- <div>
<ToggleButtonGroup bind:selected={format} let:item>
+37 -74
View File
@@ -16,7 +16,6 @@
import JobRunsPreview from '$lib/components/runs/JobRunsPreview.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import CalendarPicker from '$lib/components/common/calendarPicker/CalendarPicker.svelte'
import RunsTable from '$lib/components/runs/RunsTable.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
@@ -33,13 +32,17 @@
} from '$lib/components/runs/BatchReRunOptionsPane.svelte'
import { untrack } from 'svelte'
import { page } from '$app/state'
import RunOption from '$lib/components/runs/RunOption.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'
import DropdownV2 from './DropdownV2.svelte'
import TimeframeSelect, {
buildManualTimeframe,
runsTimeframes,
useUrlSyncedTimeframe
} from './runs/TimeframeSelect.svelte'
interface Props {
/** Initial path from route params (e.g., /runs/u/user/script) */
@@ -117,12 +120,18 @@
}
}
let _timeframe = useUrlSyncedTimeframe(runsTimeframes)
let timeframe = $derived(_timeframe.timeframe)
let manualTimeframe = $derived(timeframe.type === 'manual' ? timeframe : undefined)
let graph: 'RunChart' | 'ConcurrencyChart' = $state(
typeOfChart(page.url.searchParams.get('graph'))
)
let innerWidth = $state(window.innerWidth)
let jobsLoader = useJobsLoader(() => ({
filters,
timeframe,
jobKinds,
autoRefresh,
argError,
@@ -154,8 +163,7 @@
}
function reset() {
filters.min_ts = null
filters.max_ts = null
_timeframe.timeframe = { ...runsTimeframes[0] }
selectedIds = []
filters.schedule_path = null
selectedWorkspace = undefined
@@ -215,10 +223,11 @@
function getSelectedFilters() {
const argFilter = filters.arg && JSON.parse(filters.arg)
const resultFilter = filters.result && JSON.parse(filters.result)
const { minTs, maxTs } = timeframe.computeMinMax()
return {
workspace: $workspaceStore ?? '',
startedBefore: filters.max_ts ?? undefined,
startedAfter: filters.min_ts ?? undefined,
startedBefore: maxTs ?? undefined,
startedAfter: minTs ?? undefined,
schedulePath: filters.schedule_path ?? undefined,
scriptPathExact: filters.path === null || filters.path === '' ? undefined : filters.path,
createdBy: filters.user || undefined,
@@ -574,58 +583,13 @@
<div class="py-2 flex items-start gap-x-4 gap-y-2 flex-row grow min-w-0 justify-end">
<!-- Dates -->
<div class="flex flex-row gap-2">
<RunOption label="From" for="min-datetimes">
{#if filters.min_ts || filters.max_ts}
<input
type="text"
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'}
disabled
name="min-datetimes"
/>
{/if}
<CalendarPicker
clearable={true}
bind:date={filters.min_ts}
on:clear={() => (filters.min_ts = null)}
label="From"
class={filters.min_ts || filters.max_ts
? ''
: 'relative top-0 bottom-0 left-0 right-0 h-[34px]'}
/>
</RunOption>
<RunOption label="To" for="max-datetimes">
{#if filters.max_ts || filters.min_ts}
<input
type="text"
class="!text-sm text-primary !bg-surface-secondary h-9 !border-none"
value={filters.max_ts
? new Date(filters.max_ts).toLocaleString()
: 'zoom x axis to set max'}
name="max-datetimes"
disabled
/>
{/if}
<CalendarPicker
clearable={true}
on:clear={() => (filters.max_ts = null)}
bind:date={filters.max_ts}
label="To"
class={filters.min_ts || filters.max_ts
? ''
: 'relative top-0 bottom-0 left-0 right-0 h-[34px]'}
/>
</RunOption>
{#if filters.min_ts || filters.max_ts}
<RunOption label="Reset" for="reset" noLabel>
<Button variant="default" size="xs" onClick={reset} btnClasses="h-9">Reset</Button>
</RunOption>
{/if}
<div class="mt-auto">
<TimeframeSelect
onClick={() => jobsLoader?.loadJobs(true)}
loading={jobsLoader?.loading}
items={runsTimeframes}
bind:value={_timeframe.timeframe}
/>
</div>
<!-- Filters 1 -->
@@ -650,17 +614,11 @@
bind:argFilter={filters.arg}
bind:resultFilter={filters.result}
on:change={reloadJobsWithoutFilterError}
on:successChange={(e) => {
if (e.detail == 'running' && filters.max_ts != undefined) {
filters.max_ts = null
}
}}
{usernames}
{folders}
{paths}
mobile={innerWidth < verySmallScreenWidth}
small={innerWidth < smallScreenWidth}
calendarSmall={!filters.min_ts && !filters.max_ts}
/>
</div>
</div>
@@ -696,16 +654,19 @@
{/if}
</div>
{#if graph === 'RunChart'}
{@const manualTimeframe = timeframe.type === 'manual' ? timeframe : undefined}
<RunChart
bind:selectedIds
canSelect
minTimeSet={filters.min_ts}
maxTimeSet={filters.max_ts}
maxIsNow={filters.max_ts == undefined}
minTimeSet={manualTimeframe?.minTs}
maxTimeSet={manualTimeframe?.maxTs}
maxIsNow={manualTimeframe?.maxTs == undefined}
jobs={completedJobs}
onZoom={async (zoom) => {
filters.min_ts = zoom.min.toISOString()
filters.max_ts = zoom.max.toISOString()
_timeframe.timeframe = buildManualTimeframe(
zoom.min.toISOString(),
zoom.max.toISOString()
)
jobsLoader?.loadJobs(true)
}}
onPointClicked={(ids) => {
@@ -714,13 +675,15 @@
/>
{:else if graph === 'ConcurrencyChart'}
<ConcurrentJobsChart
minTimeSet={filters.min_ts}
maxTimeSet={filters.max_ts}
maxIsNow={filters.max_ts == undefined}
minTimeSet={manualTimeframe?.minTs}
maxTimeSet={manualTimeframe?.maxTs}
maxIsNow={manualTimeframe?.maxTs == undefined}
{extendedJobs}
onZoom={async (zoom) => {
filters.min_ts = zoom.min.toISOString()
filters.max_ts = zoom.max.toISOString()
_timeframe.timeframe = buildManualTimeframe(
zoom.min.toISOString(),
zoom.max.toISOString()
)
jobsLoader?.loadJobs(true)
}}
/>
@@ -4,12 +4,15 @@
const bubble = createBubbler()
import { IndexSearchService, ServiceLogsService } from '$lib/gen'
import ManuelDatePicker from './runs/ManuelDatePicker.svelte'
import TimeframeSelect, {
serviceLogsTimeframes,
useUrlSyncedTimeframe
} from './runs/TimeframeSelect.svelte'
import CalendarPicker from './common/calendarPicker/CalendarPicker.svelte'
import LogViewer from './LogViewer.svelte'
import Toggle from './Toggle.svelte'
import { sendUserToast } from '$lib/toast'
import { onDestroy, tick, untrack } from 'svelte'
import { onDestroy, tick } from 'svelte'
import { Loader2 } from 'lucide-svelte'
import { copyToClipboard, scroll_into_view_if_needed_polyfill, truncateRev } from '$lib/utils'
import LogSnippetViewer from './LogSnippetViewer.svelte'
@@ -20,6 +23,7 @@
import Select from './select/Select.svelte'
import { goto } from '$lib/navigation'
import { page } from '$app/stores'
import { watch } from 'runed'
interface Props {
searchTerm: string
@@ -32,9 +36,6 @@
let minTs: undefined | string = $state(undefined)
let maxTs: undefined | string = $state(undefined)
let minTsManual: undefined | string = $state($page.url.searchParams.get('minTs') ?? undefined)
let maxTsManual: undefined | string = $state($page.url.searchParams.get('maxTs') ?? undefined)
let max_lines: undefined | number = $state(undefined)
// let lastSeen: undefined | string = undefined
@@ -58,15 +59,17 @@
let timeout: number | undefined = $state(undefined)
let allLogs: ByMode | undefined = $state(undefined)
let manualPicker: ManuelDatePicker | undefined = $state(undefined)
let _timeframe = useUrlSyncedTimeframe(serviceLogsTimeframes)
let timeframe = $derived(_timeframe.timeframe)
let [minTsManual, maxTsManual] = $derived(
timeframe.type === 'manual' ? [timeframe.minTs ?? undefined, timeframe.maxTs ?? undefined] : []
)
let upTo: undefined | string = $state(undefined)
let upToIsLatest = $state(true)
function onManualChanges() {
getAllLogs(minTsManual ?? maxTs, maxTsManual)
}
function getAllLogs(queryMinTs: string | undefined, queryMaxTs: string | undefined) {
timeout && clearTimeout(timeout)
loading = true
@@ -151,11 +154,6 @@
if (autoRefresh && searchTerm === '' && !maxTsManual) {
timeout = setTimeout(() => {
if (searchTerm !== '') return
let minMax = manualPicker?.computeMinMax()
if (minMax) {
maxTsManual = minMax?.maxTs ?? undefined
minTsManual = minMax?.minTs ?? undefined
}
let maxTsPlus1 = maxTs ? new Date(new Date(maxTs).getTime() + 1000) : undefined
getAllLogs(maxTsPlus1?.toISOString(), undefined)
}, 5000)
@@ -315,8 +313,6 @@
) {
const params = new URLSearchParams()
if (searchTerm) params.set('searchTerm', searchTerm)
if (minTs) params.set('minTs', minTs)
if (maxTs) params.set('maxTs', maxTs)
if (selected?.mode) params.set('mode', selected.mode)
if (selected?.workerGroup) params.set('workerGroup', selected.workerGroup)
if (selected?.hostname) params.set('hostname', selected.hostname)
@@ -435,13 +431,22 @@
)
}
$effect(() => {
minTsManual || maxTsManual || untrack(() => onManualChanges())
})
$effect(() => {
;[searchTerm, selected, minTsManual, maxTsManual, allLogs]
untrack(() => searchLogs(searchTerm, selected, minTsManual, maxTsManual, allLogs))
})
watch(
() => timeframe,
() => {
const ts = timeframe.computeMinMax()
minTs = undefined
maxTs = undefined
allLogs = undefined
getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined)
}
)
watch(
() => [searchTerm, selected, timeframe, allLogs],
() => {
searchLogs(searchTerm, selected, minTsManual, maxTsManual, allLogs)
}
)
</script>
<Drawer bind:this={logDrawer} bind:open={logDrawerOpen} size="1400px">
@@ -477,71 +482,19 @@
class="flex flex-col lg:flex-row gap-y-1 justify-between w-full relative pb-4 gap-x-0.5"
id="service-logs-date-pickers"
>
<div class="flex relative">
<input
type="text"
value={minTsManual
? new Date(minTsManual).toLocaleTimeString([], {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
: 'min datetime'}
disabled
/>
<CalendarPicker
label="min datetime"
date={minTsManual}
on:change={({ detail }) => {
minTs = undefined
maxTs = undefined
allLogs = undefined
minTsManual = detail
getAllLogs(minTsManual, maxTsManual)
}}
placement="top-start"
/>
</div>
<ManuelDatePicker
bind:minTs={() => minTsManual ?? null, (v) => (minTsManual = v ?? undefined)}
bind:maxTs={() => maxTsManual ?? null, (v) => (maxTsManual = v ?? undefined)}
bind:this={manualPicker}
<TimeframeSelect
items={serviceLogsTimeframes}
bind:value={timeframe}
{loading}
on:loadJobs={() => {
wrapperClasses="w-full"
onClick={() => {
minTs = undefined
maxTs = undefined
allLogs = undefined
getAllLogs(minTsManual, maxTsManual)
const ts = timeframe.computeMinMax()
getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined)
}}
serviceLogsChoices
loadText={searchTerm === '' ? 'Last 1000 logfiles' : 'All time'}
/>
<div class="flex relative">
<input
type="text"
value={maxTsManual
? new Date(maxTsManual).toLocaleTimeString([], {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
: 'max datetime'}
disabled
/>
<CalendarPicker
label="max datetime"
date={maxTsManual}
on:change={({ detail }) => {
minTs = undefined
maxTs = undefined
allLogs = undefined
maxTsManual = detail
getAllLogs(minTsManual, maxTsManual)
}}
/>
</div>
</div>
<div class="flex w-full flex-row-reverse pb-4 -mt-2 gap-2"
><Toggle
@@ -1,125 +0,0 @@
<script lang="ts">
import { RefreshCw } from 'lucide-svelte'
import { Button } from '../common'
import { createEventDispatcher } from 'svelte'
interface Props {
minTs: string | null
maxTs: string | null
loading?: boolean
selectedManualDate?: number
loadText?: string | undefined
serviceLogsChoices?: boolean
numberOfLastJobsToFetch?: number
}
let {
minTs = $bindable(),
maxTs = $bindable(),
loading = false,
selectedManualDate = $bindable(0),
loadText = undefined,
serviceLogsChoices = false,
numberOfLastJobsToFetch = 1000
}: Props = $props()
export function computeMinMax(): { minTs: string; maxTs: string | null } | undefined {
return manualDates[selectedManualDate].computeMinMax()
}
export function resetChoice() {
selectedManualDate = 0
}
function computeMinMaxInc(inc: number) {
let minTs = new Date(new Date().getTime() - inc).toISOString()
let maxTs = null
return { minTs, maxTs }
}
const fixedManualDates: {
label: string
computeMinMax: () => { minTs: string; maxTs: string | null } | undefined
}[] = [
...(!serviceLogsChoices
? [
{
label: 'Within 30 seconds',
computeMinMax: () => computeMinMaxInc(30 * 1000)
},
{
label: 'Within last minute',
computeMinMax: () => computeMinMaxInc(60 * 1000)
}
]
: []),
{
label: 'Within last 5 minutes',
computeMinMax: () => computeMinMaxInc(5 * 60 * 1000)
},
{
label: 'Within last 30 minutes',
computeMinMax: () => computeMinMaxInc(30 * 60 * 1000)
},
{
label: 'Within last 24 hours',
computeMinMax: () => computeMinMaxInc(24 * 60 * 60 * 1000)
},
{
label: 'Within last 7 days',
computeMinMax: () => computeMinMaxInc(7 * 24 * 60 * 60 * 1000)
},
{
label: 'Within last month',
computeMinMax: () => computeMinMaxInc(30 * 24 * 60 * 60 * 1000)
}
]
let manualDates = $derived([
{
label: loadText ?? `Last ${numberOfLastJobsToFetch} runs`,
computeMinMax: () => {
return undefined
}
},
...fixedManualDates
])
const dispatch = createEventDispatcher()
</script>
<Button
unifiedSize="md"
variant="default"
on:click={() => {
const ts = computeMinMax()
if (ts) {
minTs = ts.minTs
maxTs = ts.maxTs
}
dispatch('loadJobs', { minTs, maxTs })
}}
dropdownItems={[
...manualDates.map((d, i) => ({
label: d.label,
onClick: (e) => {
e.preventDefault()
selectedManualDate = i
const ts = d.computeMinMax()
if (ts) {
minTs = ts.minTs
maxTs = ts.maxTs
} else {
minTs = null
maxTs = null
}
dispatch('loadJobs')
}
}))
]}
>
<div class="flex flex-row items-center gap-2">
<RefreshCw size={14} class={loading ? 'animate-spin' : ''} />
{manualDates[selectedManualDate].label}
</div>
</Button>
@@ -15,8 +15,6 @@
.default(null),
show_skipped: z.boolean().default(false),
show_schedules: z.boolean().default(true),
min_ts: z.string().nullable().default(null),
max_ts: z.string().nullable().default(null),
schedule_path: z.string().nullable().default(null),
job_kinds: z.string().default('runs'),
all_workspaces: z.boolean().default(false),
@@ -90,7 +88,6 @@
| 'tag'
| 'schedulePath'
small?: boolean
calendarSmall?: boolean
}
let {
@@ -116,9 +113,8 @@
usernames = [],
folders = [],
allWorkspaces = $bindable(),
filterBy = $bindable(),
small = false,
calendarSmall = false
filterBy = $bindable('path'),
small = false
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -526,7 +522,7 @@
<!-- Kind -->
<RunOption label="Kind" for="kind">
{#if small && !calendarSmall}
{#if small}
<Select
items={[
{ label: 'All', value: 'all' },
@@ -0,0 +1,184 @@
<script module lang="ts">
function computeMinMaxInc(inc: number) {
let minTs = new Date(new Date().getTime() - inc).toISOString()
let maxTs = new Date().toISOString()
return { minTs, maxTs }
}
export type Timeframe =
| {
label: string
computeMinMax: () => { minTs: string | null; maxTs: string | null }
type: 'dynamic'
}
| {
label: string
computeMinMax: () => { minTs: string | null; maxTs: string | null }
minTs: string | null
maxTs: string | null
type: 'manual'
}
export function buildManualTimeframe(minTs: string | null, maxTs: string | null): Timeframe {
return {
label: formatDateRange(minTs ?? undefined, maxTs ?? undefined),
minTs,
maxTs,
type: 'manual',
computeMinMax: () => ({ minTs, maxTs })
}
}
export const serviceLogsTimeframes: Timeframe[] = [
{ label: '1000 last service logs', computeMinMax: () => ({ minTs: null, maxTs: null }) },
{ label: 'Within last 5 minutes', computeMinMax: () => computeMinMaxInc(5 * 60 * 1000) },
{ label: 'Within last 30 minutes', computeMinMax: () => computeMinMaxInc(30 * 60 * 1000) },
{ label: 'Within last 24 hours', computeMinMax: () => computeMinMaxInc(24 * 60 * 60 * 1000) },
{ label: 'Within last 7 days', computeMinMax: () => computeMinMaxInc(7 * 24 * 60 * 60 * 1000) },
{ label: 'Within last month', computeMinMax: () => computeMinMaxInc(30 * 24 * 60 * 60 * 1000) }
].map((item) => ({ ...item, type: 'dynamic' }))
export const runsTimeframes: Timeframe[] = [
{ label: 'Latest runs', computeMinMax: () => ({ minTs: null, maxTs: null }) },
{ label: 'Within 30 seconds', computeMinMax: () => computeMinMaxInc(30 * 1000) },
{ label: 'Within last minute', computeMinMax: () => computeMinMaxInc(60 * 1000) },
{ label: 'Within last 5 minutes', computeMinMax: () => computeMinMaxInc(5 * 60 * 1000) },
{ label: 'Within last 30 minutes', computeMinMax: () => computeMinMaxInc(30 * 60 * 1000) },
{ label: 'Within last 24 hours', computeMinMax: () => computeMinMaxInc(24 * 60 * 60 * 1000) },
{ label: 'Within last 7 days', computeMinMax: () => computeMinMaxInc(7 * 24 * 60 * 60 * 1000) },
{ label: 'Within last month', computeMinMax: () => computeMinMaxInc(30 * 24 * 60 * 60 * 1000) }
].map((item) => ({ ...item, type: 'dynamic' }))
export function useUrlSyncedTimeframe(timeframes: Timeframe[]) {
let obj = $state({ timeframe: timeframes[0] })
let timeframe = $derived(obj.timeframe)
watch(
() => [page, timeframe],
() => {
const url = new URL(page.url)
if (timeframe.type === 'manual' && timeframe.minTs)
url.searchParams.set('min_ts', timeframe.minTs)
else url.searchParams.delete('min_ts')
if (timeframe.type === 'manual' && timeframe.maxTs)
url.searchParams.set('max_ts', timeframe.maxTs)
else url.searchParams.delete('max_ts')
if (timeframe.type === 'dynamic' && timeframe.label !== timeframes[0].label)
url.searchParams.set('timeframe', timeframe.label)
else url.searchParams.delete('timeframe')
history.replaceState(null, '', url)
}
)
if (page.url.searchParams.get('min_ts') || page.url.searchParams.get('max_ts')) {
obj.timeframe = buildManualTimeframe(
page.url.searchParams.get('min_ts') || null,
page.url.searchParams.get('max_ts') || null
)
} else {
const tfLabel = page.url.searchParams.get('timeframe')
const tf = timeframes.find((tf) => tf.label === tfLabel)
if (tf) obj.timeframe = { ...tf }
}
return obj
}
</script>
<script lang="ts">
import { CalendarIcon, RefreshCw } from 'lucide-svelte'
import { Button } from '../common'
import Popover from '../meltComponents/Popover.svelte'
import DateTimeInput from '../DateTimeInput.svelte'
import Label from '../Label.svelte'
import { formatDateRange } from '$lib/utils'
import { watch } from 'runed'
import { page } from '$app/state'
interface Props {
loading?: boolean
items: Timeframe[]
value: Timeframe
wrapperClasses?: string
onClick?: () => void
}
let { loading = false, onClick, items, value = $bindable(), wrapperClasses }: Props = $props()
let isOpen = $state(false)
function onManualInput(input: { minTs?: string | null; maxTs?: string | null }) {
if (value.type !== 'manual')
value = buildManualTimeframe(input.minTs ?? null, input.maxTs ?? null)
else
value = buildManualTimeframe(
'minTs' in input ? (input.minTs ?? null) : value.minTs,
'maxTs' in input ? (input.maxTs ?? null) : value.maxTs
)
if (value.type == 'manual' && value.minTs == null && value.maxTs == null) {
value = { ...items[0] }
}
}
</script>
<div class="relative flex {wrapperClasses}">
<Button
unifiedSize="md"
wrapperClasses="flex-1"
btnClasses="!rounded-r-none whitespace-nowrap"
onClick={() => onClick?.()}
>
<div class="flex flex-row items-center gap-2">
<RefreshCw size={14} class={loading ? 'animate-spin' : ''} />
{value.label}
</div>
</Button>
<Popover enableFlyTransition bind:isOpen>
{#snippet trigger()}
<Button
unifiedSize="md"
iconOnly
btnClasses="!rounded-l-none border-l-0"
endIcon={{ icon: CalendarIcon }}
/>
{/snippet}
{#snippet content()}
<div class="flex flex-col">
{#each items as item}
<Button
onClick={() => ((value = { ...item }), (isOpen = false))}
variant="subtle"
unifiedSize="md"
btnClasses="justify-start"
>
{item.label}
</Button>
{/each}
<div class="border-b"></div>
<div class="px-4 py-2 flex flex-col gap-2">
<Label label="From">
<DateTimeInput
clearable
bind:value={
() => (value.type === 'manual' ? value.minTs : undefined),
(v) => onManualInput({ minTs: v ?? null })
}
/>
</Label>
<Label label="To">
<DateTimeInput
clearable
bind:value={
() => (value.type === 'manual' ? value.maxTs : undefined),
(v) => onManualInput({ maxTs: v ?? null })
}
/>
</Label>
</div>
</div>
{/snippet}
</Popover>
</div>
@@ -16,6 +16,7 @@ import { tweened, type Tweened } from 'svelte/motion'
import { subtractDaysFromDateString } from '$lib/utils'
import { CancelablePromiseUtils } from '$lib/cancelable-promise-utils'
import type { RunsFilters } from './RunsFilter.svelte'
import type { Timeframe } from './TimeframeSelect.svelte'
export function computeJobKinds(jobKindsCat: string | null): string {
if (jobKindsCat == 'all') {
@@ -47,6 +48,7 @@ export function computeJobKinds(jobKindsCat: string | null): string {
export interface UseJobLoaderArgs {
currentWorkspace: string
filters?: Partial<RunsFilters>
timeframe?: Timeframe
jobKinds?: string
autoRefresh?: boolean
argError?: string
@@ -71,6 +73,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
let syncQueuedRunsCount = $derived(_args.syncQueuedRunsCount ?? true)
let lookback = $derived(_args.lookback ?? 0)
let onSetPerPage = $derived(_args.onSetPerPage)
let timeframe = $derived(_args?.timeframe)
let label = $derived(filters?.label ?? null)
let worker = $derived(filters?.worker ?? null)
@@ -90,8 +93,6 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
let folder = $derived(filters?.folder)
let path = $derived(filters?.path)
let argFilter = $derived(filters?.arg)
let minTs = $derived(filters?.min_ts ?? null)
let maxTs = $derived(filters?.max_ts ?? null)
let perPage = $derived(filters?.per_page ?? 100)
let queue_count: Tweened<number> | undefined = $state()
@@ -158,6 +159,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
// const minCreated = lastJob?.created_at
const minCreated = new Date(new Date(ts).getTime() - 1).toISOString()
const minTs = timeframe?.computeMinMax().minTs ?? null
let olderJobs = await fetchJobs(minCreated, minTs, undefined)
jobs = updateWithNewJobs(olderJobs ?? [], jobs ?? [])
computeCompletedJobs()
@@ -295,6 +297,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
intervalId = setInterval(syncer, refreshRate)
}
function loadJobsIntern(shouldGetCount?: boolean): CancelablePromise<void> {
const { minTs, maxTs } = timeframe?.computeMinMax() ?? { minTs: null, maxTs: null }
if (shouldGetCount) {
getCount()
}
@@ -378,6 +381,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
getCount()
}
const { minTs, maxTs } = timeframe?.computeMinMax() ?? { minTs: null, maxTs: null }
if (jobs && maxTs == undefined) {
if (success == 'running') {
loadJobsIntern(false)
@@ -519,6 +523,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
Object.keys(filters ?? {}).map((k) => filters?.[k as keyof RunsFilters])
currentWorkspace
lookback
timeframe
let p = untrack(() => onParamChanges())
return () => p.cancel()
})
+108
View File
@@ -1569,6 +1569,114 @@ export function formatDateShort(dateString: string | undefined): string {
}).format(date)
}
/**
* Formats a date range intelligently by omitting redundant information.
* Examples:
* - Before 01/03 7:32 PM (no "after" when only before date is in the past)
* - Before 01/03/2026 11:01 PM (different year)
* - 01:12 AM to 02:50 AM (same day)
* - 01/03 to 01/05 (same year, different days)
* - 12/31/2025 to 01/05/2026 (different years)
*
* @param before - The start/before date (can be string or Date or undefined)
* @param after - The end/after date (can be string or Date or undefined)
* @returns Formatted string representing the date range
*/
export function formatDateRange(
before: string | Date | undefined,
after: string | Date | undefined
): string {
const now = new Date()
const beforeDate = before ? new Date(before) : undefined
const afterDate = after ? new Date(after) : undefined
// Helper to format time only
const formatTime = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit'
}).format(date)
}
// Helper to format date without year
const formatDateNoYear = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
month: '2-digit',
day: '2-digit'
}).format(date)
}
// Helper to format date with year
const formatDateWithYear = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric'
}).format(date)
}
// Helper to check if dates are same day
const isSameDay = (d1: Date, d2: Date) => {
return (
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
)
}
// Helper to check if date is in current year
const isSameYear = (date: Date, reference: Date = now) => {
return date.getFullYear() === reference.getFullYear()
}
// Only before date provided
if (beforeDate && !afterDate) {
const timeStr = formatTime(beforeDate)
// If it's today, only show time
if (isSameDay(beforeDate, now)) {
return `Before ${timeStr}`
}
const needsYear = !isSameYear(beforeDate)
const dateStr = needsYear ? formatDateWithYear(beforeDate) : formatDateNoYear(beforeDate)
return `Before ${dateStr} ${timeStr}`
}
// Only after date provided
if (afterDate && !beforeDate) {
const timeStr = formatTime(afterDate)
// If it's today, only show time
if (isSameDay(afterDate, now)) {
return `After ${timeStr}`
}
const needsYear = !isSameYear(afterDate)
const dateStr = needsYear ? formatDateWithYear(afterDate) : formatDateNoYear(afterDate)
return `After ${dateStr} ${timeStr}`
}
// Both dates provided
if (beforeDate && afterDate) {
// Same day - only show times
if (isSameDay(beforeDate, afterDate)) {
return `${formatTime(beforeDate)} to ${formatTime(afterDate)}`
}
// Different days, same year
if (isSameYear(beforeDate, afterDate)) {
return `${formatDateNoYear(beforeDate)} to ${formatDateNoYear(afterDate)}`
}
// Different years
return `${formatDateWithYear(beforeDate)} to ${formatDateWithYear(afterDate)}`
}
// No dates provided
return ''
}
export function toJsonStr(result: any) {
try {
// console.log(result)