feat(dashboard): add live observability overview
This commit is contained in:
parent
2ff9f3c0ed
commit
2d2b905d18
27 changed files with 1417 additions and 144 deletions
|
|
@ -50,9 +50,13 @@
|
|||
<style>
|
||||
.dashboard-frame {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
gap: var(--ui-space-3);
|
||||
padding: clamp(0.75rem, 1.3vw, 1.25rem);
|
||||
gap: 0.36rem;
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
padding: clamp(0.42rem, 0.65vw, 0.62rem);
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%),
|
||||
rgba(0, 0, 0, 0.18);
|
||||
|
|
@ -60,11 +64,12 @@
|
|||
|
||||
.dashboard-frame__header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-4);
|
||||
grid-template-columns: minmax(30rem, 1fr) minmax(30rem, 0.9fr);
|
||||
gap: 0.5rem;
|
||||
align-items: start;
|
||||
border-bottom: var(--ui-border);
|
||||
padding-bottom: var(--ui-space-3);
|
||||
min-height: 5.35rem;
|
||||
padding-bottom: 0.34rem;
|
||||
}
|
||||
|
||||
.dashboard-frame__header p,
|
||||
|
|
@ -85,32 +90,65 @@
|
|||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(2.45rem, 5vw, 4.2rem);
|
||||
font-size: clamp(2.35rem, 3.65vw, 3.25rem);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.85;
|
||||
line-height: 0.78;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-frame__modules {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(15rem, 0.66fr) minmax(17rem, 1fr);
|
||||
justify-content: end;
|
||||
gap: var(--ui-space-3);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
||||
gap: var(--ui-space-3);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-content: start;
|
||||
gap: 0.36rem;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels :global(.service-panel[data-model-id="runtime-health"]) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels
|
||||
:global(.service-panel[data-model-id="runtime-health"] .panel__body) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.dashboard-frame {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dashboard-frame__header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-frame__modules {
|
||||
justify-content: stretch;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels
|
||||
:global(.service-panel[data-model-id="runtime-health"] .panel__body) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
.footer-cell {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
min-height: 2.65rem;
|
||||
min-height: 1.9rem;
|
||||
align-items: center;
|
||||
background: rgba(2, 3, 2, 0.92);
|
||||
color: inherit;
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
span,
|
||||
strong {
|
||||
min-width: 0;
|
||||
padding: var(--ui-space-2) var(--ui-space-3);
|
||||
padding: 0.34rem 0.52rem;
|
||||
overflow-wrap: anywhere;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
|
@ -53,13 +53,13 @@
|
|||
height: 100%;
|
||||
border-right: var(--ui-border);
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.68rem;
|
||||
font-size: 0.52rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--ui-color-text);
|
||||
font-size: 0.76rem;
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.footer-cell[data-severity="ok"] strong {
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@
|
|||
<style>
|
||||
.icon-glyph {
|
||||
display: inline-grid;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
width: 1.55rem;
|
||||
height: 1.55rem;
|
||||
place-items: center;
|
||||
border: var(--ui-border);
|
||||
background: #050605;
|
||||
|
|
@ -37,17 +37,17 @@
|
|||
}
|
||||
|
||||
.icon-glyph :global(svg) {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.icon-glyph[data-size="sm"] {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.icon-glyph[data-size="lg"] {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
147
src/lib/ui/components/LineChart.svelte
Normal file
147
src/lib/ui/components/LineChart.svelte
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
import type { UiSeverity } from "../types";
|
||||
|
||||
let {
|
||||
values = [],
|
||||
severity = "neutral",
|
||||
label = "Telemetry trend",
|
||||
}: {
|
||||
values?: number[];
|
||||
severity?: UiSeverity;
|
||||
label?: string;
|
||||
} = $props();
|
||||
|
||||
let chartElement: HTMLDivElement;
|
||||
let chart: ChartInstance | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
const module = await import("uplot");
|
||||
chart = new module.default(chartOptions(chartElement, severity), chartData(values), chartElement);
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
chart?.setSize(chartSize(chartElement));
|
||||
});
|
||||
resizeObserver.observe(chartElement);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
chart?.setData(chartData(values));
|
||||
chart?.setSize(chartSize(chartElement));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
resizeObserver?.disconnect();
|
||||
chart?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="line-chart"
|
||||
data-chart-library="uplot"
|
||||
data-severity={severity}
|
||||
role="img"
|
||||
aria-label={label}
|
||||
>
|
||||
<div class="line-chart__canvas" bind:this={chartElement} aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.line-chart {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: 1rem;
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.line-chart[data-severity="warning"] {
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.line-chart[data-severity="danger"] {
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.line-chart[data-severity="stale"],
|
||||
.line-chart[data-severity="unavailable"] {
|
||||
color: var(--ui-color-stale);
|
||||
}
|
||||
|
||||
.line-chart__canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.line-chart__canvas :global(.uplot) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background: transparent;
|
||||
font-family: var(--ui-font-mono);
|
||||
}
|
||||
|
||||
.line-chart__canvas :global(.u-over),
|
||||
.line-chart__canvas :global(.u-under) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" module>
|
||||
type ChartInstance = {
|
||||
destroy(): void;
|
||||
setData(data: import("uplot").AlignedData): void;
|
||||
setSize(size: { width: number; height: number }): void;
|
||||
};
|
||||
|
||||
function chartData(values: number[]): import("uplot").AlignedData {
|
||||
const normalized = values.length ? values : [0, 0];
|
||||
return [
|
||||
normalized.map((_, index) => index),
|
||||
normalized.map((value) => Math.max(0, Number(value) || 0)),
|
||||
];
|
||||
}
|
||||
|
||||
function chartOptions(
|
||||
element: HTMLElement,
|
||||
severity: import("../types").UiSeverity,
|
||||
): import("uplot").Options {
|
||||
return {
|
||||
...chartSize(element),
|
||||
cursor: { show: false },
|
||||
legend: { show: false },
|
||||
padding: [2, 0, 2, 0],
|
||||
scales: {
|
||||
x: { time: false },
|
||||
y: { auto: true },
|
||||
},
|
||||
axes: [
|
||||
{ show: false },
|
||||
{ show: false },
|
||||
],
|
||||
series: [
|
||||
{},
|
||||
{
|
||||
stroke: chartStroke(element, severity),
|
||||
width: 2,
|
||||
points: { show: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function chartSize(element?: HTMLElement): { width: number; height: number } {
|
||||
return {
|
||||
width: Math.max(80, Math.round(element?.clientWidth || 120)),
|
||||
height: Math.max(20, Math.round(element?.clientHeight || 24)),
|
||||
};
|
||||
}
|
||||
|
||||
function chartStroke(element: HTMLElement, severity: import("../types").UiSeverity): string {
|
||||
const styles = window.getComputedStyle(element.closest(".line-chart") || element);
|
||||
const currentColor = styles.color;
|
||||
if (currentColor) return currentColor;
|
||||
if (severity === "danger") return "#ff1744";
|
||||
if (severity === "warning") return "#ffb020";
|
||||
return "#d7ff00";
|
||||
}
|
||||
</script>
|
||||
|
|
@ -37,12 +37,13 @@
|
|||
.module-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-4);
|
||||
gap: 0.5rem;
|
||||
align-items: start;
|
||||
min-width: min(100%, 18rem);
|
||||
min-width: 0;
|
||||
min-height: 4.15rem;
|
||||
border: var(--ui-border);
|
||||
background: rgba(6, 8, 7, 0.82);
|
||||
padding: var(--ui-space-3);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
|
|
@ -52,23 +53,29 @@
|
|||
|
||||
h2 {
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-top: var(--ui-space-1);
|
||||
margin-top: 0.15rem;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: 2.25rem;
|
||||
line-height: 0.9;
|
||||
font-size: clamp(1.45rem, 2vw, 2rem);
|
||||
line-height: 0.82;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: var(--ui-space-1);
|
||||
margin-top: 0.2rem;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.52rem;
|
||||
line-height: 1.05;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.module-card[data-severity="ok"] :global(.icon-glyph) {
|
||||
|
|
|
|||
|
|
@ -56,13 +56,13 @@
|
|||
}
|
||||
|
||||
.panel__header {
|
||||
padding: var(--ui-space-3) var(--ui-space-3) 0;
|
||||
padding: 0.42rem 0.55rem 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(1.75rem, 3vw, 2.5rem);
|
||||
font-size: clamp(1.45rem, 2.05vw, 1.95rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.9;
|
||||
|
|
@ -71,8 +71,8 @@
|
|||
|
||||
.panel__body {
|
||||
display: grid;
|
||||
gap: var(--ui-space-2);
|
||||
padding: var(--ui-space-3);
|
||||
gap: 0.25rem;
|
||||
padding: 0.42rem 0.55rem 0.55rem;
|
||||
}
|
||||
|
||||
.panel[data-density="compact"] .panel__body {
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@
|
|||
.service-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-3);
|
||||
gap: 0.42rem;
|
||||
align-items: center;
|
||||
min-height: 3.75rem;
|
||||
min-height: 2.35rem;
|
||||
border: var(--ui-border);
|
||||
background: rgba(12, 13, 12, 0.72);
|
||||
color: inherit;
|
||||
padding: var(--ui-space-2);
|
||||
padding: 0.3rem 0.38rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
|
@ -79,19 +79,25 @@
|
|||
}
|
||||
|
||||
h3 {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 0.88rem;
|
||||
overflow: hidden;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.05;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.22rem;
|
||||
margin-top: 0.16rem;
|
||||
overflow: hidden;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.25;
|
||||
font-size: 0.5rem;
|
||||
line-height: 1.08;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
|
|
|
|||
|
|
@ -15,15 +15,16 @@
|
|||
<style>
|
||||
.status-badge {
|
||||
display: inline-grid;
|
||||
min-height: 1.6rem;
|
||||
min-height: 1.12rem;
|
||||
align-items: center;
|
||||
border: var(--ui-border);
|
||||
padding: 0 var(--ui-space-2);
|
||||
padding: 0 0.34rem;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.52rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge[data-severity="ok"] {
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@
|
|||
<style>
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
.status-strip[data-compact="true"] {
|
||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { formatMetricValue } from "../format";
|
||||
import type { UiTelemetryCard } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
import LineChart from "./LineChart.svelte";
|
||||
|
||||
let { card }: { card: UiTelemetryCard } = $props();
|
||||
|
||||
|
|
@ -23,9 +24,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if card.sparkline?.length}
|
||||
<svg class="telemetry-card__sparkline" viewBox="0 0 100 24" aria-hidden="true">
|
||||
<polyline points={sparklinePoints(card.sparkline)} />
|
||||
</svg>
|
||||
<LineChart values={card.sparkline} severity={card.severity} label={`${card.label} trend`} />
|
||||
{/if}
|
||||
{#if card.detail || card.description}
|
||||
<p>{card.detail || card.description}</p>
|
||||
|
|
@ -35,12 +34,12 @@
|
|||
<style>
|
||||
.telemetry-card {
|
||||
display: grid;
|
||||
min-height: 7.15rem;
|
||||
min-height: 5.15rem;
|
||||
border: var(--ui-border);
|
||||
background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
|
||||
padding: var(--ui-space-3);
|
||||
padding: 0.45rem 0.55rem 0.42rem;
|
||||
color: var(--ui-color-text);
|
||||
gap: var(--ui-space-2);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.telemetry-card[data-severity="warning"] {
|
||||
|
|
@ -69,7 +68,11 @@
|
|||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--ui-space-2);
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
header :global(.icon-glyph) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h3,
|
||||
|
|
@ -80,29 +83,34 @@
|
|||
h3 {
|
||||
overflow-wrap: anywhere;
|
||||
color: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.02;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(2rem, 4vw, 3rem);
|
||||
font-size: clamp(1.75rem, 2.75vw, 2.4rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.82;
|
||||
line-height: 0.76;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p {
|
||||
overflow: hidden;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.67rem;
|
||||
line-height: 1.2;
|
||||
font-size: 0.53rem;
|
||||
line-height: 1.05;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.telemetry-card__bar {
|
||||
height: 0.35rem;
|
||||
height: 0.25rem;
|
||||
border: var(--ui-border);
|
||||
background: #030403;
|
||||
}
|
||||
|
|
@ -114,19 +122,6 @@
|
|||
background: currentColor;
|
||||
}
|
||||
|
||||
.telemetry-card__sparkline {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.telemetry-card__sparkline polyline {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: square;
|
||||
stroke-linejoin: miter;
|
||||
stroke-width: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" module>
|
||||
|
|
@ -142,17 +137,4 @@
|
|||
const progress = Number(card.value.value);
|
||||
return Number.isFinite(progress) ? clampPercent(progress) : null;
|
||||
}
|
||||
|
||||
function sparklinePoints(values: number[]): string {
|
||||
const max = Math.max(...values, 1);
|
||||
const step = values.length > 1 ? 100 / (values.length - 1) : 100;
|
||||
|
||||
return values
|
||||
.map((value, index) => {
|
||||
const x = index * step;
|
||||
const y = 24 - (Math.max(0, value) / max) * 22;
|
||||
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,21 @@
|
|||
<style>
|
||||
.telemetry-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--ui-density-card-min), 1fr));
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.telemetry-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.telemetry-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -108,6 +108,22 @@ describe("dashboard UI components", () => {
|
|||
expect(withProgress.body).toContain("--metric-progress: 42%");
|
||||
});
|
||||
|
||||
test("renders telemetry trends through the uPlot chart surface", () => {
|
||||
const { body } = render(TelemetryCard, {
|
||||
props: {
|
||||
card: {
|
||||
id: "trend-card",
|
||||
label: "Trend Card",
|
||||
value: { kind: "percent", value: 64 },
|
||||
sparkline: [18, 24, 64],
|
||||
severity: "ok",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body).toContain('data-chart-library="uplot"');
|
||||
});
|
||||
|
||||
test("base button controls forward native attributes", () => {
|
||||
const button = render(Button, {
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export function formatMetricValue(metric: UiMetricValue): string {
|
|||
const unit = metric.unit ?? defaultUnit(metric.kind);
|
||||
|
||||
if (metric.kind === "bytes") return formatBytes(value, precision);
|
||||
if (metric.kind === "percent") return `${value.toFixed(precision)}%`;
|
||||
if (metric.kind === "temperature") return `${value.toFixed(precision)}°C`;
|
||||
return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export { default as FooterStatusCell } from "./components/FooterStatusCell.svelt
|
|||
export { default as GridFrame } from "./components/GridFrame.svelte";
|
||||
export { default as IconGlyph } from "./components/IconGlyph.svelte";
|
||||
export { default as IconButton } from "./components/IconButton.svelte";
|
||||
export { default as LineChart } from "./components/LineChart.svelte";
|
||||
export { default as ModuleCard } from "./components/ModuleCard.svelte";
|
||||
export { default as Panel } from "./components/Panel.svelte";
|
||||
export { default as ProgressMeter } from "./components/ProgressMeter.svelte";
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ describe("dashboard model renderer", () => {
|
|||
"footer-status:last-sync",
|
||||
"footer-status:uptime",
|
||||
"footer-status:load-avg",
|
||||
"footer-status:auto-refresh",
|
||||
]);
|
||||
expect(dashboard.statusStripId).toBe("footer-status");
|
||||
});
|
||||
|
|
|
|||
13
src/lib/ui/stories/LineChart.stories.svelte
Normal file
13
src/lib/ui/stories/LineChart.stories.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import LineChart from "../components/LineChart.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/LineChart",
|
||||
component: LineChart,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Neutral Trend" args={{ values: [12, 18, 15, 28, 24, 32] }} />
|
||||
<Story name="Warning Trend" args={{ values: [22, 34, 48, 62, 74, 70], severity: "warning" }} />
|
||||
<Story name="Danger Trend" args={{ values: [20, 40, 64, 82, 90, 94], severity: "danger" }} />
|
||||
Loading…
Add table
Add a link
Reference in a new issue