@flanksource/clicky-ui 0.2.6 → 0.2.8

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.
@@ -11,10 +11,12 @@ function MatrixTable({
11
11
  density = "default",
12
12
  columnWidth = 48,
13
13
  headerHeight = 120,
14
+ maxHeight,
14
15
  className,
15
16
  columnClassName,
16
17
  rowLabelClassName,
17
- cellClassName
18
+ cellClassName,
19
+ headerClassName
18
20
  }) {
19
21
  if (rows.length === 0 || columns.length === 0) {
20
22
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-muted-foreground", children: emptyMessage });
@@ -29,21 +31,25 @@ function MatrixTable({
29
31
  "div",
30
32
  {
31
33
  className: utils.cn("overflow-auto rounded-md border border-border", className),
32
- style: angledHeaders ? { paddingRight: diagonalOverhang } : void 0,
34
+ style: {
35
+ ...angledHeaders ? { paddingRight: diagonalOverhang } : void 0,
36
+ ...maxHeight !== void 0 ? { maxHeight } : void 0
37
+ },
33
38
  children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: utils.cn("w-max border-collapse", compact ? "text-xs" : "text-sm"), children: [
34
39
  angledHeaders && /* @__PURE__ */ jsxRuntime.jsxs("colgroup", { children: [
35
40
  /* @__PURE__ */ jsxRuntime.jsx("col", {}),
36
41
  columns.map((_, index) => /* @__PURE__ */ jsxRuntime.jsx("col", { style: { width: columnWidth } }, index))
37
42
  ] }),
38
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: utils.cn("bg-muted/40", !angledHeaders && "border-b border-border"), children: [
43
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: utils.cn("bg-muted/40", !angledHeaders && "border-b border-border", headerClassName), children: [
39
44
  /* @__PURE__ */ jsxRuntime.jsx(
40
45
  "th",
41
46
  {
42
47
  scope: "col",
43
48
  className: utils.cn(
44
- "sticky left-0 z-10 min-w-44 border-r border-border bg-muted/40 text-left font-medium",
49
+ "sticky left-0 top-0 z-30 min-w-44 border-r border-border bg-muted text-left font-medium",
45
50
  headerCellPadding,
46
- angledHeaders ? "align-bottom" : columnClassName
51
+ angledHeaders ? "align-bottom" : utils.cn("border-b border-border", columnClassName),
52
+ headerClassName
47
53
  ),
48
54
  children: corner
49
55
  }
@@ -55,8 +61,9 @@ function MatrixTable({
55
61
  scope: "col",
56
62
  title: columnTitle(column),
57
63
  className: utils.cn(
58
- "relative overflow-visible border-b border-border p-0 align-bottom",
59
- columnClassName
64
+ "sticky top-0 z-20 overflow-visible border-b border-border bg-muted p-0 align-bottom",
65
+ columnClassName,
66
+ headerClassName
60
67
  ),
61
68
  style: {
62
69
  width: columnWidth,
@@ -110,9 +117,10 @@ function MatrixTable({
110
117
  {
111
118
  scope: "col",
112
119
  className: utils.cn(
113
- "min-w-24 whitespace-nowrap text-center text-xs font-medium text-muted-foreground",
120
+ "sticky top-0 z-20 min-w-24 whitespace-nowrap border-b border-border bg-muted text-center text-xs font-medium text-muted-foreground",
114
121
  headerCellPadding,
115
- columnClassName
122
+ columnClassName,
123
+ headerClassName
116
124
  ),
117
125
  children: column
118
126
  },
@@ -1 +1 @@
1
- {"version":3,"file":"MatrixTable.cjs","sources":["../../src/data/MatrixTable.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\n\nexport type MatrixTableRow = {\n key: string;\n label: ReactNode;\n cells: ReactNode[];\n};\n\nexport type MatrixTableProps = {\n columns: ReactNode[];\n rows: MatrixTableRow[];\n corner?: ReactNode;\n emptyMessage?: ReactNode;\n angledHeaders?: boolean;\n density?: \"default\" | \"compact\";\n columnWidth?: number;\n headerHeight?: number;\n className?: string;\n columnClassName?: string;\n rowLabelClassName?: string;\n cellClassName?: string;\n};\n\nexport function MatrixTable({\n columns,\n rows,\n corner,\n emptyMessage = \"No data\",\n angledHeaders = false,\n density = \"default\",\n columnWidth = 48,\n headerHeight = 120,\n className,\n columnClassName,\n rowLabelClassName,\n cellClassName,\n}: MatrixTableProps) {\n if (rows.length === 0 || columns.length === 0) {\n return <div className=\"text-sm text-muted-foreground\">{emptyMessage}</div>;\n }\n\n const textWidth = Math.round(headerHeight * 1.41);\n const diagonalOverhang = Math.round(headerHeight * 0.71);\n const compact = density === \"compact\";\n const headerCellPadding = compact ? \"px-density-2 py-density-1\" : \"px-density-3 py-density-2\";\n const rowLabelPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n const bodyCellPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n\n return (\n <div\n className={cn(\"overflow-auto rounded-md border border-border\", className)}\n style={angledHeaders ? { paddingRight: diagonalOverhang } : undefined}\n >\n <table className={cn(\"w-max border-collapse\", compact ? \"text-xs\" : \"text-sm\")}>\n {angledHeaders && (\n <colgroup>\n <col />\n {columns.map((_, index) => (\n <col key={index} style={{ width: columnWidth }} />\n ))}\n </colgroup>\n )}\n <thead>\n <tr className={cn(\"bg-muted/40\", !angledHeaders && \"border-b border-border\")}>\n <th\n scope=\"col\"\n className={cn(\n \"sticky left-0 z-10 min-w-44 border-r border-border bg-muted/40 text-left font-medium\",\n headerCellPadding,\n angledHeaders ? \"align-bottom\" : columnClassName,\n )}\n >\n {corner}\n </th>\n {columns.map((column, index) =>\n angledHeaders ? (\n <th\n key={index}\n scope=\"col\"\n title={columnTitle(column)}\n className={cn(\n \"relative overflow-visible border-b border-border p-0 align-bottom\",\n columnClassName,\n )}\n style={{\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n height: headerHeight,\n }}\n >\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: columnWidth,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n {index === 0 && (\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: 0,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n )}\n <div\n className=\"absolute bottom-2 overflow-hidden text-ellipsis whitespace-nowrap pl-1 text-left text-xs font-medium leading-none text-muted-foreground\"\n style={{\n left: columnWidth / 2,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n >\n {column}\n </div>\n </th>\n ) : (\n <th\n key={index}\n scope=\"col\"\n className={cn(\n \"min-w-24 whitespace-nowrap text-center text-xs font-medium text-muted-foreground\",\n headerCellPadding,\n columnClassName,\n )}\n >\n {column}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr key={row.key} className=\"border-b border-border last:border-b-0\">\n <th\n scope=\"row\"\n className={cn(\n \"sticky left-0 z-10 border-r border-border bg-background text-left font-medium\",\n rowLabelPadding,\n rowLabelClassName,\n )}\n >\n {row.label}\n </th>\n {columns.map((_, index) => (\n <td\n key={index}\n className={cn(\n angledHeaders\n ? \"p-0 text-center align-middle\"\n : cn(bodyCellPadding, \"text-center align-middle\"),\n cellClassName,\n )}\n style={\n angledHeaders\n ? {\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n }\n : undefined\n }\n >\n {row.cells[index] ?? null}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction columnTitle(column: ReactNode) {\n if (typeof column === \"string\" || typeof column === \"number\") {\n return String(column);\n }\n return undefined;\n}\n"],"names":["jsx","cn","jsxs"],"mappings":"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"}
1
+ {"version":3,"file":"MatrixTable.cjs","sources":["../../src/data/MatrixTable.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\n\nexport type MatrixTableRow = {\n key: string;\n label: ReactNode;\n cells: ReactNode[];\n};\n\nexport type MatrixTableProps = {\n columns: ReactNode[];\n rows: MatrixTableRow[];\n corner?: ReactNode;\n emptyMessage?: ReactNode;\n angledHeaders?: boolean;\n density?: \"default\" | \"compact\";\n columnWidth?: number;\n headerHeight?: number;\n // maxHeight caps the scroll viewport so the body scrolls under frozen\n // headers (Excel-style freeze panes). Accepts any CSS length; number => px.\n maxHeight?: number | string;\n className?: string;\n columnClassName?: string;\n rowLabelClassName?: string;\n cellClassName?: string;\n // headerClassName is appended to the header row and every header cell (corner\n // + columns). Use it to override the default gray header background, e.g.\n // \"bg-transparent\", since tailwind-merge lets the later class win.\n headerClassName?: string;\n};\n\nexport function MatrixTable({\n columns,\n rows,\n corner,\n emptyMessage = \"No data\",\n angledHeaders = false,\n density = \"default\",\n columnWidth = 48,\n headerHeight = 120,\n maxHeight,\n className,\n columnClassName,\n rowLabelClassName,\n cellClassName,\n headerClassName,\n}: MatrixTableProps) {\n if (rows.length === 0 || columns.length === 0) {\n return <div className=\"text-sm text-muted-foreground\">{emptyMessage}</div>;\n }\n\n const textWidth = Math.round(headerHeight * 1.41);\n const diagonalOverhang = Math.round(headerHeight * 0.71);\n const compact = density === \"compact\";\n const headerCellPadding = compact ? \"px-density-2 py-density-1\" : \"px-density-3 py-density-2\";\n const rowLabelPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n const bodyCellPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n\n return (\n <div\n className={cn(\"overflow-auto rounded-md border border-border\", className)}\n style={{\n ...(angledHeaders ? { paddingRight: diagonalOverhang } : undefined),\n ...(maxHeight !== undefined ? { maxHeight } : undefined),\n }}\n >\n <table className={cn(\"w-max border-collapse\", compact ? \"text-xs\" : \"text-sm\")}>\n {angledHeaders && (\n <colgroup>\n <col />\n {columns.map((_, index) => (\n <col key={index} style={{ width: columnWidth }} />\n ))}\n </colgroup>\n )}\n <thead>\n <tr className={cn(\"bg-muted/40\", !angledHeaders && \"border-b border-border\", headerClassName)}>\n <th\n scope=\"col\"\n className={cn(\n \"sticky left-0 top-0 z-30 min-w-44 border-r border-border bg-muted text-left font-medium\",\n headerCellPadding,\n angledHeaders ? \"align-bottom\" : cn(\"border-b border-border\", columnClassName),\n headerClassName,\n )}\n >\n {corner}\n </th>\n {columns.map((column, index) =>\n angledHeaders ? (\n <th\n key={index}\n scope=\"col\"\n title={columnTitle(column)}\n className={cn(\n \"sticky top-0 z-20 overflow-visible border-b border-border bg-muted p-0 align-bottom\",\n columnClassName,\n headerClassName,\n )}\n style={{\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n height: headerHeight,\n }}\n >\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: columnWidth,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n {index === 0 && (\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: 0,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n )}\n <div\n className=\"absolute bottom-2 overflow-hidden text-ellipsis whitespace-nowrap pl-1 text-left text-xs font-medium leading-none text-muted-foreground\"\n style={{\n left: columnWidth / 2,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n >\n {column}\n </div>\n </th>\n ) : (\n <th\n key={index}\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-20 min-w-24 whitespace-nowrap border-b border-border bg-muted text-center text-xs font-medium text-muted-foreground\",\n headerCellPadding,\n columnClassName,\n headerClassName,\n )}\n >\n {column}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr key={row.key} className=\"border-b border-border last:border-b-0\">\n <th\n scope=\"row\"\n className={cn(\n \"sticky left-0 z-10 border-r border-border bg-background text-left font-medium\",\n rowLabelPadding,\n rowLabelClassName,\n )}\n >\n {row.label}\n </th>\n {columns.map((_, index) => (\n <td\n key={index}\n className={cn(\n angledHeaders\n ? \"p-0 text-center align-middle\"\n : cn(bodyCellPadding, \"text-center align-middle\"),\n cellClassName,\n )}\n style={\n angledHeaders\n ? {\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n }\n : undefined\n }\n >\n {row.cells[index] ?? null}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction columnTitle(column: ReactNode) {\n if (typeof column === \"string\" || typeof column === \"number\") {\n return String(column);\n }\n return undefined;\n}\n"],"names":["jsx","cn","jsxs"],"mappings":"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"}
@@ -13,10 +13,12 @@ export type MatrixTableProps = {
13
13
  density?: "default" | "compact";
14
14
  columnWidth?: number;
15
15
  headerHeight?: number;
16
+ maxHeight?: number | string;
16
17
  className?: string;
17
18
  columnClassName?: string;
18
19
  rowLabelClassName?: string;
19
20
  cellClassName?: string;
21
+ headerClassName?: string;
20
22
  };
21
- export declare function MatrixTable({ columns, rows, corner, emptyMessage, angledHeaders, density, columnWidth, headerHeight, className, columnClassName, rowLabelClassName, cellClassName, }: MatrixTableProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function MatrixTable({ columns, rows, corner, emptyMessage, angledHeaders, density, columnWidth, headerHeight, maxHeight, className, columnClassName, rowLabelClassName, cellClassName, headerClassName, }: MatrixTableProps): import("react/jsx-runtime").JSX.Element;
22
24
  //# sourceMappingURL=MatrixTable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MatrixTable.d.ts","sourceRoot":"","sources":["../../src/data/MatrixTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,IAAI,EACJ,MAAM,EACN,YAAwB,EACxB,aAAqB,EACrB,OAAmB,EACnB,WAAgB,EAChB,YAAkB,EAClB,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,aAAa,GACd,EAAE,gBAAgB,2CAgJlB"}
1
+ {"version":3,"file":"MatrixTable.d.ts","sourceRoot":"","sources":["../../src/data/MatrixTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IAIvB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,IAAI,EACJ,MAAM,EACN,YAAwB,EACxB,aAAqB,EACrB,OAAmB,EACnB,WAAgB,EAChB,YAAkB,EAClB,SAAS,EACT,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,eAAe,GAChB,EAAE,gBAAgB,2CAsJlB"}
@@ -9,10 +9,12 @@ function MatrixTable({
9
9
  density = "default",
10
10
  columnWidth = 48,
11
11
  headerHeight = 120,
12
+ maxHeight,
12
13
  className,
13
14
  columnClassName,
14
15
  rowLabelClassName,
15
- cellClassName
16
+ cellClassName,
17
+ headerClassName
16
18
  }) {
17
19
  if (rows.length === 0 || columns.length === 0) {
18
20
  return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: emptyMessage });
@@ -27,21 +29,25 @@ function MatrixTable({
27
29
  "div",
28
30
  {
29
31
  className: cn("overflow-auto rounded-md border border-border", className),
30
- style: angledHeaders ? { paddingRight: diagonalOverhang } : void 0,
32
+ style: {
33
+ ...angledHeaders ? { paddingRight: diagonalOverhang } : void 0,
34
+ ...maxHeight !== void 0 ? { maxHeight } : void 0
35
+ },
31
36
  children: /* @__PURE__ */ jsxs("table", { className: cn("w-max border-collapse", compact ? "text-xs" : "text-sm"), children: [
32
37
  angledHeaders && /* @__PURE__ */ jsxs("colgroup", { children: [
33
38
  /* @__PURE__ */ jsx("col", {}),
34
39
  columns.map((_, index) => /* @__PURE__ */ jsx("col", { style: { width: columnWidth } }, index))
35
40
  ] }),
36
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: cn("bg-muted/40", !angledHeaders && "border-b border-border"), children: [
41
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: cn("bg-muted/40", !angledHeaders && "border-b border-border", headerClassName), children: [
37
42
  /* @__PURE__ */ jsx(
38
43
  "th",
39
44
  {
40
45
  scope: "col",
41
46
  className: cn(
42
- "sticky left-0 z-10 min-w-44 border-r border-border bg-muted/40 text-left font-medium",
47
+ "sticky left-0 top-0 z-30 min-w-44 border-r border-border bg-muted text-left font-medium",
43
48
  headerCellPadding,
44
- angledHeaders ? "align-bottom" : columnClassName
49
+ angledHeaders ? "align-bottom" : cn("border-b border-border", columnClassName),
50
+ headerClassName
45
51
  ),
46
52
  children: corner
47
53
  }
@@ -53,8 +59,9 @@ function MatrixTable({
53
59
  scope: "col",
54
60
  title: columnTitle(column),
55
61
  className: cn(
56
- "relative overflow-visible border-b border-border p-0 align-bottom",
57
- columnClassName
62
+ "sticky top-0 z-20 overflow-visible border-b border-border bg-muted p-0 align-bottom",
63
+ columnClassName,
64
+ headerClassName
58
65
  ),
59
66
  style: {
60
67
  width: columnWidth,
@@ -108,9 +115,10 @@ function MatrixTable({
108
115
  {
109
116
  scope: "col",
110
117
  className: cn(
111
- "min-w-24 whitespace-nowrap text-center text-xs font-medium text-muted-foreground",
118
+ "sticky top-0 z-20 min-w-24 whitespace-nowrap border-b border-border bg-muted text-center text-xs font-medium text-muted-foreground",
112
119
  headerCellPadding,
113
- columnClassName
120
+ columnClassName,
121
+ headerClassName
114
122
  ),
115
123
  children: column
116
124
  },
@@ -1 +1 @@
1
- {"version":3,"file":"MatrixTable.js","sources":["../../src/data/MatrixTable.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\n\nexport type MatrixTableRow = {\n key: string;\n label: ReactNode;\n cells: ReactNode[];\n};\n\nexport type MatrixTableProps = {\n columns: ReactNode[];\n rows: MatrixTableRow[];\n corner?: ReactNode;\n emptyMessage?: ReactNode;\n angledHeaders?: boolean;\n density?: \"default\" | \"compact\";\n columnWidth?: number;\n headerHeight?: number;\n className?: string;\n columnClassName?: string;\n rowLabelClassName?: string;\n cellClassName?: string;\n};\n\nexport function MatrixTable({\n columns,\n rows,\n corner,\n emptyMessage = \"No data\",\n angledHeaders = false,\n density = \"default\",\n columnWidth = 48,\n headerHeight = 120,\n className,\n columnClassName,\n rowLabelClassName,\n cellClassName,\n}: MatrixTableProps) {\n if (rows.length === 0 || columns.length === 0) {\n return <div className=\"text-sm text-muted-foreground\">{emptyMessage}</div>;\n }\n\n const textWidth = Math.round(headerHeight * 1.41);\n const diagonalOverhang = Math.round(headerHeight * 0.71);\n const compact = density === \"compact\";\n const headerCellPadding = compact ? \"px-density-2 py-density-1\" : \"px-density-3 py-density-2\";\n const rowLabelPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n const bodyCellPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n\n return (\n <div\n className={cn(\"overflow-auto rounded-md border border-border\", className)}\n style={angledHeaders ? { paddingRight: diagonalOverhang } : undefined}\n >\n <table className={cn(\"w-max border-collapse\", compact ? \"text-xs\" : \"text-sm\")}>\n {angledHeaders && (\n <colgroup>\n <col />\n {columns.map((_, index) => (\n <col key={index} style={{ width: columnWidth }} />\n ))}\n </colgroup>\n )}\n <thead>\n <tr className={cn(\"bg-muted/40\", !angledHeaders && \"border-b border-border\")}>\n <th\n scope=\"col\"\n className={cn(\n \"sticky left-0 z-10 min-w-44 border-r border-border bg-muted/40 text-left font-medium\",\n headerCellPadding,\n angledHeaders ? \"align-bottom\" : columnClassName,\n )}\n >\n {corner}\n </th>\n {columns.map((column, index) =>\n angledHeaders ? (\n <th\n key={index}\n scope=\"col\"\n title={columnTitle(column)}\n className={cn(\n \"relative overflow-visible border-b border-border p-0 align-bottom\",\n columnClassName,\n )}\n style={{\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n height: headerHeight,\n }}\n >\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: columnWidth,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n {index === 0 && (\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: 0,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n )}\n <div\n className=\"absolute bottom-2 overflow-hidden text-ellipsis whitespace-nowrap pl-1 text-left text-xs font-medium leading-none text-muted-foreground\"\n style={{\n left: columnWidth / 2,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n >\n {column}\n </div>\n </th>\n ) : (\n <th\n key={index}\n scope=\"col\"\n className={cn(\n \"min-w-24 whitespace-nowrap text-center text-xs font-medium text-muted-foreground\",\n headerCellPadding,\n columnClassName,\n )}\n >\n {column}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr key={row.key} className=\"border-b border-border last:border-b-0\">\n <th\n scope=\"row\"\n className={cn(\n \"sticky left-0 z-10 border-r border-border bg-background text-left font-medium\",\n rowLabelPadding,\n rowLabelClassName,\n )}\n >\n {row.label}\n </th>\n {columns.map((_, index) => (\n <td\n key={index}\n className={cn(\n angledHeaders\n ? \"p-0 text-center align-middle\"\n : cn(bodyCellPadding, \"text-center align-middle\"),\n cellClassName,\n )}\n style={\n angledHeaders\n ? {\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n }\n : undefined\n }\n >\n {row.cells[index] ?? null}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction columnTitle(column: ReactNode) {\n if (typeof column === \"string\" || typeof column === \"number\") {\n return String(column);\n }\n return undefined;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MatrixTable.js","sources":["../../src/data/MatrixTable.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\n\nexport type MatrixTableRow = {\n key: string;\n label: ReactNode;\n cells: ReactNode[];\n};\n\nexport type MatrixTableProps = {\n columns: ReactNode[];\n rows: MatrixTableRow[];\n corner?: ReactNode;\n emptyMessage?: ReactNode;\n angledHeaders?: boolean;\n density?: \"default\" | \"compact\";\n columnWidth?: number;\n headerHeight?: number;\n // maxHeight caps the scroll viewport so the body scrolls under frozen\n // headers (Excel-style freeze panes). Accepts any CSS length; number => px.\n maxHeight?: number | string;\n className?: string;\n columnClassName?: string;\n rowLabelClassName?: string;\n cellClassName?: string;\n // headerClassName is appended to the header row and every header cell (corner\n // + columns). Use it to override the default gray header background, e.g.\n // \"bg-transparent\", since tailwind-merge lets the later class win.\n headerClassName?: string;\n};\n\nexport function MatrixTable({\n columns,\n rows,\n corner,\n emptyMessage = \"No data\",\n angledHeaders = false,\n density = \"default\",\n columnWidth = 48,\n headerHeight = 120,\n maxHeight,\n className,\n columnClassName,\n rowLabelClassName,\n cellClassName,\n headerClassName,\n}: MatrixTableProps) {\n if (rows.length === 0 || columns.length === 0) {\n return <div className=\"text-sm text-muted-foreground\">{emptyMessage}</div>;\n }\n\n const textWidth = Math.round(headerHeight * 1.41);\n const diagonalOverhang = Math.round(headerHeight * 0.71);\n const compact = density === \"compact\";\n const headerCellPadding = compact ? \"px-density-2 py-density-1\" : \"px-density-3 py-density-2\";\n const rowLabelPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n const bodyCellPadding = compact ? \"px-density-2 py-1\" : \"px-density-3 py-density-2\";\n\n return (\n <div\n className={cn(\"overflow-auto rounded-md border border-border\", className)}\n style={{\n ...(angledHeaders ? { paddingRight: diagonalOverhang } : undefined),\n ...(maxHeight !== undefined ? { maxHeight } : undefined),\n }}\n >\n <table className={cn(\"w-max border-collapse\", compact ? \"text-xs\" : \"text-sm\")}>\n {angledHeaders && (\n <colgroup>\n <col />\n {columns.map((_, index) => (\n <col key={index} style={{ width: columnWidth }} />\n ))}\n </colgroup>\n )}\n <thead>\n <tr className={cn(\"bg-muted/40\", !angledHeaders && \"border-b border-border\", headerClassName)}>\n <th\n scope=\"col\"\n className={cn(\n \"sticky left-0 top-0 z-30 min-w-44 border-r border-border bg-muted text-left font-medium\",\n headerCellPadding,\n angledHeaders ? \"align-bottom\" : cn(\"border-b border-border\", columnClassName),\n headerClassName,\n )}\n >\n {corner}\n </th>\n {columns.map((column, index) =>\n angledHeaders ? (\n <th\n key={index}\n scope=\"col\"\n title={columnTitle(column)}\n className={cn(\n \"sticky top-0 z-20 overflow-visible border-b border-border bg-muted p-0 align-bottom\",\n columnClassName,\n headerClassName,\n )}\n style={{\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n height: headerHeight,\n }}\n >\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: columnWidth,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n {index === 0 && (\n <div\n className=\"absolute bottom-0 border-b border-border\"\n style={{\n left: 0,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n />\n )}\n <div\n className=\"absolute bottom-2 overflow-hidden text-ellipsis whitespace-nowrap pl-1 text-left text-xs font-medium leading-none text-muted-foreground\"\n style={{\n left: columnWidth / 2,\n width: textWidth,\n transform: \"rotate(-45deg)\",\n transformOrigin: \"0 100%\",\n }}\n >\n {column}\n </div>\n </th>\n ) : (\n <th\n key={index}\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-20 min-w-24 whitespace-nowrap border-b border-border bg-muted text-center text-xs font-medium text-muted-foreground\",\n headerCellPadding,\n columnClassName,\n headerClassName,\n )}\n >\n {column}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr key={row.key} className=\"border-b border-border last:border-b-0\">\n <th\n scope=\"row\"\n className={cn(\n \"sticky left-0 z-10 border-r border-border bg-background text-left font-medium\",\n rowLabelPadding,\n rowLabelClassName,\n )}\n >\n {row.label}\n </th>\n {columns.map((_, index) => (\n <td\n key={index}\n className={cn(\n angledHeaders\n ? \"p-0 text-center align-middle\"\n : cn(bodyCellPadding, \"text-center align-middle\"),\n cellClassName,\n )}\n style={\n angledHeaders\n ? {\n width: columnWidth,\n minWidth: columnWidth,\n maxWidth: columnWidth,\n }\n : undefined\n }\n >\n {row.cells[index] ?? null}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\nfunction columnTitle(column: ReactNode) {\n if (typeof column === \"string\" || typeof column === \"number\") {\n return String(column);\n }\n return undefined;\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":"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"}
@@ -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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"}