@flanksource/clicky-ui 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/StatusBreakdown.cjs +80 -0
- package/dist/data/StatusBreakdown.cjs.map +1 -0
- package/dist/data/StatusBreakdown.d.ts +36 -0
- package/dist/data/StatusBreakdown.d.ts.map +1 -0
- package/dist/data/StatusBreakdown.js +80 -0
- package/dist/data/StatusBreakdown.js.map +1 -0
- package/dist/data/TimeseriesCoreBars.cjs +125 -0
- package/dist/data/TimeseriesCoreBars.cjs.map +1 -0
- package/dist/data/TimeseriesCoreBars.d.ts +61 -0
- package/dist/data/TimeseriesCoreBars.d.ts.map +1 -0
- package/dist/data/TimeseriesCoreBars.js +125 -0
- package/dist/data/TimeseriesCoreBars.js.map +1 -0
- package/dist/data/TimeseriesGauge.cjs +144 -0
- package/dist/data/TimeseriesGauge.cjs.map +1 -0
- package/dist/data/TimeseriesGauge.d.ts +56 -0
- package/dist/data/TimeseriesGauge.d.ts.map +1 -0
- package/dist/data/TimeseriesGauge.js +144 -0
- package/dist/data/TimeseriesGauge.js.map +1 -0
- package/dist/data.cjs +9 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +3 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +9 -0
- package/dist/data.js.map +1 -1
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Section.cjs +23 -14
- package/dist/layout/Section.cjs.map +1 -1
- package/dist/layout/Section.d.ts +14 -2
- package/dist/layout/Section.d.ts.map +1 -1
- package/dist/layout/Section.js +24 -15
- package/dist/layout/Section.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
require("react");
|
|
5
|
+
const utils = require("../lib/utils.cjs");
|
|
6
|
+
const button = require("../components/button.cjs");
|
|
7
|
+
function segment(key, label, count, className) {
|
|
8
|
+
return { key, label, count: Math.max(0, count || 0), className };
|
|
9
|
+
}
|
|
10
|
+
const defaultRenderLink = ({ to, className, title, children }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: to, className, title, children });
|
|
11
|
+
function StackedStatusBar({
|
|
12
|
+
segments,
|
|
13
|
+
ariaLabel
|
|
14
|
+
}) {
|
|
15
|
+
const visible = segments.filter((s) => s.count > 0);
|
|
16
|
+
const total = visible.reduce((sum, s) => sum + s.count, 0);
|
|
17
|
+
if (visible.length === 0 || total <= 0) {
|
|
18
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded border border-dashed p-3 text-xs text-muted-foreground", children: "No status data" });
|
|
19
|
+
}
|
|
20
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-2 overflow-hidden rounded bg-muted", "aria-label": ariaLabel, children: visible.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
21
|
+
"div",
|
|
22
|
+
{
|
|
23
|
+
className: s.className,
|
|
24
|
+
title: `${s.label}: ${s.count}`,
|
|
25
|
+
style: { width: `${Math.max(1, s.count / total * 100)}%` }
|
|
26
|
+
},
|
|
27
|
+
s.key
|
|
28
|
+
)) });
|
|
29
|
+
}
|
|
30
|
+
function StatusRows({
|
|
31
|
+
segments,
|
|
32
|
+
ariaLabel,
|
|
33
|
+
onRetry,
|
|
34
|
+
retryingKey,
|
|
35
|
+
isRetryable,
|
|
36
|
+
renderLink = defaultRenderLink
|
|
37
|
+
}) {
|
|
38
|
+
const visible = segments.filter((s) => s.count > 0);
|
|
39
|
+
const total = visible.reduce((sum, s) => sum + s.count, 0);
|
|
40
|
+
if (visible.length === 0 || total <= 0) {
|
|
41
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded border border-dashed p-3 text-xs text-muted-foreground", children: "No status data" });
|
|
42
|
+
}
|
|
43
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", "aria-label": ariaLabel, children: visible.map((s) => {
|
|
44
|
+
const retryable = !!onRetry && ((isRetryable == null ? void 0 : isRetryable(s)) ?? false);
|
|
45
|
+
const meta = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
|
|
46
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: utils.cn("h-2.5 w-2.5 shrink-0 rounded-sm", s.className), "aria-hidden": "true" }),
|
|
47
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs", children: s.label }),
|
|
48
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto shrink-0 font-mono text-xs text-muted-foreground", children: s.count })
|
|
49
|
+
] });
|
|
50
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
51
|
+
s.href ? renderLink({
|
|
52
|
+
to: s.href,
|
|
53
|
+
className: "flex min-w-0 flex-1 rounded hover:bg-accent/40",
|
|
54
|
+
title: `View ${s.label} records`,
|
|
55
|
+
children: meta
|
|
56
|
+
}) : meta,
|
|
57
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-32 shrink-0 overflow-hidden rounded bg-muted sm:w-48", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: utils.cn("h-full", s.className), style: { width: `${Math.max(1, s.count / total * 100)}%` } }) }),
|
|
58
|
+
retryable ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
59
|
+
button.Button,
|
|
60
|
+
{
|
|
61
|
+
type: "button",
|
|
62
|
+
variant: "outline",
|
|
63
|
+
size: "sm",
|
|
64
|
+
className: "h-6 shrink-0 px-2 text-[11px]",
|
|
65
|
+
disabled: retryingKey === s.key,
|
|
66
|
+
onClick: (e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
onRetry == null ? void 0 : onRetry(s);
|
|
70
|
+
},
|
|
71
|
+
children: retryingKey === s.key ? "Retrying…" : "Retry"
|
|
72
|
+
}
|
|
73
|
+
) : null
|
|
74
|
+
] }, s.key);
|
|
75
|
+
}) });
|
|
76
|
+
}
|
|
77
|
+
exports.StackedStatusBar = StackedStatusBar;
|
|
78
|
+
exports.StatusRows = StatusRows;
|
|
79
|
+
exports.segment = segment;
|
|
80
|
+
//# sourceMappingURL=StatusBreakdown.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusBreakdown.cjs","sources":["../../src/data/StatusBreakdown.tsx"],"sourcesContent":["import { type ReactElement, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../components/button\";\n\n// A status-breakdown family: one StatusSegment list rendered either as a single\n// horizontal StackedStatusBar (scan a mix at a glance) or as vertical StatusRows\n// (each status on its own line with a proportional bar, optional drill-down link\n// and retry). Shared by monitor/intake/cycle breakdowns and any panel that shows\n// a count mix.\n\nexport interface StatusSegment {\n key: string;\n label: string;\n count: number;\n /** Tailwind background class for the swatch/bar fill, e.g. \"bg-green-500\". */\n className: string;\n /**\n * When set, the row links here (the breakdown points each status at its\n * filtered records/members view). Rendered via the `renderLink` prop so the\n * component stays router-agnostic.\n */\n href?: string;\n}\n\n/** Builds a StatusSegment, clamping the count to a non-negative integer. */\nexport function segment(key: string, label: string, count: number, className: string): StatusSegment {\n return { key, label, count: Math.max(0, count || 0), className };\n}\n\n/** Render-prop for a status row's link, so callers supply their router's Link (or a plain <a>). */\nexport type StatusRenderLink = (args: {\n to: string;\n className?: string;\n title?: string;\n children: ReactNode;\n}) => ReactElement;\n\nconst defaultRenderLink: StatusRenderLink = ({ to, className, title, children }) => (\n <a href={to} className={className} title={title}>\n {children}\n </a>\n);\n\n// StackedStatusBar renders the segments as a single horizontal multi-color bar —\n// segments share one track sized proportionally to the visible-segment total.\nexport function StackedStatusBar({\n segments,\n ariaLabel,\n}: {\n segments: StatusSegment[];\n ariaLabel?: string;\n}) {\n const visible = segments.filter((s) => s.count > 0);\n const total = visible.reduce((sum, s) => sum + s.count, 0);\n if (visible.length === 0 || total <= 0) {\n return <div className=\"rounded border border-dashed p-3 text-xs text-muted-foreground\">No status data</div>;\n }\n return (\n <div className=\"flex h-2 overflow-hidden rounded bg-muted\" aria-label={ariaLabel}>\n {visible.map((s) => (\n <div\n key={s.key}\n className={s.className}\n title={`${s.label}: ${s.count}`}\n style={{ width: `${Math.max(1, (s.count / total) * 100)}%` }}\n />\n ))}\n </div>\n );\n}\n\n// StatusRows renders each status as its own row — a colored swatch + label, the\n// count, and a horizontal bar sized proportionally to the visible-segment total.\n// The vertical counterpart to StackedStatusBar. An optional onRetry surfaces a\n// Retry button per row; callers that omit it get no button. When a segment\n// carries an href the whole row links there via renderLink (defaults to a plain\n// <a>); the Retry button stops propagation so it stays independently clickable.\nexport function StatusRows({\n segments,\n ariaLabel,\n onRetry,\n retryingKey,\n isRetryable,\n renderLink = defaultRenderLink,\n}: {\n segments: StatusSegment[];\n ariaLabel?: string;\n onRetry?: (segment: StatusSegment) => void;\n retryingKey?: string;\n isRetryable?: (segment: StatusSegment) => boolean;\n renderLink?: StatusRenderLink;\n}) {\n const visible = segments.filter((s) => s.count > 0);\n const total = visible.reduce((sum, s) => sum + s.count, 0);\n if (visible.length === 0 || total <= 0) {\n return <div className=\"rounded border border-dashed p-3 text-xs text-muted-foreground\">No status data</div>;\n }\n return (\n <div className=\"space-y-2\" aria-label={ariaLabel}>\n {visible.map((s) => {\n const retryable = !!onRetry && (isRetryable?.(s) ?? false);\n const meta = (\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <span className={cn(\"h-2.5 w-2.5 shrink-0 rounded-sm\", s.className)} aria-hidden=\"true\" />\n <span className=\"truncate text-xs\">{s.label}</span>\n <span className=\"ml-auto shrink-0 font-mono text-xs text-muted-foreground\">{s.count}</span>\n </div>\n );\n return (\n <div key={s.key} className=\"flex items-center gap-3\">\n {s.href\n ? renderLink({\n to: s.href,\n className: \"flex min-w-0 flex-1 rounded hover:bg-accent/40\",\n title: `View ${s.label} records`,\n children: meta,\n })\n : meta}\n <div className=\"h-2 w-32 shrink-0 overflow-hidden rounded bg-muted sm:w-48\">\n <div className={cn(\"h-full\", s.className)} style={{ width: `${Math.max(1, (s.count / total) * 100)}%` }} />\n </div>\n {retryable ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-6 shrink-0 px-2 text-[11px]\"\n disabled={retryingKey === s.key}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n onRetry?.(s);\n }}\n >\n {retryingKey === s.key ? \"Retrying…\" : \"Retry\"}\n </Button>\n ) : null}\n </div>\n );\n })}\n </div>\n );\n}\n"],"names":["jsx","jsxs","cn","Button"],"mappings":";;;;;;AAyBO,SAAS,QAAQ,KAAa,OAAe,OAAe,WAAkC;AACnG,SAAO,EAAE,KAAK,OAAO,OAAO,KAAK,IAAI,GAAG,SAAS,CAAC,GAAG,UAAA;AACvD;AAUA,MAAM,oBAAsC,CAAC,EAAE,IAAI,WAAW,OAAO,SAAA,MACnEA,2BAAAA,IAAC,KAAA,EAAE,MAAM,IAAI,WAAsB,OAChC,SAAA,CACH;AAKK,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AACF,GAGG;AACD,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC;AAClD,QAAM,QAAQ,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AACzD,MAAI,QAAQ,WAAW,KAAK,SAAS,GAAG;AACtC,WAAOA,2BAAAA,IAAC,OAAA,EAAI,WAAU,kEAAiE,UAAA,kBAAc;AAAA,EACvG;AACA,SACEA,+BAAC,SAAI,WAAU,6CAA4C,cAAY,WACpE,UAAA,QAAQ,IAAI,CAAC,MACZA,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MAEC,WAAW,EAAE;AAAA,MACb,OAAO,GAAG,EAAE,KAAK,KAAK,EAAE,KAAK;AAAA,MAC7B,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,GAAI,EAAE,QAAQ,QAAS,GAAG,CAAC,IAAA;AAAA,IAAI;AAAA,IAHtD,EAAE;AAAA,EAAA,CAKV,GACH;AAEJ;AAQO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AACf,GAOG;AACD,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC;AAClD,QAAM,QAAQ,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AACzD,MAAI,QAAQ,WAAW,KAAK,SAAS,GAAG;AACtC,WAAOA,2BAAAA,IAAC,OAAA,EAAI,WAAU,kEAAiE,UAAA,kBAAc;AAAA,EACvG;AACA,SACEA,+BAAC,SAAI,WAAU,aAAY,cAAY,WACpC,UAAA,QAAQ,IAAI,CAAC,MAAM;AAClB,UAAM,YAAY,CAAC,CAAC,aAAY,2CAAc,OAAM;AACpD,UAAM,OACJC,2BAAAA,KAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,MAAAD,2BAAAA,IAAC,QAAA,EAAK,WAAWE,SAAG,mCAAmC,EAAE,SAAS,GAAG,eAAY,QAAO;AAAA,MACxFF,2BAAAA,IAAC,QAAA,EAAK,WAAU,oBAAoB,YAAE,OAAM;AAAA,MAC5CA,2BAAAA,IAAC,QAAA,EAAK,WAAU,4DAA4D,YAAE,MAAA,CAAM;AAAA,IAAA,GACtF;AAEF,WACEC,2BAAAA,KAAC,OAAA,EAAgB,WAAU,2BACxB,UAAA;AAAA,MAAA,EAAE,OACC,WAAW;AAAA,QACT,IAAI,EAAE;AAAA,QACN,WAAW;AAAA,QACX,OAAO,QAAQ,EAAE,KAAK;AAAA,QACtB,UAAU;AAAA,MAAA,CACX,IACD;AAAA,MACJD,2BAAAA,IAAC,OAAA,EAAI,WAAU,8DACb,UAAAA,2BAAAA,IAAC,OAAA,EAAI,WAAWE,MAAAA,GAAG,UAAU,EAAE,SAAS,GAAG,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,GAAI,EAAE,QAAQ,QAAS,GAAG,CAAC,IAAA,EAAI,CAAG,EAAA,CAC3G;AAAA,MACC,YACCF,2BAAAA;AAAAA,QAACG,OAAAA;AAAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,UAAU,gBAAgB,EAAE;AAAA,UAC5B,SAAS,CAAC,MAAM;AACd,cAAE,eAAA;AACF,cAAE,gBAAA;AACF,+CAAU;AAAA,UACZ;AAAA,UAEC,UAAA,gBAAgB,EAAE,MAAM,cAAc;AAAA,QAAA;AAAA,MAAA,IAEvC;AAAA,IAAA,EAAA,GA3BI,EAAE,GA4BZ;AAAA,EAEJ,CAAC,EAAA,CACH;AAEJ;;;;"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ReactElement, ReactNode } from 'react';
|
|
2
|
+
export interface StatusSegment {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
count: number;
|
|
6
|
+
/** Tailwind background class for the swatch/bar fill, e.g. "bg-green-500". */
|
|
7
|
+
className: string;
|
|
8
|
+
/**
|
|
9
|
+
* When set, the row links here (the breakdown points each status at its
|
|
10
|
+
* filtered records/members view). Rendered via the `renderLink` prop so the
|
|
11
|
+
* component stays router-agnostic.
|
|
12
|
+
*/
|
|
13
|
+
href?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Builds a StatusSegment, clamping the count to a non-negative integer. */
|
|
16
|
+
export declare function segment(key: string, label: string, count: number, className: string): StatusSegment;
|
|
17
|
+
/** Render-prop for a status row's link, so callers supply their router's Link (or a plain <a>). */
|
|
18
|
+
export type StatusRenderLink = (args: {
|
|
19
|
+
to: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}) => ReactElement;
|
|
24
|
+
export declare function StackedStatusBar({ segments, ariaLabel, }: {
|
|
25
|
+
segments: StatusSegment[];
|
|
26
|
+
ariaLabel?: string;
|
|
27
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function StatusRows({ segments, ariaLabel, onRetry, retryingKey, isRetryable, renderLink, }: {
|
|
29
|
+
segments: StatusSegment[];
|
|
30
|
+
ariaLabel?: string;
|
|
31
|
+
onRetry?: (segment: StatusSegment) => void;
|
|
32
|
+
retryingKey?: string;
|
|
33
|
+
isRetryable?: (segment: StatusSegment) => boolean;
|
|
34
|
+
renderLink?: StatusRenderLink;
|
|
35
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
//# sourceMappingURL=StatusBreakdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusBreakdown.d.ts","sourceRoot":"","sources":["../../src/data/StatusBreakdown.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAU1D,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4EAA4E;AAC5E,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,CAEnG;AAED,mGAAmG;AACnG,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB,KAAK,YAAY,CAAC;AAUnB,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,SAAS,GACV,EAAE;IACD,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAkBA;AAQD,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,OAAO,EACP,WAAW,EACX,WAAW,EACX,UAA8B,GAC/B,EAAE;IACD,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC;IAClD,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,2CAmDA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "react";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { Button } from "../components/button.js";
|
|
5
|
+
function segment(key, label, count, className) {
|
|
6
|
+
return { key, label, count: Math.max(0, count || 0), className };
|
|
7
|
+
}
|
|
8
|
+
const defaultRenderLink = ({ to, className, title, children }) => /* @__PURE__ */ jsx("a", { href: to, className, title, children });
|
|
9
|
+
function StackedStatusBar({
|
|
10
|
+
segments,
|
|
11
|
+
ariaLabel
|
|
12
|
+
}) {
|
|
13
|
+
const visible = segments.filter((s) => s.count > 0);
|
|
14
|
+
const total = visible.reduce((sum, s) => sum + s.count, 0);
|
|
15
|
+
if (visible.length === 0 || total <= 0) {
|
|
16
|
+
return /* @__PURE__ */ jsx("div", { className: "rounded border border-dashed p-3 text-xs text-muted-foreground", children: "No status data" });
|
|
17
|
+
}
|
|
18
|
+
return /* @__PURE__ */ jsx("div", { className: "flex h-2 overflow-hidden rounded bg-muted", "aria-label": ariaLabel, children: visible.map((s) => /* @__PURE__ */ jsx(
|
|
19
|
+
"div",
|
|
20
|
+
{
|
|
21
|
+
className: s.className,
|
|
22
|
+
title: `${s.label}: ${s.count}`,
|
|
23
|
+
style: { width: `${Math.max(1, s.count / total * 100)}%` }
|
|
24
|
+
},
|
|
25
|
+
s.key
|
|
26
|
+
)) });
|
|
27
|
+
}
|
|
28
|
+
function StatusRows({
|
|
29
|
+
segments,
|
|
30
|
+
ariaLabel,
|
|
31
|
+
onRetry,
|
|
32
|
+
retryingKey,
|
|
33
|
+
isRetryable,
|
|
34
|
+
renderLink = defaultRenderLink
|
|
35
|
+
}) {
|
|
36
|
+
const visible = segments.filter((s) => s.count > 0);
|
|
37
|
+
const total = visible.reduce((sum, s) => sum + s.count, 0);
|
|
38
|
+
if (visible.length === 0 || total <= 0) {
|
|
39
|
+
return /* @__PURE__ */ jsx("div", { className: "rounded border border-dashed p-3 text-xs text-muted-foreground", children: "No status data" });
|
|
40
|
+
}
|
|
41
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-2", "aria-label": ariaLabel, children: visible.map((s) => {
|
|
42
|
+
const retryable = !!onRetry && ((isRetryable == null ? void 0 : isRetryable(s)) ?? false);
|
|
43
|
+
const meta = /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("span", { className: cn("h-2.5 w-2.5 shrink-0 rounded-sm", s.className), "aria-hidden": "true" }),
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-xs", children: s.label }),
|
|
46
|
+
/* @__PURE__ */ jsx("span", { className: "ml-auto shrink-0 font-mono text-xs text-muted-foreground", children: s.count })
|
|
47
|
+
] });
|
|
48
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
49
|
+
s.href ? renderLink({
|
|
50
|
+
to: s.href,
|
|
51
|
+
className: "flex min-w-0 flex-1 rounded hover:bg-accent/40",
|
|
52
|
+
title: `View ${s.label} records`,
|
|
53
|
+
children: meta
|
|
54
|
+
}) : meta,
|
|
55
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 w-32 shrink-0 overflow-hidden rounded bg-muted sm:w-48", children: /* @__PURE__ */ jsx("div", { className: cn("h-full", s.className), style: { width: `${Math.max(1, s.count / total * 100)}%` } }) }),
|
|
56
|
+
retryable ? /* @__PURE__ */ jsx(
|
|
57
|
+
Button,
|
|
58
|
+
{
|
|
59
|
+
type: "button",
|
|
60
|
+
variant: "outline",
|
|
61
|
+
size: "sm",
|
|
62
|
+
className: "h-6 shrink-0 px-2 text-[11px]",
|
|
63
|
+
disabled: retryingKey === s.key,
|
|
64
|
+
onClick: (e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
onRetry == null ? void 0 : onRetry(s);
|
|
68
|
+
},
|
|
69
|
+
children: retryingKey === s.key ? "Retrying…" : "Retry"
|
|
70
|
+
}
|
|
71
|
+
) : null
|
|
72
|
+
] }, s.key);
|
|
73
|
+
}) });
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
StackedStatusBar,
|
|
77
|
+
StatusRows,
|
|
78
|
+
segment
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=StatusBreakdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusBreakdown.js","sources":["../../src/data/StatusBreakdown.tsx"],"sourcesContent":["import { type ReactElement, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../components/button\";\n\n// A status-breakdown family: one StatusSegment list rendered either as a single\n// horizontal StackedStatusBar (scan a mix at a glance) or as vertical StatusRows\n// (each status on its own line with a proportional bar, optional drill-down link\n// and retry). Shared by monitor/intake/cycle breakdowns and any panel that shows\n// a count mix.\n\nexport interface StatusSegment {\n key: string;\n label: string;\n count: number;\n /** Tailwind background class for the swatch/bar fill, e.g. \"bg-green-500\". */\n className: string;\n /**\n * When set, the row links here (the breakdown points each status at its\n * filtered records/members view). Rendered via the `renderLink` prop so the\n * component stays router-agnostic.\n */\n href?: string;\n}\n\n/** Builds a StatusSegment, clamping the count to a non-negative integer. */\nexport function segment(key: string, label: string, count: number, className: string): StatusSegment {\n return { key, label, count: Math.max(0, count || 0), className };\n}\n\n/** Render-prop for a status row's link, so callers supply their router's Link (or a plain <a>). */\nexport type StatusRenderLink = (args: {\n to: string;\n className?: string;\n title?: string;\n children: ReactNode;\n}) => ReactElement;\n\nconst defaultRenderLink: StatusRenderLink = ({ to, className, title, children }) => (\n <a href={to} className={className} title={title}>\n {children}\n </a>\n);\n\n// StackedStatusBar renders the segments as a single horizontal multi-color bar —\n// segments share one track sized proportionally to the visible-segment total.\nexport function StackedStatusBar({\n segments,\n ariaLabel,\n}: {\n segments: StatusSegment[];\n ariaLabel?: string;\n}) {\n const visible = segments.filter((s) => s.count > 0);\n const total = visible.reduce((sum, s) => sum + s.count, 0);\n if (visible.length === 0 || total <= 0) {\n return <div className=\"rounded border border-dashed p-3 text-xs text-muted-foreground\">No status data</div>;\n }\n return (\n <div className=\"flex h-2 overflow-hidden rounded bg-muted\" aria-label={ariaLabel}>\n {visible.map((s) => (\n <div\n key={s.key}\n className={s.className}\n title={`${s.label}: ${s.count}`}\n style={{ width: `${Math.max(1, (s.count / total) * 100)}%` }}\n />\n ))}\n </div>\n );\n}\n\n// StatusRows renders each status as its own row — a colored swatch + label, the\n// count, and a horizontal bar sized proportionally to the visible-segment total.\n// The vertical counterpart to StackedStatusBar. An optional onRetry surfaces a\n// Retry button per row; callers that omit it get no button. When a segment\n// carries an href the whole row links there via renderLink (defaults to a plain\n// <a>); the Retry button stops propagation so it stays independently clickable.\nexport function StatusRows({\n segments,\n ariaLabel,\n onRetry,\n retryingKey,\n isRetryable,\n renderLink = defaultRenderLink,\n}: {\n segments: StatusSegment[];\n ariaLabel?: string;\n onRetry?: (segment: StatusSegment) => void;\n retryingKey?: string;\n isRetryable?: (segment: StatusSegment) => boolean;\n renderLink?: StatusRenderLink;\n}) {\n const visible = segments.filter((s) => s.count > 0);\n const total = visible.reduce((sum, s) => sum + s.count, 0);\n if (visible.length === 0 || total <= 0) {\n return <div className=\"rounded border border-dashed p-3 text-xs text-muted-foreground\">No status data</div>;\n }\n return (\n <div className=\"space-y-2\" aria-label={ariaLabel}>\n {visible.map((s) => {\n const retryable = !!onRetry && (isRetryable?.(s) ?? false);\n const meta = (\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <span className={cn(\"h-2.5 w-2.5 shrink-0 rounded-sm\", s.className)} aria-hidden=\"true\" />\n <span className=\"truncate text-xs\">{s.label}</span>\n <span className=\"ml-auto shrink-0 font-mono text-xs text-muted-foreground\">{s.count}</span>\n </div>\n );\n return (\n <div key={s.key} className=\"flex items-center gap-3\">\n {s.href\n ? renderLink({\n to: s.href,\n className: \"flex min-w-0 flex-1 rounded hover:bg-accent/40\",\n title: `View ${s.label} records`,\n children: meta,\n })\n : meta}\n <div className=\"h-2 w-32 shrink-0 overflow-hidden rounded bg-muted sm:w-48\">\n <div className={cn(\"h-full\", s.className)} style={{ width: `${Math.max(1, (s.count / total) * 100)}%` }} />\n </div>\n {retryable ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-6 shrink-0 px-2 text-[11px]\"\n disabled={retryingKey === s.key}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n onRetry?.(s);\n }}\n >\n {retryingKey === s.key ? \"Retrying…\" : \"Retry\"}\n </Button>\n ) : null}\n </div>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const reactQuery = require("@tanstack/react-query");
|
|
5
|
+
const react = require("react");
|
|
6
|
+
const utils = require("../lib/utils.cjs");
|
|
7
|
+
const Icon = require("./Icon.cjs");
|
|
8
|
+
const defaultFetcher = async (url) => {
|
|
9
|
+
const res = await fetch(url);
|
|
10
|
+
if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);
|
|
11
|
+
return res.json();
|
|
12
|
+
};
|
|
13
|
+
function latestValue(resp) {
|
|
14
|
+
var _a;
|
|
15
|
+
const points = resp == null ? void 0 : resp.points;
|
|
16
|
+
if (!points || points.length === 0) return void 0;
|
|
17
|
+
return (_a = points[points.length - 1]) == null ? void 0 : _a.value;
|
|
18
|
+
}
|
|
19
|
+
function toneClass(pct, [warn, danger]) {
|
|
20
|
+
if (pct >= danger) return "bg-red-500";
|
|
21
|
+
if (pct >= warn) return "bg-amber-500";
|
|
22
|
+
return "bg-emerald-500";
|
|
23
|
+
}
|
|
24
|
+
const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
25
|
+
function deriveCoreBars(usageMilli, limitMilli) {
|
|
26
|
+
const hasUsage = usageMilli !== void 0;
|
|
27
|
+
const usageCores = (usageMilli ?? 0) / 1e3;
|
|
28
|
+
const hasLimit = limitMilli !== void 0 && limitMilli > 0;
|
|
29
|
+
const limitCores = hasLimit ? limitMilli / 1e3 : void 0;
|
|
30
|
+
const coreCount = Math.max(
|
|
31
|
+
1,
|
|
32
|
+
Math.ceil(hasLimit ? limitCores : usageCores)
|
|
33
|
+
);
|
|
34
|
+
const pct = hasLimit ? Math.min(100, Math.round(usageCores / limitCores * 100)) : 0;
|
|
35
|
+
const bars = Array.from({ length: coreCount }, (_, i) => ({
|
|
36
|
+
fill: clamp01(usageCores - i)
|
|
37
|
+
}));
|
|
38
|
+
return { hasUsage, usageCores, limitCores, coreCount, pct, bars };
|
|
39
|
+
}
|
|
40
|
+
function CoreBarsIcon({ icon }) {
|
|
41
|
+
if (typeof icon === "string") {
|
|
42
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { name: icon, width: 14, height: 14, className: "text-muted-foreground" });
|
|
43
|
+
}
|
|
44
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon, width: 14, height: 14, className: "text-muted-foreground" });
|
|
45
|
+
}
|
|
46
|
+
function formatCores(cores) {
|
|
47
|
+
return Number.isInteger(cores) ? String(cores) : cores.toFixed(1);
|
|
48
|
+
}
|
|
49
|
+
function TimeseriesCoreBars({
|
|
50
|
+
baseUrl = "",
|
|
51
|
+
value,
|
|
52
|
+
max,
|
|
53
|
+
title,
|
|
54
|
+
icon,
|
|
55
|
+
range = "1h",
|
|
56
|
+
refreshMs = 5e3,
|
|
57
|
+
thresholds = [75, 90],
|
|
58
|
+
fetcher = defaultFetcher,
|
|
59
|
+
className
|
|
60
|
+
}) {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
const maxIsSeries = typeof max === "object";
|
|
63
|
+
const maxSeries = maxIsSeries ? max : void 0;
|
|
64
|
+
const ids = react.useMemo(() => {
|
|
65
|
+
const list = [value.id];
|
|
66
|
+
if (maxSeries) list.push(maxSeries.id);
|
|
67
|
+
return list;
|
|
68
|
+
}, [value.id, maxSeries]);
|
|
69
|
+
const results = reactQuery.useQueries({
|
|
70
|
+
queries: ids.map((id) => {
|
|
71
|
+
const u = new URL(baseUrl + id, window.location.origin);
|
|
72
|
+
if (range) u.searchParams.set("since", range);
|
|
73
|
+
const requestUrl = u.pathname + u.search;
|
|
74
|
+
return {
|
|
75
|
+
queryKey: ["timeseries", requestUrl],
|
|
76
|
+
queryFn: () => fetcher(requestUrl),
|
|
77
|
+
refetchInterval: refreshMs > 0 ? refreshMs : false,
|
|
78
|
+
staleTime: 0,
|
|
79
|
+
retry: 0
|
|
80
|
+
};
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
const rawValue = latestValue((_a = results[0]) == null ? void 0 : _a.data);
|
|
84
|
+
const usage = rawValue === void 0 ? void 0 : value.transform ? value.transform(rawValue) : rawValue;
|
|
85
|
+
let limit;
|
|
86
|
+
if (typeof max === "number") {
|
|
87
|
+
limit = max;
|
|
88
|
+
} else if (maxSeries) {
|
|
89
|
+
const rawMax = latestValue((_b = results[1]) == null ? void 0 : _b.data);
|
|
90
|
+
limit = rawMax === void 0 ? void 0 : maxSeries.transform ? maxSeries.transform(rawMax) : rawMax;
|
|
91
|
+
}
|
|
92
|
+
const model = deriveCoreBars(usage, limit);
|
|
93
|
+
const tone = toneClass(model.pct, thresholds);
|
|
94
|
+
const caption = model.hasUsage ? `${formatCores(model.usageCores)} / ${model.limitCores !== void 0 ? formatCores(model.limitCores) : "?"} cores` : "—";
|
|
95
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: utils.cn("flex flex-col items-center gap-1", className), children: [
|
|
96
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-10 items-end gap-0.5", children: model.bars.map((bar, i) => {
|
|
97
|
+
const corePct = Math.round(bar.fill * 100);
|
|
98
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
99
|
+
"div",
|
|
100
|
+
{
|
|
101
|
+
className: "relative h-full w-2 overflow-hidden rounded-sm bg-muted",
|
|
102
|
+
title: `core ${i + 1}: ${corePct}%`,
|
|
103
|
+
"aria-label": `core ${i + 1}: ${corePct}%`,
|
|
104
|
+
"data-fill": bar.fill,
|
|
105
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
106
|
+
"div",
|
|
107
|
+
{
|
|
108
|
+
className: utils.cn("absolute inset-x-0 bottom-0 transition-all duration-300", tone),
|
|
109
|
+
style: { height: `${corePct}%` }
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
},
|
|
113
|
+
i
|
|
114
|
+
);
|
|
115
|
+
}) }),
|
|
116
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-foreground", children: caption }),
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
118
|
+
icon ? /* @__PURE__ */ jsxRuntime.jsx(CoreBarsIcon, { icon }) : null,
|
|
119
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: title })
|
|
120
|
+
] })
|
|
121
|
+
] });
|
|
122
|
+
}
|
|
123
|
+
exports.TimeseriesCoreBars = TimeseriesCoreBars;
|
|
124
|
+
exports.deriveCoreBars = deriveCoreBars;
|
|
125
|
+
//# sourceMappingURL=TimeseriesCoreBars.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeseriesCoreBars.cjs","sources":["../../src/data/TimeseriesCoreBars.tsx"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport type { GaugeSeries } from \"./TimeseriesGauge\";\nimport type { TimeseriesResponse } from \"./TimeseriesPanel\";\n\nexport interface TimeseriesCoreBarsProps {\n /** Common prefix; the value/max requests are `baseUrl + id`. */\n baseUrl?: string;\n /** The metric whose latest value (CPU millicores) fills the bars. */\n value: GaugeSeries;\n /**\n * The CPU capacity. Either a `.limit`-style metric series (its latest value in\n * millicores) or a fixed number of millicores. Determines the number of bars\n * (one per core). When omitted or 0, the bar count falls back to the ceiling of\n * the usage in cores so an unbounded reading still renders.\n */\n max?: GaugeSeries | number;\n title: string;\n /** Iconify name or static icon component shown beside the label. */\n icon?: string | StaticIconComponent;\n /** Look-back window passed as ?since=; defaults to \"1h\". */\n range?: string;\n /** Poll interval in ms; defaults to 5000. Pass 0 to disable polling. */\n refreshMs?: number;\n /**\n * Utilisation thresholds (percent of capacity) at which the fill turns amber\n * then red. Defaults to [75, 90].\n */\n thresholds?: [warning: number, danger: number];\n /** Override the default fetch (e.g. to route through an app's API client). */\n fetcher?: (url: string) => Promise<TimeseriesResponse>;\n className?: string;\n}\n\nconst defaultFetcher = async (url: string): Promise<TimeseriesResponse> => {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);\n return res.json();\n};\n\nfunction latestValue(resp: TimeseriesResponse | undefined): number | undefined {\n const points = resp?.points;\n if (!points || points.length === 0) return undefined;\n return points[points.length - 1]?.value;\n}\n\nfunction toneClass(pct: number, [warn, danger]: [number, number]): string {\n if (pct >= danger) return \"bg-red-500\";\n if (pct >= warn) return \"bg-amber-500\";\n return \"bg-emerald-500\";\n}\n\nexport interface CoreBar {\n /** Fraction of this core that is in use, in [0, 1]. */\n fill: number;\n}\n\nexport interface CoreBarsModel {\n /** Whether a usage reading was available. */\n hasUsage: boolean;\n /** Usage expressed in cores (millicores / 1000). */\n usageCores: number;\n /** Capacity in cores; undefined when no limit was resolved. */\n limitCores?: number;\n /** Number of bars rendered (one per core). */\n coreCount: number;\n /** Utilisation percentage of capacity; 0 when unbounded. */\n pct: number;\n /** Per-core fill fractions, left to right. */\n bars: CoreBar[];\n}\n\nconst clamp01 = (n: number): number => Math.min(1, Math.max(0, n));\n\n// deriveCoreBars turns the latest usage/limit millicore readings into a per-core\n// bar model: one bar per core (ceil of the limit), filled left to right by usage\n// so the bar straddling the usage boundary is partially filled and the remainder\n// stays empty. When the limit is missing or 0 the bar count falls back to the\n// ceiling of the usage so an unbounded reading still renders sensibly.\nexport function deriveCoreBars(\n usageMilli: number | undefined,\n limitMilli: number | undefined,\n): CoreBarsModel {\n const hasUsage = usageMilli !== undefined;\n const usageCores = (usageMilli ?? 0) / 1000;\n const hasLimit = limitMilli !== undefined && limitMilli > 0;\n const limitCores = hasLimit ? (limitMilli as number) / 1000 : undefined;\n\n const coreCount = Math.max(\n 1,\n Math.ceil(hasLimit ? (limitCores as number) : usageCores),\n );\n const pct = hasLimit\n ? Math.min(100, Math.round((usageCores / (limitCores as number)) * 100))\n : 0;\n\n const bars: CoreBar[] = Array.from({ length: coreCount }, (_, i) => ({\n fill: clamp01(usageCores - i),\n }));\n\n return { hasUsage, usageCores, limitCores, coreCount, pct, bars };\n}\n\nfunction CoreBarsIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n}\n\nfunction formatCores(cores: number): string {\n return Number.isInteger(cores) ? String(cores) : cores.toFixed(1);\n}\n\n/**\n * TimeseriesCoreBars renders CPU utilisation as a row of vertical bars — one bar\n * per core (the ceiling of the CPU limit) — filled left to right by the latest\n * usage. The bar that straddles the usage boundary is partially filled (\"blocked\n * out\") to show the fractional core in use, and the trailing bars stay empty to\n * show headroom. Both usage and limit are read live from the timeseries store,\n * mirroring TimeseriesGauge's data plumbing; the bar colour crosses warning and\n * danger thresholds. A \"<used> / <cores>\" caption sits below with the icon+title.\n */\nexport function TimeseriesCoreBars({\n baseUrl = \"\",\n value,\n max,\n title,\n icon,\n range = \"1h\",\n refreshMs = 5000,\n thresholds = [75, 90],\n fetcher = defaultFetcher,\n className,\n}: TimeseriesCoreBarsProps) {\n const maxIsSeries = typeof max === \"object\";\n const maxSeries = maxIsSeries ? max : undefined;\n const ids = useMemo(() => {\n const list = [value.id];\n if (maxSeries) list.push(maxSeries.id);\n return list;\n }, [value.id, maxSeries]);\n\n const results = useQueries({\n queries: ids.map((id) => {\n const u = new URL(baseUrl + id, window.location.origin);\n if (range) u.searchParams.set(\"since\", range);\n const requestUrl = u.pathname + u.search;\n return {\n queryKey: [\"timeseries\", requestUrl],\n queryFn: () => fetcher(requestUrl),\n refetchInterval: refreshMs > 0 ? refreshMs : false,\n staleTime: 0,\n retry: 0,\n };\n }),\n });\n\n const rawValue = latestValue(results[0]?.data);\n const usage = rawValue === undefined ? undefined : value.transform ? value.transform(rawValue) : rawValue;\n\n let limit: number | undefined;\n if (typeof max === \"number\") {\n limit = max;\n } else if (maxSeries) {\n const rawMax = latestValue(results[1]?.data);\n limit = rawMax === undefined ? undefined : maxSeries.transform ? maxSeries.transform(rawMax) : rawMax;\n }\n\n const model = deriveCoreBars(usage, limit);\n const tone = toneClass(model.pct, thresholds);\n const caption = model.hasUsage\n ? `${formatCores(model.usageCores)} / ${model.limitCores !== undefined ? formatCores(model.limitCores) : \"?\"} cores`\n : \"—\";\n\n return (\n <div className={cn(\"flex flex-col items-center gap-1\", className)}>\n <div className=\"flex h-10 items-end gap-0.5\">\n {model.bars.map((bar, i) => {\n const corePct = Math.round(bar.fill * 100);\n return (\n <div\n key={i}\n className=\"relative h-full w-2 overflow-hidden rounded-sm bg-muted\"\n title={`core ${i + 1}: ${corePct}%`}\n aria-label={`core ${i + 1}: ${corePct}%`}\n data-fill={bar.fill}\n >\n <div\n className={cn(\"absolute inset-x-0 bottom-0 transition-all duration-300\", tone)}\n style={{ height: `${corePct}%` }}\n />\n </div>\n );\n })}\n </div>\n <span className=\"text-xs font-semibold text-foreground\">{caption}</span>\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n {icon ? <CoreBarsIcon icon={icon} /> : null}\n <span>{title}</span>\n </div>\n </div>\n );\n}\n"],"names":["jsx","Icon","useMemo","useQueries","cn","jsxs"],"mappings":";;;;;;;AAoCA,MAAM,iBAAiB,OAAO,QAA6C;AACzE,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,2BAA2B,IAAI,MAAM,EAAE;AACpE,SAAO,IAAI,KAAA;AACb;AAEA,SAAS,YAAY,MAA0D;;AAC7E,QAAM,SAAS,6BAAM;AACrB,MAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAC3C,UAAO,YAAO,OAAO,SAAS,CAAC,MAAxB,mBAA2B;AACpC;AAEA,SAAS,UAAU,KAAa,CAAC,MAAM,MAAM,GAA6B;AACxE,MAAI,OAAO,OAAQ,QAAO;AAC1B,MAAI,OAAO,KAAM,QAAO;AACxB,SAAO;AACT;AAsBA,MAAM,UAAU,CAAC,MAAsB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AAO1D,SAAS,eACd,YACA,YACe;AACf,QAAM,WAAW,eAAe;AAChC,QAAM,cAAc,cAAc,KAAK;AACvC,QAAM,WAAW,eAAe,UAAa,aAAa;AAC1D,QAAM,aAAa,WAAY,aAAwB,MAAO;AAE9D,QAAM,YAAY,KAAK;AAAA,IACrB;AAAA,IACA,KAAK,KAAK,WAAY,aAAwB,UAAU;AAAA,EAAA;AAE1D,QAAM,MAAM,WACR,KAAK,IAAI,KAAK,KAAK,MAAO,aAAc,aAAyB,GAAG,CAAC,IACrE;AAEJ,QAAM,OAAkB,MAAM,KAAK,EAAE,QAAQ,UAAA,GAAa,CAAC,GAAG,OAAO;AAAA,IACnE,MAAM,QAAQ,aAAa,CAAC;AAAA,EAAA,EAC5B;AAEF,SAAO,EAAE,UAAU,YAAY,YAAY,WAAW,KAAK,KAAA;AAC7D;AAEA,SAAS,aAAa,EAAE,QAAgD;AACtE,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAOA,+BAACC,KAAAA,QAAK,MAAM,MAAM,OAAO,IAAI,QAAQ,IAAI,WAAU,wBAAA,CAAwB;AAAA,EACpF;AACA,SAAOD,+BAACC,KAAAA,QAAK,MAAY,OAAO,IAAI,QAAQ,IAAI,WAAU,yBAAwB;AACpF;AAEA,SAAS,YAAY,OAAuB;AAC1C,SAAO,OAAO,UAAU,KAAK,IAAI,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAClE;AAWO,SAAS,mBAAmB;AAAA,EACjC,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa,CAAC,IAAI,EAAE;AAAA,EACpB,UAAU;AAAA,EACV;AACF,GAA4B;;AAC1B,QAAM,cAAc,OAAO,QAAQ;AACnC,QAAM,YAAY,cAAc,MAAM;AACtC,QAAM,MAAMC,MAAAA,QAAQ,MAAM;AACxB,UAAM,OAAO,CAAC,MAAM,EAAE;AACtB,QAAI,UAAW,MAAK,KAAK,UAAU,EAAE;AACrC,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,IAAI,SAAS,CAAC;AAExB,QAAM,UAAUC,WAAAA,WAAW;AAAA,IACzB,SAAS,IAAI,IAAI,CAAC,OAAO;AACvB,YAAM,IAAI,IAAI,IAAI,UAAU,IAAI,OAAO,SAAS,MAAM;AACtD,UAAI,MAAO,GAAE,aAAa,IAAI,SAAS,KAAK;AAC5C,YAAM,aAAa,EAAE,WAAW,EAAE;AAClC,aAAO;AAAA,QACL,UAAU,CAAC,cAAc,UAAU;AAAA,QACnC,SAAS,MAAM,QAAQ,UAAU;AAAA,QACjC,iBAAiB,YAAY,IAAI,YAAY;AAAA,QAC7C,WAAW;AAAA,QACX,OAAO;AAAA,MAAA;AAAA,IAEX,CAAC;AAAA,EAAA,CACF;AAED,QAAM,WAAW,aAAY,aAAQ,CAAC,MAAT,mBAAY,IAAI;AAC7C,QAAM,QAAQ,aAAa,SAAY,SAAY,MAAM,YAAY,MAAM,UAAU,QAAQ,IAAI;AAEjG,MAAI;AACJ,MAAI,OAAO,QAAQ,UAAU;AAC3B,YAAQ;AAAA,EACV,WAAW,WAAW;AACpB,UAAM,SAAS,aAAY,aAAQ,CAAC,MAAT,mBAAY,IAAI;AAC3C,YAAQ,WAAW,SAAY,SAAY,UAAU,YAAY,UAAU,UAAU,MAAM,IAAI;AAAA,EACjG;AAEA,QAAM,QAAQ,eAAe,OAAO,KAAK;AACzC,QAAM,OAAO,UAAU,MAAM,KAAK,UAAU;AAC5C,QAAM,UAAU,MAAM,WAClB,GAAG,YAAY,MAAM,UAAU,CAAC,MAAM,MAAM,eAAe,SAAY,YAAY,MAAM,UAAU,IAAI,GAAG,WAC1G;AAEJ,yCACG,OAAA,EAAI,WAAWC,MAAAA,GAAG,oCAAoC,SAAS,GAC9D,UAAA;AAAA,IAAAJ,2BAAAA,IAAC,OAAA,EAAI,WAAU,+BACZ,UAAA,MAAM,KAAK,IAAI,CAAC,KAAK,MAAM;AAC1B,YAAM,UAAU,KAAK,MAAM,IAAI,OAAO,GAAG;AACzC,aACEA,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UAEC,WAAU;AAAA,UACV,OAAO,QAAQ,IAAI,CAAC,KAAK,OAAO;AAAA,UAChC,cAAY,QAAQ,IAAI,CAAC,KAAK,OAAO;AAAA,UACrC,aAAW,IAAI;AAAA,UAEf,UAAAA,2BAAAA;AAAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAWI,MAAAA,GAAG,2DAA2D,IAAI;AAAA,cAC7E,OAAO,EAAE,QAAQ,GAAG,OAAO,IAAA;AAAA,YAAI;AAAA,UAAA;AAAA,QACjC;AAAA,QATK;AAAA,MAAA;AAAA,IAYX,CAAC,EAAA,CACH;AAAA,IACAJ,2BAAAA,IAAC,QAAA,EAAK,WAAU,yCAAyC,UAAA,SAAQ;AAAA,IACjEK,2BAAAA,KAAC,OAAA,EAAI,WAAU,yDACZ,UAAA;AAAA,MAAA,OAAOL,2BAAAA,IAAC,cAAA,EAAa,KAAA,CAAY,IAAK;AAAA,MACvCA,2BAAAA,IAAC,UAAM,UAAA,MAAA,CAAM;AAAA,IAAA,EAAA,CACf;AAAA,EAAA,GACF;AAEJ;;;"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { StaticIconComponent } from './Icon';
|
|
2
|
+
import { GaugeSeries } from './TimeseriesGauge';
|
|
3
|
+
import { TimeseriesResponse } from './TimeseriesPanel';
|
|
4
|
+
export interface TimeseriesCoreBarsProps {
|
|
5
|
+
/** Common prefix; the value/max requests are `baseUrl + id`. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/** The metric whose latest value (CPU millicores) fills the bars. */
|
|
8
|
+
value: GaugeSeries;
|
|
9
|
+
/**
|
|
10
|
+
* The CPU capacity. Either a `.limit`-style metric series (its latest value in
|
|
11
|
+
* millicores) or a fixed number of millicores. Determines the number of bars
|
|
12
|
+
* (one per core). When omitted or 0, the bar count falls back to the ceiling of
|
|
13
|
+
* the usage in cores so an unbounded reading still renders.
|
|
14
|
+
*/
|
|
15
|
+
max?: GaugeSeries | number;
|
|
16
|
+
title: string;
|
|
17
|
+
/** Iconify name or static icon component shown beside the label. */
|
|
18
|
+
icon?: string | StaticIconComponent;
|
|
19
|
+
/** Look-back window passed as ?since=; defaults to "1h". */
|
|
20
|
+
range?: string;
|
|
21
|
+
/** Poll interval in ms; defaults to 5000. Pass 0 to disable polling. */
|
|
22
|
+
refreshMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Utilisation thresholds (percent of capacity) at which the fill turns amber
|
|
25
|
+
* then red. Defaults to [75, 90].
|
|
26
|
+
*/
|
|
27
|
+
thresholds?: [warning: number, danger: number];
|
|
28
|
+
/** Override the default fetch (e.g. to route through an app's API client). */
|
|
29
|
+
fetcher?: (url: string) => Promise<TimeseriesResponse>;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CoreBar {
|
|
33
|
+
/** Fraction of this core that is in use, in [0, 1]. */
|
|
34
|
+
fill: number;
|
|
35
|
+
}
|
|
36
|
+
export interface CoreBarsModel {
|
|
37
|
+
/** Whether a usage reading was available. */
|
|
38
|
+
hasUsage: boolean;
|
|
39
|
+
/** Usage expressed in cores (millicores / 1000). */
|
|
40
|
+
usageCores: number;
|
|
41
|
+
/** Capacity in cores; undefined when no limit was resolved. */
|
|
42
|
+
limitCores?: number;
|
|
43
|
+
/** Number of bars rendered (one per core). */
|
|
44
|
+
coreCount: number;
|
|
45
|
+
/** Utilisation percentage of capacity; 0 when unbounded. */
|
|
46
|
+
pct: number;
|
|
47
|
+
/** Per-core fill fractions, left to right. */
|
|
48
|
+
bars: CoreBar[];
|
|
49
|
+
}
|
|
50
|
+
export declare function deriveCoreBars(usageMilli: number | undefined, limitMilli: number | undefined): CoreBarsModel;
|
|
51
|
+
/**
|
|
52
|
+
* TimeseriesCoreBars renders CPU utilisation as a row of vertical bars — one bar
|
|
53
|
+
* per core (the ceiling of the CPU limit) — filled left to right by the latest
|
|
54
|
+
* usage. The bar that straddles the usage boundary is partially filled ("blocked
|
|
55
|
+
* out") to show the fractional core in use, and the trailing bars stay empty to
|
|
56
|
+
* show headroom. Both usage and limit are read live from the timeseries store,
|
|
57
|
+
* mirroring TimeseriesGauge's data plumbing; the bar colour crosses warning and
|
|
58
|
+
* danger thresholds. A "<used> / <cores>" caption sits below with the icon+title.
|
|
59
|
+
*/
|
|
60
|
+
export declare function TimeseriesCoreBars({ baseUrl, value, max, title, icon, range, refreshMs, thresholds, fetcher, className, }: TimeseriesCoreBarsProps): import("react/jsx-runtime").JSX.Element;
|
|
61
|
+
//# sourceMappingURL=TimeseriesCoreBars.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeseriesCoreBars.d.ts","sourceRoot":"","sources":["../../src/data/TimeseriesCoreBars.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAC;IACnB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACpC,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAoBD,MAAM,WAAW,OAAO;IACtB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,QAAQ,EAAE,OAAO,CAAC;IAClB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AASD,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,aAAa,CAmBf;AAaD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAY,EACZ,KAAK,EACL,GAAG,EACH,KAAK,EACL,IAAI,EACJ,KAAY,EACZ,SAAgB,EAChB,UAAqB,EACrB,OAAwB,EACxB,SAAS,GACV,EAAE,uBAAuB,2CAqEzB"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useQueries } from "@tanstack/react-query";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Icon } from "./Icon.js";
|
|
6
|
+
const defaultFetcher = async (url) => {
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);
|
|
9
|
+
return res.json();
|
|
10
|
+
};
|
|
11
|
+
function latestValue(resp) {
|
|
12
|
+
var _a;
|
|
13
|
+
const points = resp == null ? void 0 : resp.points;
|
|
14
|
+
if (!points || points.length === 0) return void 0;
|
|
15
|
+
return (_a = points[points.length - 1]) == null ? void 0 : _a.value;
|
|
16
|
+
}
|
|
17
|
+
function toneClass(pct, [warn, danger]) {
|
|
18
|
+
if (pct >= danger) return "bg-red-500";
|
|
19
|
+
if (pct >= warn) return "bg-amber-500";
|
|
20
|
+
return "bg-emerald-500";
|
|
21
|
+
}
|
|
22
|
+
const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
23
|
+
function deriveCoreBars(usageMilli, limitMilli) {
|
|
24
|
+
const hasUsage = usageMilli !== void 0;
|
|
25
|
+
const usageCores = (usageMilli ?? 0) / 1e3;
|
|
26
|
+
const hasLimit = limitMilli !== void 0 && limitMilli > 0;
|
|
27
|
+
const limitCores = hasLimit ? limitMilli / 1e3 : void 0;
|
|
28
|
+
const coreCount = Math.max(
|
|
29
|
+
1,
|
|
30
|
+
Math.ceil(hasLimit ? limitCores : usageCores)
|
|
31
|
+
);
|
|
32
|
+
const pct = hasLimit ? Math.min(100, Math.round(usageCores / limitCores * 100)) : 0;
|
|
33
|
+
const bars = Array.from({ length: coreCount }, (_, i) => ({
|
|
34
|
+
fill: clamp01(usageCores - i)
|
|
35
|
+
}));
|
|
36
|
+
return { hasUsage, usageCores, limitCores, coreCount, pct, bars };
|
|
37
|
+
}
|
|
38
|
+
function CoreBarsIcon({ icon }) {
|
|
39
|
+
if (typeof icon === "string") {
|
|
40
|
+
return /* @__PURE__ */ jsx(Icon, { name: icon, width: 14, height: 14, className: "text-muted-foreground" });
|
|
41
|
+
}
|
|
42
|
+
return /* @__PURE__ */ jsx(Icon, { icon, width: 14, height: 14, className: "text-muted-foreground" });
|
|
43
|
+
}
|
|
44
|
+
function formatCores(cores) {
|
|
45
|
+
return Number.isInteger(cores) ? String(cores) : cores.toFixed(1);
|
|
46
|
+
}
|
|
47
|
+
function TimeseriesCoreBars({
|
|
48
|
+
baseUrl = "",
|
|
49
|
+
value,
|
|
50
|
+
max,
|
|
51
|
+
title,
|
|
52
|
+
icon,
|
|
53
|
+
range = "1h",
|
|
54
|
+
refreshMs = 5e3,
|
|
55
|
+
thresholds = [75, 90],
|
|
56
|
+
fetcher = defaultFetcher,
|
|
57
|
+
className
|
|
58
|
+
}) {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
const maxIsSeries = typeof max === "object";
|
|
61
|
+
const maxSeries = maxIsSeries ? max : void 0;
|
|
62
|
+
const ids = useMemo(() => {
|
|
63
|
+
const list = [value.id];
|
|
64
|
+
if (maxSeries) list.push(maxSeries.id);
|
|
65
|
+
return list;
|
|
66
|
+
}, [value.id, maxSeries]);
|
|
67
|
+
const results = useQueries({
|
|
68
|
+
queries: ids.map((id) => {
|
|
69
|
+
const u = new URL(baseUrl + id, window.location.origin);
|
|
70
|
+
if (range) u.searchParams.set("since", range);
|
|
71
|
+
const requestUrl = u.pathname + u.search;
|
|
72
|
+
return {
|
|
73
|
+
queryKey: ["timeseries", requestUrl],
|
|
74
|
+
queryFn: () => fetcher(requestUrl),
|
|
75
|
+
refetchInterval: refreshMs > 0 ? refreshMs : false,
|
|
76
|
+
staleTime: 0,
|
|
77
|
+
retry: 0
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
const rawValue = latestValue((_a = results[0]) == null ? void 0 : _a.data);
|
|
82
|
+
const usage = rawValue === void 0 ? void 0 : value.transform ? value.transform(rawValue) : rawValue;
|
|
83
|
+
let limit;
|
|
84
|
+
if (typeof max === "number") {
|
|
85
|
+
limit = max;
|
|
86
|
+
} else if (maxSeries) {
|
|
87
|
+
const rawMax = latestValue((_b = results[1]) == null ? void 0 : _b.data);
|
|
88
|
+
limit = rawMax === void 0 ? void 0 : maxSeries.transform ? maxSeries.transform(rawMax) : rawMax;
|
|
89
|
+
}
|
|
90
|
+
const model = deriveCoreBars(usage, limit);
|
|
91
|
+
const tone = toneClass(model.pct, thresholds);
|
|
92
|
+
const caption = model.hasUsage ? `${formatCores(model.usageCores)} / ${model.limitCores !== void 0 ? formatCores(model.limitCores) : "?"} cores` : "—";
|
|
93
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-center gap-1", className), children: [
|
|
94
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-10 items-end gap-0.5", children: model.bars.map((bar, i) => {
|
|
95
|
+
const corePct = Math.round(bar.fill * 100);
|
|
96
|
+
return /* @__PURE__ */ jsx(
|
|
97
|
+
"div",
|
|
98
|
+
{
|
|
99
|
+
className: "relative h-full w-2 overflow-hidden rounded-sm bg-muted",
|
|
100
|
+
title: `core ${i + 1}: ${corePct}%`,
|
|
101
|
+
"aria-label": `core ${i + 1}: ${corePct}%`,
|
|
102
|
+
"data-fill": bar.fill,
|
|
103
|
+
children: /* @__PURE__ */ jsx(
|
|
104
|
+
"div",
|
|
105
|
+
{
|
|
106
|
+
className: cn("absolute inset-x-0 bottom-0 transition-all duration-300", tone),
|
|
107
|
+
style: { height: `${corePct}%` }
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
i
|
|
112
|
+
);
|
|
113
|
+
}) }),
|
|
114
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-foreground", children: caption }),
|
|
115
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
116
|
+
icon ? /* @__PURE__ */ jsx(CoreBarsIcon, { icon }) : null,
|
|
117
|
+
/* @__PURE__ */ jsx("span", { children: title })
|
|
118
|
+
] })
|
|
119
|
+
] });
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
TimeseriesCoreBars,
|
|
123
|
+
deriveCoreBars
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=TimeseriesCoreBars.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeseriesCoreBars.js","sources":["../../src/data/TimeseriesCoreBars.tsx"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport type { GaugeSeries } from \"./TimeseriesGauge\";\nimport type { TimeseriesResponse } from \"./TimeseriesPanel\";\n\nexport interface TimeseriesCoreBarsProps {\n /** Common prefix; the value/max requests are `baseUrl + id`. */\n baseUrl?: string;\n /** The metric whose latest value (CPU millicores) fills the bars. */\n value: GaugeSeries;\n /**\n * The CPU capacity. Either a `.limit`-style metric series (its latest value in\n * millicores) or a fixed number of millicores. Determines the number of bars\n * (one per core). When omitted or 0, the bar count falls back to the ceiling of\n * the usage in cores so an unbounded reading still renders.\n */\n max?: GaugeSeries | number;\n title: string;\n /** Iconify name or static icon component shown beside the label. */\n icon?: string | StaticIconComponent;\n /** Look-back window passed as ?since=; defaults to \"1h\". */\n range?: string;\n /** Poll interval in ms; defaults to 5000. Pass 0 to disable polling. */\n refreshMs?: number;\n /**\n * Utilisation thresholds (percent of capacity) at which the fill turns amber\n * then red. Defaults to [75, 90].\n */\n thresholds?: [warning: number, danger: number];\n /** Override the default fetch (e.g. to route through an app's API client). */\n fetcher?: (url: string) => Promise<TimeseriesResponse>;\n className?: string;\n}\n\nconst defaultFetcher = async (url: string): Promise<TimeseriesResponse> => {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);\n return res.json();\n};\n\nfunction latestValue(resp: TimeseriesResponse | undefined): number | undefined {\n const points = resp?.points;\n if (!points || points.length === 0) return undefined;\n return points[points.length - 1]?.value;\n}\n\nfunction toneClass(pct: number, [warn, danger]: [number, number]): string {\n if (pct >= danger) return \"bg-red-500\";\n if (pct >= warn) return \"bg-amber-500\";\n return \"bg-emerald-500\";\n}\n\nexport interface CoreBar {\n /** Fraction of this core that is in use, in [0, 1]. */\n fill: number;\n}\n\nexport interface CoreBarsModel {\n /** Whether a usage reading was available. */\n hasUsage: boolean;\n /** Usage expressed in cores (millicores / 1000). */\n usageCores: number;\n /** Capacity in cores; undefined when no limit was resolved. */\n limitCores?: number;\n /** Number of bars rendered (one per core). */\n coreCount: number;\n /** Utilisation percentage of capacity; 0 when unbounded. */\n pct: number;\n /** Per-core fill fractions, left to right. */\n bars: CoreBar[];\n}\n\nconst clamp01 = (n: number): number => Math.min(1, Math.max(0, n));\n\n// deriveCoreBars turns the latest usage/limit millicore readings into a per-core\n// bar model: one bar per core (ceil of the limit), filled left to right by usage\n// so the bar straddling the usage boundary is partially filled and the remainder\n// stays empty. When the limit is missing or 0 the bar count falls back to the\n// ceiling of the usage so an unbounded reading still renders sensibly.\nexport function deriveCoreBars(\n usageMilli: number | undefined,\n limitMilli: number | undefined,\n): CoreBarsModel {\n const hasUsage = usageMilli !== undefined;\n const usageCores = (usageMilli ?? 0) / 1000;\n const hasLimit = limitMilli !== undefined && limitMilli > 0;\n const limitCores = hasLimit ? (limitMilli as number) / 1000 : undefined;\n\n const coreCount = Math.max(\n 1,\n Math.ceil(hasLimit ? (limitCores as number) : usageCores),\n );\n const pct = hasLimit\n ? Math.min(100, Math.round((usageCores / (limitCores as number)) * 100))\n : 0;\n\n const bars: CoreBar[] = Array.from({ length: coreCount }, (_, i) => ({\n fill: clamp01(usageCores - i),\n }));\n\n return { hasUsage, usageCores, limitCores, coreCount, pct, bars };\n}\n\nfunction CoreBarsIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n}\n\nfunction formatCores(cores: number): string {\n return Number.isInteger(cores) ? String(cores) : cores.toFixed(1);\n}\n\n/**\n * TimeseriesCoreBars renders CPU utilisation as a row of vertical bars — one bar\n * per core (the ceiling of the CPU limit) — filled left to right by the latest\n * usage. The bar that straddles the usage boundary is partially filled (\"blocked\n * out\") to show the fractional core in use, and the trailing bars stay empty to\n * show headroom. Both usage and limit are read live from the timeseries store,\n * mirroring TimeseriesGauge's data plumbing; the bar colour crosses warning and\n * danger thresholds. A \"<used> / <cores>\" caption sits below with the icon+title.\n */\nexport function TimeseriesCoreBars({\n baseUrl = \"\",\n value,\n max,\n title,\n icon,\n range = \"1h\",\n refreshMs = 5000,\n thresholds = [75, 90],\n fetcher = defaultFetcher,\n className,\n}: TimeseriesCoreBarsProps) {\n const maxIsSeries = typeof max === \"object\";\n const maxSeries = maxIsSeries ? max : undefined;\n const ids = useMemo(() => {\n const list = [value.id];\n if (maxSeries) list.push(maxSeries.id);\n return list;\n }, [value.id, maxSeries]);\n\n const results = useQueries({\n queries: ids.map((id) => {\n const u = new URL(baseUrl + id, window.location.origin);\n if (range) u.searchParams.set(\"since\", range);\n const requestUrl = u.pathname + u.search;\n return {\n queryKey: [\"timeseries\", requestUrl],\n queryFn: () => fetcher(requestUrl),\n refetchInterval: refreshMs > 0 ? refreshMs : false,\n staleTime: 0,\n retry: 0,\n };\n }),\n });\n\n const rawValue = latestValue(results[0]?.data);\n const usage = rawValue === undefined ? undefined : value.transform ? value.transform(rawValue) : rawValue;\n\n let limit: number | undefined;\n if (typeof max === \"number\") {\n limit = max;\n } else if (maxSeries) {\n const rawMax = latestValue(results[1]?.data);\n limit = rawMax === undefined ? undefined : maxSeries.transform ? maxSeries.transform(rawMax) : rawMax;\n }\n\n const model = deriveCoreBars(usage, limit);\n const tone = toneClass(model.pct, thresholds);\n const caption = model.hasUsage\n ? `${formatCores(model.usageCores)} / ${model.limitCores !== undefined ? formatCores(model.limitCores) : \"?\"} cores`\n : \"—\";\n\n return (\n <div className={cn(\"flex flex-col items-center gap-1\", className)}>\n <div className=\"flex h-10 items-end gap-0.5\">\n {model.bars.map((bar, i) => {\n const corePct = Math.round(bar.fill * 100);\n return (\n <div\n key={i}\n className=\"relative h-full w-2 overflow-hidden rounded-sm bg-muted\"\n title={`core ${i + 1}: ${corePct}%`}\n aria-label={`core ${i + 1}: ${corePct}%`}\n data-fill={bar.fill}\n >\n <div\n className={cn(\"absolute inset-x-0 bottom-0 transition-all duration-300\", tone)}\n style={{ height: `${corePct}%` }}\n />\n </div>\n );\n })}\n </div>\n <span className=\"text-xs font-semibold text-foreground\">{caption}</span>\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n {icon ? <CoreBarsIcon icon={icon} /> : null}\n <span>{title}</span>\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|