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}