mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 08:02:58 +00:00
feat: service logs and the audit filters move into the band
Service logs: the page's name is the breadcrumb's, so the `<h1>` 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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0f7b0f6a44
commit
64ada4e26d
@@ -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 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col grow min-h-0 min-w-0 gap-2 pb-2">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 pt-4 pb-2">
|
||||
{@render title?.()}
|
||||
<div class="relative grow min-w-64">
|
||||
<Search
|
||||
size={16}
|
||||
class="absolute left-2 top-1/2 -translate-y-1/2 text-hint pointer-events-none z-10"
|
||||
/>
|
||||
<TextInput
|
||||
bind:value={searchTerm}
|
||||
size="md"
|
||||
class="pl-8 pr-8"
|
||||
inputProps={{
|
||||
id: 'quickSearchInput',
|
||||
placeholder: 'Search service logs',
|
||||
autocomplete: 'off',
|
||||
autofocus: true
|
||||
}}
|
||||
/>
|
||||
{#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0}
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 z-10 flex">
|
||||
<Popover notClickable placement="bottom-end">
|
||||
<AlertTriangle size={16} class="text-yellow-500" />
|
||||
{#snippet text()}
|
||||
<div class="flex flex-col gap-1 text-xs">
|
||||
<span>Some search terms were ignored because they could not be parsed:</span>
|
||||
<ul class="list-disc pl-4">
|
||||
{#each queryParseErrors ?? [] as msg}
|
||||
<li>{msg}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<TimeframeSelect
|
||||
items={serviceLogsTimeframes}
|
||||
bind:value={timeframe}
|
||||
{loading}
|
||||
wrapperClasses="w-64"
|
||||
onClick={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
allLogs = undefined
|
||||
const ts = timeframe.computeMinMax()
|
||||
getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined)
|
||||
{#snippet serviceLogsHint()}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/service_logs">
|
||||
Logs written by the servers, workers and indexers of this instance, grouped by host.
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
|
||||
{#snippet serviceLogsActions()}
|
||||
<!-- The widest control here, because searching is what the page is for. It is also the one that
|
||||
gives width back when the bar is tight — that is what `actionsFlexible` on the registration
|
||||
asks the bar to allow — down to a floor that still fits a query worth reading. -->
|
||||
<div class="relative w-96 min-w-40 shrink">
|
||||
<Search
|
||||
size={16}
|
||||
class="absolute left-2 top-1/2 -translate-y-1/2 text-hint pointer-events-none z-10"
|
||||
/>
|
||||
<TextInput
|
||||
bind:value={searchTerm}
|
||||
size="sm"
|
||||
class="pl-8 pr-8"
|
||||
inputProps={{
|
||||
id: 'quickSearchInput',
|
||||
placeholder: 'Search service logs',
|
||||
autocomplete: 'off',
|
||||
autofocus: true
|
||||
}}
|
||||
/>
|
||||
<div class="flex items-center gap-4">
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={withError}
|
||||
options={{
|
||||
right: 'With errors only',
|
||||
rightTooltip: 'Only list the log files that contain at least one error line'
|
||||
}}
|
||||
on:change={() => {
|
||||
allLogs = undefined
|
||||
getAllLogs(minTs, maxTs)
|
||||
}}
|
||||
/>
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={autoRefresh}
|
||||
disabled={searchTerm != ''}
|
||||
on:change={(e) => {
|
||||
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.'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0}
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 z-10 flex">
|
||||
<Popover notClickable placement="bottom-end">
|
||||
<AlertTriangle size={16} class="text-yellow-500" />
|
||||
{#snippet text()}
|
||||
<div class="flex flex-col gap-1 text-xs">
|
||||
<span>Some search terms were ignored because they could not be parsed:</span>
|
||||
<ul class="list-disc pl-4">
|
||||
{#each queryParseErrors ?? [] as msg}
|
||||
<li>{msg}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- No width of its own: the preset labels run to "1000 last service logs", and a fixed box
|
||||
narrower than that does not clip it — the control's overflow is visible, so it spills into
|
||||
whatever sits next to it in the bar. -->
|
||||
<TimeframeSelect
|
||||
unifiedSize="sm"
|
||||
items={serviceLogsTimeframes}
|
||||
bind:value={timeframe}
|
||||
{loading}
|
||||
wrapperClasses="shrink-0"
|
||||
onClick={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
allLogs = undefined
|
||||
const ts = timeframe.computeMinMax()
|
||||
getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined)
|
||||
}}
|
||||
/>
|
||||
<!-- The two labelled switches are the widest thing in this row, and the search field is the one
|
||||
that pays for them: it is the only control that yields width. So they fold into a popover
|
||||
well before the bar is tight, which keeps the field at its full width down to a much
|
||||
narrower window. Folded, not dropped — the page below no longer carries a toolbar they
|
||||
could fall back to, so hiding them would put them out of reach. -->
|
||||
{#if compactSwitches}
|
||||
<MeltPopover placement="bottom-end">
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
iconOnly
|
||||
startIcon={{ icon: SlidersHorizontal }}
|
||||
title="Log listing options"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-col gap-3 p-3">
|
||||
{@render logSwitches()}
|
||||
</div>
|
||||
{/snippet}
|
||||
</MeltPopover>
|
||||
{:else}
|
||||
<div class="flex items-center gap-4 shrink-0">
|
||||
{@render logSwitches()}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet logSwitches()}
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={withError}
|
||||
options={{
|
||||
right: 'With errors only',
|
||||
rightTooltip: 'Only list the log files that contain at least one error line'
|
||||
}}
|
||||
on:change={() => {
|
||||
allLogs = undefined
|
||||
getAllLogs(minTs, maxTs)
|
||||
}}
|
||||
/>
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={autoRefresh}
|
||||
disabled={searchTerm != ''}
|
||||
on:change={(e) => {
|
||||
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.'
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<!-- The page's name is the breadcrumb's, so the band carries the hint beside it and the toolbar at
|
||||
its far end; the page below starts straight into the logs.
|
||||
No `separator`: the default draws the edge only while something is passing under the bar, and
|
||||
the two panes below start lower than it with borders of their own, so at rest there is nothing
|
||||
for the band to divide. -->
|
||||
<PageHeaderContent afterName={serviceLogsHint} actions={serviceLogsActions} actionsFlexible />
|
||||
|
||||
<div class="flex flex-col grow min-h-0 min-w-0 gap-2 py-2">
|
||||
{#if searchError}
|
||||
<Alert type="warning" title="Service logs search unavailable" size="xs">
|
||||
{searchError}
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
import type { ActionKind } from '$lib/common'
|
||||
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import CalendarPicker from '$lib/components/common/calendarPicker/CalendarPicker.svelte'
|
||||
import TimeframeSelect, {
|
||||
auditTimeframes,
|
||||
useSyncedTimeframe
|
||||
} from '$lib/components/runs/TimeframeSelect.svelte'
|
||||
import {
|
||||
type AuditLog,
|
||||
ResourceService,
|
||||
@@ -29,7 +32,7 @@
|
||||
} from '$lib/gen'
|
||||
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { ChevronDown, Download, Loader2, RefreshCcw } from 'lucide-svelte'
|
||||
import { ChevronDown, Download } from 'lucide-svelte'
|
||||
import { onDestroy, onMount, untrack } from 'svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
@@ -266,22 +269,34 @@
|
||||
untrack(() => updateQueryParams())
|
||||
})
|
||||
|
||||
// `leading-none`: the label's own line box is what decides whether a stacked field fits the
|
||||
// header band. At the default leading the field is 52px against a 50px bar and the label is
|
||||
// clipped off the top; at the font's own height it is 44px.
|
||||
const fieldLabel = 'text-xs font-semibold text-emphasis shrink-0 leading-none'
|
||||
// The bounds stay the filters' own `after`/`before`, because the timeline's zoom writes them
|
||||
// too; picking a preset resolves it to a concrete pair here rather than keeping a label that
|
||||
// the chart could then contradict.
|
||||
const _timeframe = useSyncedTimeframe(
|
||||
auditTimeframes,
|
||||
() => ({ minTs: after, maxTs: before }),
|
||||
(v) => {
|
||||
const { minTs, maxTs } = v.timeframe
|
||||
? (auditTimeframes.find((t) => t.label === v.timeframe)?.computeMinMax() ?? {
|
||||
minTs: null,
|
||||
maxTs: null
|
||||
})
|
||||
: { minTs: v.minTs ?? null, maxTs: v.maxTs ?? null }
|
||||
after = minTs ?? undefined
|
||||
before = maxTs ?? undefined
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<!-- Label above input, 4px apart, fields 8px apart — the form layout in brand-guidelines.md. The
|
||||
labels used to be positioned absolutely above the controls, which put them outside the box and
|
||||
clipped them against the top of the window once these filters moved into the header band. -->
|
||||
<!-- `inline` rather than a breakpoint: the caller measures whether the row fits the width the band
|
||||
<!-- No labels above the controls: each one says what it filters while it is unset ("all usernames"),
|
||||
and once it is set the value names the field by itself. That halves the row's height, which is
|
||||
what lets it sit in the header band, and halves its width, which is what lets it stay there.
|
||||
`inline` rather than a breakpoint: the caller measures whether the row fits the width the band
|
||||
leaves it, which a media query cannot see — the sidebar, a side panel and the filters' own
|
||||
values all move that width without moving the viewport's. -->
|
||||
<div class={inline ? 'flex flex-row gap-2 items-end pr-2' : 'flex flex-col gap-2'}>
|
||||
<div class={inline ? 'flex flex-row gap-2 items-center pr-2' : 'flex flex-col gap-2'}>
|
||||
{#if $workspaceStore == 'admins'}
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>Scope</span>
|
||||
<div class="flex min-w-0 shrink-0">
|
||||
<ToggleButtonGroup
|
||||
selected={scope ?? 'admins'}
|
||||
on:selected={({ detail }) => {
|
||||
@@ -311,45 +326,22 @@
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>From</span>
|
||||
<div class="flex items-center relative min-w-0 bg-surface-input">
|
||||
<input type="text" class="!h-7 !text-xs" value={after ?? ''} disabled />
|
||||
<CalendarPicker
|
||||
clearable
|
||||
date={after}
|
||||
placement="bottom-end"
|
||||
label="From"
|
||||
on:change={({ detail }) => {
|
||||
after = new Date(detail).toISOString()
|
||||
}}
|
||||
on:clear={() => {
|
||||
after = undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>To</span>
|
||||
<div class="flex items-center relative min-w-0 bg-surface-input">
|
||||
<input type="text" class="!h-7 !text-xs" value={before ?? ''} disabled />
|
||||
<CalendarPicker
|
||||
clearable
|
||||
bind:date={before}
|
||||
label="To"
|
||||
placement="bottom-end"
|
||||
on:change={({ detail }) => {
|
||||
before = new Date(detail).toISOString()
|
||||
}}
|
||||
on:clear={() => {
|
||||
before = undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The same control the runs page uses: presets and a two-calendar range in one popover,
|
||||
in place of the two date fields this row used to carry. -->
|
||||
<TimeframeSelect
|
||||
unifiedSize="sm"
|
||||
items={auditTimeframes}
|
||||
bind:value={_timeframe.val}
|
||||
{loading}
|
||||
wrapperClasses="shrink-0"
|
||||
onClick={() => {
|
||||
loadUsers()
|
||||
resources.refresh()
|
||||
onRefresh?.()
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>Username</span>
|
||||
<div class="w-40 min-w-0 shrink">
|
||||
<Select
|
||||
class="w-full"
|
||||
size="sm"
|
||||
@@ -358,7 +350,7 @@
|
||||
items={usernames
|
||||
? [
|
||||
...($userStore?.is_admin || $userStore?.is_super_admin
|
||||
? [{ value: 'all', label: 'all' }]
|
||||
? [{ value: 'all', label: 'all usernames' }]
|
||||
: []),
|
||||
...usernames.map((e) => ({
|
||||
value: e,
|
||||
@@ -369,41 +361,41 @@
|
||||
: []}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>Resource</span>
|
||||
<div class="w-40 min-w-0 shrink">
|
||||
<Select
|
||||
class="w-full"
|
||||
size="sm"
|
||||
onCreateItem={(r) => (resources.value?.push(r), (resource = r))}
|
||||
createText="Press enter to use this value"
|
||||
bind:value={resource}
|
||||
items={safeSelectItems(['all', ...(resources.value ?? [])])}
|
||||
items={[{ value: 'all', label: 'all resources' }, ...safeSelectItems(resources.value ?? [])]}
|
||||
inputClass="dark:!bg-gray-700"
|
||||
RightIcon={ChevronDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>Operation</span>
|
||||
<div class="w-40 min-w-0 shrink">
|
||||
<Select
|
||||
class="w-full"
|
||||
size="sm"
|
||||
bind:value={operation}
|
||||
items={['all', ...Object.values(operations)].map((r) => ({ value: r, label: r }))}
|
||||
items={[
|
||||
{ value: 'all', label: 'all operations' },
|
||||
...Object.values(operations).map((r) => ({ value: r, label: r }))
|
||||
]}
|
||||
inputClass="dark:!bg-gray-700"
|
||||
RightIcon={ChevronDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class={fieldLabel}>Action</span>
|
||||
<div class="w-36 min-w-0 shrink">
|
||||
<Select
|
||||
class="w-full"
|
||||
size="sm"
|
||||
bind:value={actionKind}
|
||||
RightIcon={ChevronDown}
|
||||
items={[
|
||||
{ value: 'all', label: 'all' },
|
||||
{ value: 'all', label: 'all actions' },
|
||||
{ value: 'create', label: 'Create' },
|
||||
{ value: 'update', label: 'Update' },
|
||||
{ value: 'delete', label: 'Delete' },
|
||||
@@ -412,8 +404,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- The buttons carry no label of their own, so they line up with the inputs beside them rather
|
||||
than with the labels above those. -->
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<Button
|
||||
variant="subtle"
|
||||
@@ -439,24 +429,5 @@
|
||||
startIcon={{ icon: Download }}
|
||||
iconOnly
|
||||
/>
|
||||
<Button
|
||||
variant="accent"
|
||||
on:click={() => {
|
||||
loadUsers()
|
||||
resources.refresh()
|
||||
onRefresh?.()
|
||||
}}
|
||||
unifiedSize="sm"
|
||||
wrapperClasses="ml-auto"
|
||||
>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{#if loading}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<RefreshCcw size={14} />
|
||||
{/if}
|
||||
Refresh
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
type Timeframe,
|
||||
buildManualTimeframe,
|
||||
serviceLogsTimeframes,
|
||||
runsTimeframes
|
||||
runsTimeframes,
|
||||
auditTimeframes
|
||||
} from './timeframes'
|
||||
import { type Timeframe, buildManualTimeframe } from './timeframes'
|
||||
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
{#snippet title()}
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-2xl font-semibold text-emphasis whitespace-nowrap">Service logs</h1>
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/service_logs">
|
||||
Logs written by the servers, workers and indexers of this instance, grouped by host.
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- `h-full`, not `h-screen`: the page header band sits above this box, so a viewport floor here
|
||||
would overhang the content box by the band's height. -->
|
||||
<div class="flex flex-col w-full min-w-0 h-full max-h-full px-4">
|
||||
{#if $devopsRole == false}
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
{@render title()}
|
||||
<Alert title="Service logs are only available to superadmins and devops users" type="warning">
|
||||
Ask one of them to look at the logs, or to grant you the devops role.
|
||||
</Alert>
|
||||
</div>
|
||||
{:else}
|
||||
<ServiceLogsInner bind:searchTerm {title} />
|
||||
<ServiceLogsInner bind:searchTerm />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user