This commit is contained in:
2026-03-26 04:24:32 -03:00
parent 08b67f2bb7
commit 08c58a6a9d
43 changed files with 2627 additions and 252 deletions

View File

@@ -64,8 +64,9 @@ defineProps<{
.panel-body {
flex: 1;
overflow: auto;
overflow: hidden;
padding: var(--space-2);
min-height: 0;
}
.panel-overlay {

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
direction: 'horizontal' | 'vertical'
}>()
const emit = defineEmits<{
resize: [delta: number]
}>()
const dragging = ref(false)
let startPos = 0
function onPointerDown(e: PointerEvent) {
dragging.value = true
startPos = props.direction === 'horizontal' ? e.clientX : e.clientY
const el = e.target as HTMLElement
el.setPointerCapture(e.pointerId)
}
function onPointerMove(e: PointerEvent) {
if (!dragging.value) return
const currentPos = props.direction === 'horizontal' ? e.clientX : e.clientY
const delta = currentPos - startPos
startPos = currentPos
emit('resize', delta)
}
function onPointerUp() {
dragging.value = false
}
</script>
<template>
<div
class="resize-handle"
:class="[direction, { dragging }]"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
/>
</template>
<style scoped>
.resize-handle {
flex-shrink: 0;
background: transparent;
transition: background 0.15s;
touch-action: none;
z-index: 10;
}
.resize-handle:hover,
.resize-handle.dragging {
background: var(--text-dim);
}
.resize-handle.horizontal {
width: 4px;
cursor: col-resize;
margin: 0 -2px;
}
.resize-handle.vertical {
height: 4px;
cursor: row-resize;
margin: -2px 0;
}
</style>

View File

@@ -7,6 +7,7 @@ export { useDataSource } from './composables/useDataSource'
// Components
export { default as Panel } from './components/Panel.vue'
export { default as LayoutGrid } from './components/LayoutGrid.vue'
export { default as ResizeHandle } from './components/ResizeHandle.vue'
// Renderers
export { default as LogRenderer } from './renderers/LogRenderer.vue'

View File

@@ -76,6 +76,7 @@ const sorted = computed(() => {
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th {
@@ -89,7 +90,6 @@ th {
border-bottom: var(--panel-border);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
th:hover {
@@ -104,7 +104,10 @@ th:hover {
td {
padding: var(--space-1) var(--space-3);
border-bottom: 1px solid var(--surface-3);
white-space: nowrap;
white-space: normal;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
}
tr:hover td {

View File

@@ -22,6 +22,7 @@ const props = withDefaults(defineProps<{
})
const container = ref<HTMLElement | null>(null)
const zoomed = ref(false)
let chart: uPlot | null = null
function buildOpts(): uPlot.Options {
@@ -40,25 +41,66 @@ function buildOpts(): uPlot.Options {
height: container.value?.clientHeight ?? 200,
series: seriesOpts,
axes: [
{ stroke: '#555568', grid: { stroke: '#2e2e3822' } },
{ stroke: '#555568', grid: { stroke: '#2e2e3822' } },
{
stroke: '#555568',
grid: { stroke: '#2e2e3822' },
size: 40,
font: '10px monospace',
ticks: { size: 3 },
},
{
stroke: '#555568',
grid: { stroke: '#2e2e3822' },
size: 35,
font: '10px monospace',
ticks: { size: 3 },
},
],
cursor: { show: true },
legend: { show: true },
legend: { show: true, live: false },
padding: [8, 8, 0, 0],
hooks: {
setScale: [(_self: uPlot, scaleKey: string) => {
if (scaleKey === 'x') zoomed.value = true
}],
},
}
}
function resetZoom() {
if (!chart) return
const data = chart.data
if (data && data[0] && data[0].length > 0) {
const min = data[0][0]
const max = data[0][data[0].length - 1]
chart.setScale('x', { min, max })
}
zoomed.value = false
}
function getLegendHeight(): number {
if (!container.value) return 0
const legend = container.value.querySelector('.u-legend') as HTMLElement | null
return legend ? legend.offsetHeight : 0
}
function createChart() {
if (!container.value) return
if (chart) chart.destroy()
chart = new uPlot(buildOpts(), props.data, container.value)
// Refit after legend renders
nextTick(() => resize())
}
function resize() {
if (!chart || !container.value) return
const legendH = getLegendHeight()
const availableH = container.value.clientHeight
// uPlot height = canvas height (chart sets total = canvas + legend)
const chartH = Math.max(60, availableH - legendH)
chart.setSize({
width: container.value.clientWidth,
height: container.value.clientHeight,
height: chartH,
})
}
@@ -83,19 +125,74 @@ onMounted(() => {
</script>
<template>
<div ref="container" class="timeseries-renderer" />
<div class="timeseries-wrapper">
<button v-if="zoomed" class="reset-zoom" @click="resetZoom" title="Reset zoom"></button>
<div ref="container" class="timeseries-renderer" />
</div>
</template>
<style scoped>
.timeseries-wrapper {
width: 100%;
height: 100%;
position: relative;
}
.reset-zoom {
position: absolute;
top: 4px;
right: 4px;
z-index: 20;
background: var(--surface-2);
border: 1px solid var(--surface-3);
border-radius: 4px;
color: var(--text-secondary);
font-size: 14px;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.15s;
}
.reset-zoom:hover {
opacity: 1;
color: var(--text-primary);
}
.timeseries-renderer {
width: 100%;
height: 100%;
min-height: 150px;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* uPlot creates a .u-wrap for canvas + a .u-legend below it */
.timeseries-renderer :deep(.u-wrap) {
flex: 1;
min-height: 0;
}
.timeseries-renderer :deep(.u-legend) {
flex-shrink: 0;
}
.timeseries-renderer :deep(.u-legend) {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
font-size: 10px;
color: var(--text-secondary);
padding: 2px 0;
display: flex;
flex-wrap: wrap;
gap: 0 8px;
}
.timeseries-renderer :deep(.u-legend .u-series) {
display: inline-flex;
padding: 0;
}
</style>