feat: complete Loki query, insights, settings and deployment workflows
Add the Loki query UI, access policies, ingress insights, alert and registry settings, and their backend APIs. Include the related Kubernetes deployment and collector updates required by the integrated application. Validated with go test ./..., npm run build, and npm run lint.
This commit is contained in:
1 parent
f9c6c1d826
commit
2d73661dd1
127 files changed
+8117
-2876
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { CalendarRange, ChevronDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { getUser } from '@/lib/auth-storage'
|
||||
|
||||
const presets = [
|
||||
{ label: '最近 5 分钟', ms: 300000 },
|
||||
{ label: '最近 15 分钟', ms: 900000 },
|
||||
{ label: '最近 1 小时', ms: 3600000 },
|
||||
{ label: '最近 6 小时', ms: 21600000 },
|
||||
{ label: '最近 24 小时', ms: 86400000 },
|
||||
]
|
||||
const localTime = (date: Date) => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 19)
|
||||
const historyKey = () => `opsxloki.logs.range-history.${getUser()?.id ?? 'anonymous'}`
|
||||
type SavedRange = { start: string; end: string }
|
||||
|
||||
function readHistory(): SavedRange[] {
|
||||
try {
|
||||
const value: unknown = JSON.parse(localStorage.getItem(historyKey()) ?? '[]')
|
||||
return Array.isArray(value) ? value.filter((item): item is SavedRange => typeof item?.start === 'string' && typeof item?.end === 'string').slice(0, 5) : []
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
export function TimeRangePicker({ start, end, onChange }: { start: string; end: string; onChange: (start: string, end: string) => void }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [draftStart, setDraftStart] = useState(start)
|
||||
const [draftEnd, setDraftEnd] = useState(end)
|
||||
const [history, setHistory] = useState(readHistory)
|
||||
const [selection, setSelection] = useState({ start, end, ms: 300000 as number | null })
|
||||
const root = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const close = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) setOpen(false) }
|
||||
document.addEventListener('pointerdown', close)
|
||||
return () => document.removeEventListener('pointerdown', close)
|
||||
}, [])
|
||||
|
||||
function choose(nextStart: string, nextEnd: string, nextPreset: number | null = null) {
|
||||
if (!(new Date(nextStart).getTime() < new Date(nextEnd).getTime())) return
|
||||
setSelection({ start: nextStart, end: nextEnd, ms: nextPreset })
|
||||
onChange(nextStart, nextEnd)
|
||||
setDraftStart(nextStart); setDraftEnd(nextEnd); setOpen(false)
|
||||
const next = [{ start: nextStart, end: nextEnd }, ...history.filter(item => item.start !== nextStart || item.end !== nextEnd)].slice(0, 5)
|
||||
setHistory(next)
|
||||
try { localStorage.setItem(historyKey(), JSON.stringify(next)) } catch { /* Storage may be unavailable. */ }
|
||||
}
|
||||
|
||||
const activePreset = selection.start === start && selection.end === end ? presets.find(item => item.ms === selection.ms) : undefined
|
||||
const label = activePreset?.label ?? '自定义时间'
|
||||
const rangeText = `${new Date(start).toLocaleString()} ~ ${new Date(end).toLocaleString()}`
|
||||
|
||||
return <div ref={root} className="relative min-w-0">
|
||||
<Button type="button" variant="outline" aria-expanded={open} className="h-9 max-w-full gap-2" onClick={() => { setDraftStart(start); setDraftEnd(end); setOpen(!open) }}>
|
||||
<CalendarRange className="size-4 shrink-0" /><span className="shrink-0 font-medium">{label}</span><span className="hidden truncate font-normal text-muted-foreground xl:inline">{rangeText}</span><ChevronDown className="size-4 shrink-0" />
|
||||
</Button>
|
||||
{open && <div className="absolute left-0 top-full z-40 mt-1 w-[min(92vw,520px)] rounded-lg border bg-popover p-4 shadow-xl">
|
||||
<h2 className="text-sm font-medium">选择时间范围</h2>
|
||||
<div className="mt-3 grid grid-cols-2 gap-1 sm:grid-cols-3">{presets.map(item => <Button key={item.ms} type="button" size="sm" variant={activePreset?.ms === item.ms ? 'secondary' : 'ghost'} className="justify-start" onClick={() => { const now = Date.now(); choose(localTime(new Date(now - item.ms)), localTime(new Date(now)), item.ms) }}>{item.label}</Button>)}</div>
|
||||
<div className="mt-3 grid gap-2 border-t pt-3 sm:grid-cols-2">
|
||||
<label className="text-xs text-muted-foreground">开始时间<Input className="mt-1 h-8" type="datetime-local" step={1} value={draftStart} onChange={event => setDraftStart(event.target.value)} /></label>
|
||||
<label className="text-xs text-muted-foreground">结束时间<Input className="mt-1 h-8" type="datetime-local" step={1} value={draftEnd} onChange={event => setDraftEnd(event.target.value)} /></label>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end"><Button type="button" size="sm" disabled={!(new Date(draftStart).getTime() < new Date(draftEnd).getTime())} onClick={() => choose(draftStart, draftEnd)}>应用时间</Button></div>
|
||||
{history.length > 0 && <div className="mt-3 border-t pt-3"><h3 className="mb-1 text-xs font-medium text-muted-foreground">最近使用</h3><div className="max-h-32 space-y-0.5 overflow-y-auto">{history.map(item => <button type="button" key={`${item.start}-${item.end}`} className="block w-full truncate rounded px-2 py-1.5 text-left text-xs hover:bg-muted" onClick={() => choose(item.start, item.end)}>{new Date(item.start).toLocaleString()} ~ {new Date(item.end).toLocaleString()}</button>)}</div></div>}
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
Reference in new issue
Block a user