Resolve ui stuff
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type AlertRule,
|
||||
type CompareOp,
|
||||
} from '../lib/api'
|
||||
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { queryInputClassName } from './FilterBar'
|
||||
|
||||
@@ -264,12 +265,14 @@ export function Alerts() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-border p-3 text-sm">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
||||
Alerts
|
||||
<HelpIcon sectionId="alerts" label="Help: Alerts" />
|
||||
<div className="border-b border-border text-sm">
|
||||
<div className={sectionHeaderClassName}>
|
||||
<div className={sectionHeaderLeftClassName}>
|
||||
Alerts
|
||||
<HelpIcon sectionId="alerts" label="Help: Alerts" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-4 p-3">
|
||||
<form onSubmit={handleSubmit} className="w-96 shrink-0 space-y-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import type { TimeRange } from './LogTimeline'
|
||||
|
||||
/// Shared look for a primary section's title bar: shaded so it reads as a
|
||||
/// distinct band rather than blending into the section body below it.
|
||||
export const sectionHeaderClassName =
|
||||
'flex items-center justify-between gap-2 border-b border-border-soft bg-surface-1 px-3 py-1.5'
|
||||
|
||||
/// Left-hand cluster of a section header: title text, help icon, collapse/pop-out controls.
|
||||
export const sectionHeaderLeftClassName = 'flex items-center gap-1.5 text-sm font-medium text-text-secondary'
|
||||
|
||||
/// localStorage key holding a record of section id -> collapsed, mirroring LogPreview's VISIBLE_PARAM_COLUMNS_STORAGE_KEY pattern.
|
||||
const COLLAPSED_SECTIONS_STORAGE_KEY = 'ottyl.collapsedSections'
|
||||
|
||||
@@ -46,7 +54,7 @@ export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; on
|
||||
title={collapsed ? 'Expand section' : 'Collapse section'}
|
||||
aria-label={collapsed ? 'Expand section' : 'Collapse section'}
|
||||
aria-expanded={!collapsed}
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||
>
|
||||
{collapsed ? '▸' : '▾'}
|
||||
</button>
|
||||
@@ -73,7 +81,7 @@ export function PopOutButton({ section, range }: { section: PopOutSectionId; ran
|
||||
onClick={() => window.open(popOutUrl(section, range), '_blank')}
|
||||
title="Open in new tab"
|
||||
aria-label="Open in new tab"
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||
>
|
||||
↗
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,13 @@ import {
|
||||
type Panel,
|
||||
type PromqlSeries,
|
||||
} from '../lib/api'
|
||||
import { CollapseToggle, PopOutButton, useSectionCollapsed } from './CollapsibleSection'
|
||||
import {
|
||||
CollapseToggle,
|
||||
PopOutButton,
|
||||
sectionHeaderClassName,
|
||||
sectionHeaderLeftClassName,
|
||||
useSectionCollapsed,
|
||||
} from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
|
||||
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
|
||||
@@ -210,27 +216,40 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
|
||||
|
||||
if (!activeProfile) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 border-b border-border p-3 text-xs text-text-faint">
|
||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="dashboards" range={range} />
|
||||
Dashboards — activate a profile (see the settings menu) to create and save dashboards.
|
||||
<div className="border-b border-border text-sm">
|
||||
<div className={sectionHeaderClassName}>
|
||||
<div className={sectionHeaderLeftClassName}>
|
||||
Dashboards
|
||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="dashboards" range={range} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 text-xs text-text-faint">
|
||||
Activate a profile (see the settings menu) to create and save dashboards.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-border p-3 text-sm">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="dashboards" range={range} />
|
||||
Dashboards — profile "{activeProfile}"
|
||||
<div className="border-b border-border text-sm">
|
||||
<div className={sectionHeaderClassName}>
|
||||
<div className={sectionHeaderLeftClassName}>
|
||||
Dashboards — profile "{activeProfile}"
|
||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="dashboards" range={range} />
|
||||
</div>
|
||||
</div>
|
||||
{error && !collapsed && (
|
||||
<div className="mb-2 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{error}</div>
|
||||
<div className="mx-3 mt-3 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{error}</div>
|
||||
)}
|
||||
<div className="flex gap-4" hidden={collapsed}>
|
||||
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||
<div className="w-56 shrink-0 space-y-3">
|
||||
<form onSubmit={handleCreateDashboard} className="flex gap-1">
|
||||
<input
|
||||
|
||||
@@ -9,7 +9,7 @@ export function HelpIcon({ sectionId, label }: { sectionId: string; label?: stri
|
||||
onClick={() => openSection(sectionId)}
|
||||
title="Open docs"
|
||||
aria-label={label ?? 'Open docs for this section'}
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { metricPrimitiveToDisplayString, type LogEntry, type ParamDeclaration } from '../lib/api'
|
||||
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
|
||||
// Namespaced under `ottyl.` so future per-viewer preferences elsewhere in the app won't collide with this key.
|
||||
@@ -102,8 +103,11 @@ export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDe
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b border-border-faint px-3 py-1">
|
||||
<HelpIcon sectionId="log-preview" label="Help: Log preview" />
|
||||
<div className={sectionHeaderClassName}>
|
||||
<div className={sectionHeaderLeftClassName}>
|
||||
Log preview
|
||||
<HelpIcon sectionId="log-preview" label="Help: Log preview" />
|
||||
</div>
|
||||
<ColumnPicker paramNames={paramNames} visible={visible} onToggle={toggleColumn} />
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import type { TimelineBucket } from '../lib/api'
|
||||
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
|
||||
export interface TimeRange {
|
||||
@@ -26,6 +27,14 @@ export function computePresetRange(id: TimeRangePresetId): TimeRange | null {
|
||||
return { startMs: now - preset.ms, endMs: now }
|
||||
}
|
||||
|
||||
/// Buckets a fixed range into this many slots, so e.g. a rolling `5m` preset always requests a resolution fine enough to visibly slide instead of a handful of coarse, jumpy bars.
|
||||
const TIMELINE_TARGET_BUCKETS = 60
|
||||
|
||||
/// Callers with an explicit (non-`all`) range pass this as `bucketMs` to `fetchTimeline` — `LogTimeline` derives the same value from its own `domainSpan` when laying out bars, so requested and rendered resolution always agree.
|
||||
export function computeBucketMs(domainSpanMs: number): number {
|
||||
return Math.max(1000, Math.round(domainSpanMs / TIMELINE_TARGET_BUCKETS))
|
||||
}
|
||||
|
||||
function formatTimestamp(ms: number): string {
|
||||
return new Date(ms).toLocaleTimeString()
|
||||
}
|
||||
@@ -52,6 +61,8 @@ export function LogTimeline({
|
||||
const domainStart = range?.startMs ?? buckets[0]?.start_ms ?? 0
|
||||
const domainEnd = range?.endMs ?? Date.now()
|
||||
const domainSpan = Math.max(1, domainEnd - domainStart)
|
||||
// Matches what the caller requested via `computeBucketMs` for a fixed range; with no range ('all') there's no such request, so fall back to the actual spacing between returned buckets.
|
||||
const bucketMs = range ? computeBucketMs(domainSpan) : buckets.length >= 2 ? buckets[1].start_ms - buckets[0].start_ms : domainSpan
|
||||
const firstBucket = buckets[0] ?? null
|
||||
const lastBucket = buckets[buckets.length - 1] ?? null
|
||||
|
||||
@@ -103,13 +114,13 @@ export function LogTimeline({
|
||||
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0
|
||||
|
||||
return (
|
||||
<div className="border-b border-border p-3">
|
||||
<div className="mb-1 flex items-center justify-between text-xs text-text-subtle">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<div className="border-b border-border">
|
||||
<div className={sectionHeaderClassName}>
|
||||
<span className={sectionHeaderLeftClassName}>
|
||||
Timeline
|
||||
<HelpIcon sectionId="timeline" label="Help: Timeline" />
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 text-xs text-text-subtle">
|
||||
<div className="flex items-center gap-1">
|
||||
{TIME_RANGE_PRESETS.map((preset) => (
|
||||
<button
|
||||
@@ -134,9 +145,10 @@ export function LogTimeline({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative h-24 w-full cursor-crosshair select-none rounded bg-surface-1"
|
||||
className="relative h-24 w-full cursor-crosshair select-none overflow-hidden rounded bg-surface-1"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
@@ -146,12 +158,17 @@ export function LogTimeline({
|
||||
setHoverX(null)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full w-full items-end gap-px overflow-hidden px-1">
|
||||
{/* Positioned by actual elapsed time within the domain (not evenly spread via flex), so a rolling preset like `5m` visibly slides — a bucket's left/width tracks its real place in the window instead of jumping to a fixed slot as buckets enter/leave it. */}
|
||||
<div className="absolute inset-x-1 bottom-0 top-0">
|
||||
{buckets.map((b) => (
|
||||
<div
|
||||
key={b.start_ms}
|
||||
className="min-w-[2px] flex-1 rounded-t bg-accent/70"
|
||||
style={{ height: `${(b.count / maxCount) * 100}%` }}
|
||||
className="absolute bottom-0 min-w-[2px] rounded-t bg-accent/70"
|
||||
style={{
|
||||
left: `${((b.start_ms - domainStart) / domainSpan) * 100}%`,
|
||||
width: `${(bucketMs / domainSpan) * 100}%`,
|
||||
height: `${(b.count / maxCount) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -188,6 +205,7 @@ export function LogTimeline({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type MetricDeclaration,
|
||||
type MetricParamMatcher,
|
||||
} from '../lib/api'
|
||||
import { CollapseToggle, PopOutButton, useSectionCollapsed } from './CollapsibleSection'
|
||||
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
|
||||
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
||||
@@ -150,14 +150,18 @@ export function Metrics({ range }: { range: TimeRange | null }) {
|
||||
const groups = groupByLabels(bucketValues)
|
||||
|
||||
return (
|
||||
<div className="border-b border-border p-3 text-sm">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
||||
Metrics
|
||||
<HelpIcon sectionId="metrics" label="Help: Metrics" />
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="metrics" range={range} />
|
||||
<div className="border-b border-border text-sm">
|
||||
<div className={sectionHeaderClassName}>
|
||||
<div className={sectionHeaderLeftClassName}>
|
||||
Metrics
|
||||
<HelpIcon sectionId="metrics" label="Help: Metrics" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="metrics" range={range} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4" hidden={collapsed}>
|
||||
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
||||
import { CollapseToggle, PopOutButton, useSectionCollapsed } from './CollapsibleSection'
|
||||
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
|
||||
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
||||
@@ -33,14 +33,18 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-border p-3 text-sm">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
||||
Params
|
||||
<HelpIcon sectionId="parameters" label="Help: Params" />
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="params" />
|
||||
<div className="border-b border-border text-sm">
|
||||
<div className={sectionHeaderClassName}>
|
||||
<div className={sectionHeaderLeftClassName}>
|
||||
Params
|
||||
<HelpIcon sectionId="parameters" label="Help: Params" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
<PopOutButton section="params" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4" hidden={collapsed}>
|
||||
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||
|
||||
Reference in New Issue
Block a user