From 64ada4e26dfc69b734d3710eca1af2f6bca6200f Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 5 Oct 2026 06:47:04 -0700 Subject: [PATCH] feat: service logs and the audit filters move into the band MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service logs: the page's name is the breadcrumb's, so the `

` is gone and the search field, the timeframe and the two switches sit in the band beside it, with the documentation tooltip after the name. ServiceLogsInner registers the band itself, the way RunsPage does, since that route is its only caller, and the `title` snippet the page used to hand it goes with the heading. The search is the widest control there, because searching is what the page is for, and it is the one that gives width back under pressure. The two switches fold into a popover below a 1200px bar rather than at a phone's width, so the field keeps its full width down to a much narrower window — folded rather than dropped, because the page no longer carries a toolbar they could fall back to. The timeframe control took no size of its own, so it defaulted to `md` beside a `sm` field, and sat in a `w-48` box 20px narrower than its content, whose overflow is visible and so spilled into its neighbours. No `separator`: the two panes below start under the bar with borders of their own, so at rest there is nothing for the band to divide. Audit logs: the filters lose their labels. Each select says what it filters while unset — "all usernames", "all resources" — and once set the value names the field by itself. From and To become the TimeframeSelect the runs page uses, which absorbs the standalone Refresh button, since that control's left half is the refresh. `auditTimeframes` joins the runs and service-logs presets, with "All time" first. The bounds stay the filters' own `after`/`before` rather than a remembered preset label: the timeline's zoom writes them too, and a label could then contradict the chart. That takes the row from 1326px to 898px, so it stays inline down to a ~1500px viewport instead of ~2500. The fold measures the row against the bar less what the breadcrumb takes, plus a gap the two must never close — the breadcrumb yields width grudgingly and the row not at all, so sized to just meet they overlap rather than compress, and mid-resize the tooltip after the page's name lands on the timeframe button. Co-Authored-By: Claude Opus 5 (1M context) --- .../lib/components/ServiceLogsInner.svelte | 225 +++++++++++------- .../auditLogs/AuditLogsFilters.svelte | 135 +++++------ .../components/runs/TimeframeSelect.svelte | 3 +- .../src/lib/components/runs/timeframes.ts | 9 + .../(root)/(logged)/audit_logs/+page.svelte | 11 +- .../(root)/(logged)/service_logs/+page.svelte | 13 +- 6 files changed, 215 insertions(+), 181 deletions(-) diff --git a/frontend/src/lib/components/ServiceLogsInner.svelte b/frontend/src/lib/components/ServiceLogsInner.svelte index 85c6cf7fb9..03cd5bf676 100644 --- a/frontend/src/lib/components/ServiceLogsInner.svelte +++ b/frontend/src/lib/components/ServiceLogsInner.svelte @@ -10,8 +10,10 @@ import Toggle from './Toggle.svelte' import TextInput from './text_input/TextInput.svelte' import Popover from './Popover.svelte' + // The clickable one, for the folded switches; the import above is the hover tooltip. + import MeltPopover from './meltComponents/Popover.svelte' import { sendUserToast } from '$lib/toast' - import { onDestroy, tick, type Snippet } from 'svelte' + import { onDestroy, tick } from 'svelte' import { fade } from 'svelte/transition' import { AlertTriangle, @@ -22,7 +24,8 @@ Loader2, ScrollText, Search, - SearchX + SearchX, + SlidersHorizontal } from 'lucide-svelte' import { copyToClipboard, scroll_into_view_if_needed_polyfill, truncateRev } from '$lib/utils' import LogSnippetViewer from './LogSnippetViewer.svelte' @@ -33,15 +36,24 @@ import { goto } from '$lib/navigation' import { page } from '$app/state' import { watch } from 'runed' + import Tooltip from './Tooltip.svelte' + import PageHeaderContent from './PageHeaderContent.svelte' + import { pageHeader } from './pageHeaderRegistry.svelte' interface Props { searchTerm: string tagLabel?: string - /** Rendered at the start of the toolbar row, so the page title shares it. */ - title?: Snippet } - let { searchTerm = $bindable(), tagLabel, title }: Props = $props() + let { searchTerm = $bindable(), tagLabel }: Props = $props() + + /** Bar width below which the two switches fold into a popover. Measured: the breadcrumb takes + * ~360, the search field asks for 384 and the timeframe 223, so under this the field is what + * would start giving width back — and it is the control the page exists for. */ + const COMPACT_SWITCHES_BAR = 1200 + const compactSwitches = $derived( + pageHeader.barWidth > 0 && pageHeader.barWidth < COMPACT_SWITCHES_BAR + ) let queryParseErrors: string[] | undefined = $state(undefined) @@ -692,88 +704,135 @@ -
-
- {@render title?.()} -
- - - {#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0} -
- - - {#snippet text()} -
- Some search terms were ignored because they could not be parsed: -
    - {#each queryParseErrors ?? [] as msg} -
  • {msg}
  • - {/each} -
-
- {/snippet} -
-
- {/if} -
- { - minTs = undefined - maxTs = undefined - allLogs = undefined - const ts = timeframe.computeMinMax() - getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined) +{#snippet serviceLogsHint()} + + Logs written by the servers, workers and indexers of this instance, grouped by host. + +{/snippet} + +{#snippet serviceLogsActions()} + +
+ + -
- { - allLogs = undefined - getAllLogs(minTs, maxTs) - }} - /> - { - if (e.detail) { - getAllLogs(maxTs, undefined) - } else { - timeout && clearTimeout(timeout) - } - }} - options={{ - right: 'Auto-refresh', - rightTooltip: 'Fetch new log files every 5 seconds. Paused while searching.' - }} - /> -
+ {#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0} +
+ + + {#snippet text()} +
+ Some search terms were ignored because they could not be parsed: +
    + {#each queryParseErrors ?? [] as msg} +
  • {msg}
  • + {/each} +
+
+ {/snippet} +
+
+ {/if}
+ + { + minTs = undefined + maxTs = undefined + allLogs = undefined + const ts = timeframe.computeMinMax() + getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined) + }} + /> + + {#if compactSwitches} + + {#snippet trigger()} +
diff --git a/frontend/src/lib/components/runs/TimeframeSelect.svelte b/frontend/src/lib/components/runs/TimeframeSelect.svelte index f376b082d7..7daa75f5fa 100644 --- a/frontend/src/lib/components/runs/TimeframeSelect.svelte +++ b/frontend/src/lib/components/runs/TimeframeSelect.svelte @@ -3,7 +3,8 @@ type Timeframe, buildManualTimeframe, serviceLogsTimeframes, - runsTimeframes + runsTimeframes, + auditTimeframes } from './timeframes' import { type Timeframe, buildManualTimeframe } from './timeframes' diff --git a/frontend/src/lib/components/runs/timeframes.ts b/frontend/src/lib/components/runs/timeframes.ts index 9e46dd1bd8..dbc624d323 100644 --- a/frontend/src/lib/components/runs/timeframes.ts +++ b/frontend/src/lib/components/runs/timeframes.ts @@ -39,6 +39,15 @@ export const serviceLogsTimeframes: Timeframe[] = [ { label: 'Within last month', computeMinMax: () => computeMinMaxInc(30 * 24 * 60 * 60 * 1000) } ].map((item) => ({ ...item, type: 'dynamic' })) +export const auditTimeframes: Timeframe[] = [ + { label: 'All time', computeMinMax: () => ({ minTs: null, maxTs: null }) }, + { 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) }, + { label: 'Within last year', computeMinMax: () => computeMinMaxInc(365 * 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) }, diff --git a/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte b/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte index c774e2db1f..42d8ff996e 100644 --- a/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte @@ -73,13 +73,18 @@ // fits inside itself, which it always does, and the filters would never fold. let filtersRowWidth = $state(0) /** What the breadcrumb takes before the filters get any: the workspace disc and name, the - * page's name and the hint beside it. Measured at 361-398px across bar widths; the round - * number above that is what keeps a row that only just fits from overrunning the bar. */ + * page's name and the hint beside it. Measured at 361-398px across bar widths. */ const BREADCRUMB_WIDTH = 400 + /** Clear space held between the breadcrumb and the first filter. The breadcrumb yields width + * grudgingly and the filter row not at all, so when the two are sized to just meet they + * overlap instead of compressing — and mid-resize, before the breadcrumb has settled, the + * tooltip at the end of it lands on the timeframe button. Measured without this: the gap + * reached 4px across a 1600->1200 sweep. */ + const BREADCRUMB_GAP = 48 let inlineFilters = $derived( filtersRowWidth > 0 && pageHeader.barWidth > 0 && - filtersRowWidth <= pageHeader.barWidth - BREADCRUMB_WIDTH + filtersRowWidth <= pageHeader.barWidth - BREADCRUMB_WIDTH - BREADCRUMB_GAP ) // Function to fetch missing job execution audit logs diff --git a/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte b/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte index bb69e8c916..124390aabe 100644 --- a/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte @@ -2,7 +2,6 @@ import { page } from '$app/state' import { Alert } from '$lib/components/common' import ServiceLogsInner from '$lib/components/ServiceLogsInner.svelte' - import Tooltip from '$lib/components/Tooltip.svelte' import { devopsRole } from '$lib/stores' // `query` is what the global search links with; `searchTerm` is what the page writes back. @@ -11,26 +10,16 @@ ) -{#snippet title()} -
-

Service logs

- - Logs written by the servers, workers and indexers of this instance, grouped by host. - -
-{/snippet} -
{#if $devopsRole == false}
- {@render title()} Ask one of them to look at the logs, or to grant you the devops role.
{:else} - + {/if}