@flanksource/clicky-ui 0.2.13 → 0.2.14
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/components/SecretKeySelector.cjs +1 -1
- package/dist/components/SecretKeySelector.cjs.map +1 -1
- package/dist/components/SecretKeySelector.d.ts +5 -1
- package/dist/components/SecretKeySelector.d.ts.map +1 -1
- package/dist/components/SecretKeySelector.js +1 -1
- package/dist/components/SecretKeySelector.js.map +1 -1
- package/dist/data/TimeseriesCoreBars.cjs +8 -1
- package/dist/data/TimeseriesCoreBars.cjs.map +1 -1
- package/dist/data/TimeseriesCoreBars.d.ts.map +1 -1
- package/dist/data/TimeseriesCoreBars.js +8 -1
- package/dist/data/TimeseriesCoreBars.js.map +1 -1
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecretKeySelector.cjs","sources":["../../src/components/SecretKeySelector.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { K8SSecret, K8SConfigmap } from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { UiEdit } from \"../icons\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// SecretKeySelector picks a Secret or ConfigMap and one of its keys, with a\n// mid-masked preview of every key's value so the operator can tell which key\n// holds the host vs the db vs a password. With `allowLiteral` it also offers a\n// \"Value\" mode for typing a static inline string. It is presentational: it\n// fetches nothing — the consumer supplies async `loadResources` /\n// `loadKeyPreview` getters. It emits a discriminated value the consumer lowers\n// into whatever reference shape it needs.\n\nexport type SecretKind = \"secret\" | \"configmap\";\n\n/** Toggle options: the two resource kinds plus the inline-literal mode. */\nexport type SecretValueSource = SecretKind | \"value\";\n\n/** One key's mid-masked preview. `value` is already masked by the consumer. */\nexport type KeyPreview = { key: string; value: string };\n\n/**\n * The selector's value: either a {kind,name,key} reference into a Secret /\n * ConfigMap, or a static inline literal ({kind:\"value\", value}).\n */\nexport type SecretKeyValue =\n | { kind: SecretKind; name: string; key: string }\n | { kind: \"value\"; value: string };\n\n/** A named secret/configmap and its data key names (values never returned). */\nexport type SecretResource = { name: string; keys?: string[] };\n\nexport type SecretKeySelectorProps = {\n value: SecretKeyValue | undefined;\n onChange: (next: SecretKeyValue | undefined) => void;\n /** Loads the named kind's resources (name + data key names). */\n loadResources: (kind: SecretKind) => Promise<SecretResource[]>;\n /** Loads mid-masked previews for the named resource's keys. */\n loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;\n /** Offer a third \"Value\" toggle for typing a static inline literal. */\n allowLiteral?: boolean;\n /**\n * When true, a chosen secret/configmap name absent from the loaded resources,\n * or a key absent from the chosen resource's keys, is flagged invalid (once\n * the respective load settles). The reference doesn't exist in the namespace.\n * The literal \"Value\" mode is never strict-flagged.\n */\n strict?: boolean;\n className?: string;\n};\n\nexport function SecretKeySelector({\n value,\n onChange,\n loadResources,\n loadKeyPreview,\n allowLiteral = false,\n strict = false,\n className,\n}: SecretKeySelectorProps) {\n const source: SecretValueSource = value?.kind ?? \"secret\";\n const isLiteral = source === \"value\";\n // A literal value carries no resource kind; the loaders default to \"secret\".\n const refKind: SecretKind = isLiteral ? \"secret\" : source;\n const selectedName = value && value.kind !== \"value\" ? value.name : \"\";\n const selectedKey = value && value.kind !== \"value\" ? value.key : \"\";\n const literalValue = value && value.kind === \"value\" ? value.value : \"\";\n\n const [resources, setResources] = useState<SecretResource[]>([]);\n const [resourcesLoading, setResourcesLoading] = useState(false);\n const [previews, setPreviews] = useState<KeyPreview[]>([]);\n const [previewLoading, setPreviewLoading] = useState(false);\n\n useEffect(() => {\n // Literal mode references no resource, so skip the list fetch entirely.\n if (isLiteral) return;\n let cancelled = false;\n setResourcesLoading(true);\n loadResources(refKind)\n .then((res) => !cancelled && setResources(res))\n .finally(() => !cancelled && setResourcesLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadResources, refKind, isLiteral]);\n\n useEffect(() => {\n if (!selectedName) {\n setPreviews([]);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n loadKeyPreview(refKind, selectedName)\n .then((res) => !cancelled && setPreviews(res))\n .catch(() => !cancelled && setPreviews([]))\n .finally(() => !cancelled && setPreviewLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadKeyPreview, refKind, selectedName]);\n\n const nameOptions = useMemo<ComboboxOption[]>(\n () => resources.map((r) => ({ value: r.name, label: r.name })),\n [resources],\n );\n const selectedResource = useMemo(\n () => resources.find((r) => r.name === selectedName),\n [resources, selectedName],\n );\n const keyOptions = useMemo(\n () => buildKeyOptions(selectedResource?.keys ?? [], previews, selectedKey),\n [selectedResource, selectedKey, previews],\n );\n\n // Strict validity: the name is invalid when it names no loaded resource; the\n // key is invalid when, with a resolved resource, it isn't one of that\n // resource's keys. The key list comes from the resource list (not previews),\n // so both are gated on resourcesLoading only.\n const nameInvalid = strict && !!selectedName && !resourcesLoading && !selectedResource;\n const keyInvalid =\n strict &&\n !!selectedKey &&\n !!selectedResource &&\n !resourcesLoading &&\n !(selectedResource.keys ?? []).includes(selectedKey);\n\n // Switching source resets the selection: a resource kind clears name+key; the\n // literal mode seeds an empty string (so onChange emits a value-kind value).\n const setSource = (next: SecretValueSource) =>\n onChange(next === \"value\" ? { kind: \"value\", value: \"\" } : { kind: next, name: \"\", key: \"\" });\n const setName = (name: string) =>\n onChange(name ? { kind: refKind, name, key: selectedKey } : undefined);\n const setKey = (key: string) =>\n selectedName ? onChange({ kind: refKind, name: selectedName, key }) : undefined;\n const setLiteral = (v: string) => onChange({ kind: \"value\", value: v });\n\n const sources: SecretValueSource[] = allowLiteral\n ? [\"secret\", \"configmap\", \"value\"]\n : [\"secret\", \"configmap\"];\n\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <div className=\"flex shrink-0 overflow-hidden rounded border border-border text-xs\">\n {sources.map((k) => {\n const Glyph = SOURCE_ICON[k];\n return (\n <button\n key={k}\n type=\"button\"\n onClick={() => setSource(k)}\n className={cn(\n \"flex items-center gap-1 whitespace-nowrap px-2 py-1\",\n source === k\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted text-muted-foreground hover:bg-accent\",\n )}\n >\n <Glyph className=\"h-3.5 w-3.5\" />\n {SOURCE_LABEL[k]}\n </button>\n );\n })}\n </div>\n {isLiteral ? (\n <input\n type=\"text\"\n value={literalValue}\n onChange={(e) => setLiteral(e.target.value)}\n placeholder=\"Static value…\"\n className={cn(\n \"h-control-h min-w-0 flex-1 rounded border border-input bg-background px-control-px text-sm\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n />\n ) : (\n <>\n <div className=\"w-44 shrink-0\">\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder={`Select ${refKind}…`}\n />\n </div>\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={keyOptions}\n value={selectedKey}\n onChange={setKey}\n allowCustomValue\n loading={previewLoading}\n invalid={keyInvalid}\n placeholder={selectedName ? \"Key…\" : \"—\"}\n />\n </div>\n </>\n )}\n </div>\n );\n}\n\n// Icons share a className-only call shape; the flanksource FCs carry extra\n// static metadata that doesn't unify with ComponentType, so type the slot as a\n// plain render function instead.\nconst SOURCE_ICON: Record<SecretValueSource, (p: { className?: string }) => ReactNode> = {\n secret: K8SSecret,\n configmap: K8SConfigmap,\n value: UiEdit,\n};\n\nconst SOURCE_LABEL: Record<SecretValueSource, string> = {\n secret: \"Secret\",\n configmap: \"ConfigMap\",\n value: \"Value\",\n};\n\n// buildKeyOptions labels each key with its mid-masked preview value when one is\n// available. A selected key absent from the list is pinned so the selection\n// stays visible.\nexport function buildKeyOptions(\n keys: string[],\n previews: KeyPreview[],\n selectedKey: string,\n): ComboboxOption[] {\n const masked = new Map(previews.map((p) => [p.key, p.value]));\n const base = keys.map((k) => ({\n value: k,\n label: masked.has(k) ? `${k} — ${masked.get(k)}` : k,\n }));\n if (selectedKey && !base.some((o) => o.value === selectedKey)) {\n return [{ value: selectedKey, label: selectedKey }, ...base];\n }\n return base;\n}\n"],"names":["useState","useEffect","useMemo","cn","jsx","jsxs","Fragment","Combobox","K8SSecret","K8SConfigmap","UiEdit"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SecretKeySelector.cjs","sources":["../../src/components/SecretKeySelector.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { K8SSecret, K8SConfigmap } from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { UiEdit } from \"../icons\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// SecretKeySelector picks a Secret or ConfigMap and one of its keys, with a\n// mid-masked preview of every key's value so the operator can tell which key\n// holds the host vs the db vs a password. It also offers a \"Value\" mode (on by\n// default; opt out with `allowLiteral={false}`) for typing a static inline\n// string. It is presentational: it fetches nothing — the consumer supplies\n// async `loadResources` / `loadKeyPreview` getters. It emits a discriminated\n// value the consumer lowers into whatever reference shape it needs.\n\nexport type SecretKind = \"secret\" | \"configmap\";\n\n/** Toggle options: the two resource kinds plus the inline-literal mode. */\nexport type SecretValueSource = SecretKind | \"value\";\n\n/** One key's mid-masked preview. `value` is already masked by the consumer. */\nexport type KeyPreview = { key: string; value: string };\n\n/**\n * The selector's value: either a {kind,name,key} reference into a Secret /\n * ConfigMap, or a static inline literal ({kind:\"value\", value}).\n */\nexport type SecretKeyValue =\n | { kind: SecretKind; name: string; key: string }\n | { kind: \"value\"; value: string };\n\n/** A named secret/configmap and its data key names (values never returned). */\nexport type SecretResource = { name: string; keys?: string[] };\n\nexport type SecretKeySelectorProps = {\n value: SecretKeyValue | undefined;\n onChange: (next: SecretKeyValue | undefined) => void;\n /** Loads the named kind's resources (name + data key names). */\n loadResources: (kind: SecretKind) => Promise<SecretResource[]>;\n /** Loads mid-masked previews for the named resource's keys. */\n loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;\n /**\n * Offer a third \"Value\" toggle for typing a static inline literal. Enabled by\n * default; pass `false` to restrict the selector to Secret/ConfigMap\n * references only.\n */\n allowLiteral?: boolean;\n /**\n * When true, a chosen secret/configmap name absent from the loaded resources,\n * or a key absent from the chosen resource's keys, is flagged invalid (once\n * the respective load settles). The reference doesn't exist in the namespace.\n * The literal \"Value\" mode is never strict-flagged.\n */\n strict?: boolean;\n className?: string;\n};\n\nexport function SecretKeySelector({\n value,\n onChange,\n loadResources,\n loadKeyPreview,\n allowLiteral = true,\n strict = false,\n className,\n}: SecretKeySelectorProps) {\n const source: SecretValueSource = value?.kind ?? \"secret\";\n const isLiteral = source === \"value\";\n // A literal value carries no resource kind; the loaders default to \"secret\".\n const refKind: SecretKind = isLiteral ? \"secret\" : source;\n const selectedName = value && value.kind !== \"value\" ? value.name : \"\";\n const selectedKey = value && value.kind !== \"value\" ? value.key : \"\";\n const literalValue = value && value.kind === \"value\" ? value.value : \"\";\n\n const [resources, setResources] = useState<SecretResource[]>([]);\n const [resourcesLoading, setResourcesLoading] = useState(false);\n const [previews, setPreviews] = useState<KeyPreview[]>([]);\n const [previewLoading, setPreviewLoading] = useState(false);\n\n useEffect(() => {\n // Literal mode references no resource, so skip the list fetch entirely.\n if (isLiteral) return;\n let cancelled = false;\n setResourcesLoading(true);\n loadResources(refKind)\n .then((res) => !cancelled && setResources(res))\n .finally(() => !cancelled && setResourcesLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadResources, refKind, isLiteral]);\n\n useEffect(() => {\n if (!selectedName) {\n setPreviews([]);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n loadKeyPreview(refKind, selectedName)\n .then((res) => !cancelled && setPreviews(res))\n .catch(() => !cancelled && setPreviews([]))\n .finally(() => !cancelled && setPreviewLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadKeyPreview, refKind, selectedName]);\n\n const nameOptions = useMemo<ComboboxOption[]>(\n () => resources.map((r) => ({ value: r.name, label: r.name })),\n [resources],\n );\n const selectedResource = useMemo(\n () => resources.find((r) => r.name === selectedName),\n [resources, selectedName],\n );\n const keyOptions = useMemo(\n () => buildKeyOptions(selectedResource?.keys ?? [], previews, selectedKey),\n [selectedResource, selectedKey, previews],\n );\n\n // Strict validity: the name is invalid when it names no loaded resource; the\n // key is invalid when, with a resolved resource, it isn't one of that\n // resource's keys. The key list comes from the resource list (not previews),\n // so both are gated on resourcesLoading only.\n const nameInvalid = strict && !!selectedName && !resourcesLoading && !selectedResource;\n const keyInvalid =\n strict &&\n !!selectedKey &&\n !!selectedResource &&\n !resourcesLoading &&\n !(selectedResource.keys ?? []).includes(selectedKey);\n\n // Switching source resets the selection: a resource kind clears name+key; the\n // literal mode seeds an empty string (so onChange emits a value-kind value).\n const setSource = (next: SecretValueSource) =>\n onChange(next === \"value\" ? { kind: \"value\", value: \"\" } : { kind: next, name: \"\", key: \"\" });\n const setName = (name: string) =>\n onChange(name ? { kind: refKind, name, key: selectedKey } : undefined);\n const setKey = (key: string) =>\n selectedName ? onChange({ kind: refKind, name: selectedName, key }) : undefined;\n const setLiteral = (v: string) => onChange({ kind: \"value\", value: v });\n\n const sources: SecretValueSource[] = allowLiteral\n ? [\"secret\", \"configmap\", \"value\"]\n : [\"secret\", \"configmap\"];\n\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <div className=\"flex shrink-0 overflow-hidden rounded border border-border text-xs\">\n {sources.map((k) => {\n const Glyph = SOURCE_ICON[k];\n return (\n <button\n key={k}\n type=\"button\"\n onClick={() => setSource(k)}\n className={cn(\n \"flex items-center gap-1 whitespace-nowrap px-2 py-1\",\n source === k\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted text-muted-foreground hover:bg-accent\",\n )}\n >\n <Glyph className=\"h-3.5 w-3.5\" />\n {SOURCE_LABEL[k]}\n </button>\n );\n })}\n </div>\n {isLiteral ? (\n <input\n type=\"text\"\n value={literalValue}\n onChange={(e) => setLiteral(e.target.value)}\n placeholder=\"Static value…\"\n className={cn(\n \"h-control-h min-w-0 flex-1 rounded border border-input bg-background px-control-px text-sm\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n />\n ) : (\n <>\n <div className=\"w-44 shrink-0\">\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder={`Select ${refKind}…`}\n />\n </div>\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={keyOptions}\n value={selectedKey}\n onChange={setKey}\n allowCustomValue\n loading={previewLoading}\n invalid={keyInvalid}\n placeholder={selectedName ? \"Key…\" : \"—\"}\n />\n </div>\n </>\n )}\n </div>\n );\n}\n\n// Icons share a className-only call shape; the flanksource FCs carry extra\n// static metadata that doesn't unify with ComponentType, so type the slot as a\n// plain render function instead.\nconst SOURCE_ICON: Record<SecretValueSource, (p: { className?: string }) => ReactNode> = {\n secret: K8SSecret,\n configmap: K8SConfigmap,\n value: UiEdit,\n};\n\nconst SOURCE_LABEL: Record<SecretValueSource, string> = {\n secret: \"Secret\",\n configmap: \"ConfigMap\",\n value: \"Value\",\n};\n\n// buildKeyOptions labels each key with its mid-masked preview value when one is\n// available. A selected key absent from the list is pinned so the selection\n// stays visible.\nexport function buildKeyOptions(\n keys: string[],\n previews: KeyPreview[],\n selectedKey: string,\n): ComboboxOption[] {\n const masked = new Map(previews.map((p) => [p.key, p.value]));\n const base = keys.map((k) => ({\n value: k,\n label: masked.has(k) ? `${k} — ${masked.get(k)}` : k,\n }));\n if (selectedKey && !base.some((o) => o.value === selectedKey)) {\n return [{ value: selectedKey, label: selectedKey }, ...base];\n }\n return base;\n}\n"],"names":["useState","useEffect","useMemo","cn","jsx","jsxs","Fragment","Combobox","K8SSecret","K8SConfigmap","UiEdit"],"mappings":"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"}
|
|
@@ -31,7 +31,11 @@ export type SecretKeySelectorProps = {
|
|
|
31
31
|
loadResources: (kind: SecretKind) => Promise<SecretResource[]>;
|
|
32
32
|
/** Loads mid-masked previews for the named resource's keys. */
|
|
33
33
|
loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Offer a third "Value" toggle for typing a static inline literal. Enabled by
|
|
36
|
+
* default; pass `false` to restrict the selector to Secret/ConfigMap
|
|
37
|
+
* references only.
|
|
38
|
+
*/
|
|
35
39
|
allowLiteral?: boolean;
|
|
36
40
|
/**
|
|
37
41
|
* When true, a chosen secret/configmap name absent from the loaded resources,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecretKeySelector.d.ts","sourceRoot":"","sources":["../../src/components/SecretKeySelector.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAU3D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEhD,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,OAAO,CAAC;AAErD,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE/D,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,QAAQ,EAAE,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;IACrD,gEAAgE;IAChE,aAAa,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC/D,+DAA+D;IAC/D,cAAc,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC1E
|
|
1
|
+
{"version":3,"file":"SecretKeySelector.d.ts","sourceRoot":"","sources":["../../src/components/SecretKeySelector.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAU3D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEhD,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,OAAO,CAAC;AAErD,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE/D,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,QAAQ,EAAE,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;IACrD,gEAAgE;IAChE,aAAa,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC/D,+DAA+D;IAC/D,cAAc,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC1E;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,aAAa,EACb,cAAc,EACd,YAAmB,EACnB,MAAc,EACd,SAAS,GACV,EAAE,sBAAsB,2CAgJxB;AAoBD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EAAE,EACd,QAAQ,EAAE,UAAU,EAAE,EACtB,WAAW,EAAE,MAAM,GAClB,cAAc,EAAE,CAUlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecretKeySelector.js","sources":["../../src/components/SecretKeySelector.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { K8SSecret, K8SConfigmap } from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { UiEdit } from \"../icons\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// SecretKeySelector picks a Secret or ConfigMap and one of its keys, with a\n// mid-masked preview of every key's value so the operator can tell which key\n// holds the host vs the db vs a password. With `allowLiteral` it also offers a\n// \"Value\" mode for typing a static inline string. It is presentational: it\n// fetches nothing — the consumer supplies async `loadResources` /\n// `loadKeyPreview` getters. It emits a discriminated value the consumer lowers\n// into whatever reference shape it needs.\n\nexport type SecretKind = \"secret\" | \"configmap\";\n\n/** Toggle options: the two resource kinds plus the inline-literal mode. */\nexport type SecretValueSource = SecretKind | \"value\";\n\n/** One key's mid-masked preview. `value` is already masked by the consumer. */\nexport type KeyPreview = { key: string; value: string };\n\n/**\n * The selector's value: either a {kind,name,key} reference into a Secret /\n * ConfigMap, or a static inline literal ({kind:\"value\", value}).\n */\nexport type SecretKeyValue =\n | { kind: SecretKind; name: string; key: string }\n | { kind: \"value\"; value: string };\n\n/** A named secret/configmap and its data key names (values never returned). */\nexport type SecretResource = { name: string; keys?: string[] };\n\nexport type SecretKeySelectorProps = {\n value: SecretKeyValue | undefined;\n onChange: (next: SecretKeyValue | undefined) => void;\n /** Loads the named kind's resources (name + data key names). */\n loadResources: (kind: SecretKind) => Promise<SecretResource[]>;\n /** Loads mid-masked previews for the named resource's keys. */\n loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;\n /** Offer a third \"Value\" toggle for typing a static inline literal. */\n allowLiteral?: boolean;\n /**\n * When true, a chosen secret/configmap name absent from the loaded resources,\n * or a key absent from the chosen resource's keys, is flagged invalid (once\n * the respective load settles). The reference doesn't exist in the namespace.\n * The literal \"Value\" mode is never strict-flagged.\n */\n strict?: boolean;\n className?: string;\n};\n\nexport function SecretKeySelector({\n value,\n onChange,\n loadResources,\n loadKeyPreview,\n allowLiteral = false,\n strict = false,\n className,\n}: SecretKeySelectorProps) {\n const source: SecretValueSource = value?.kind ?? \"secret\";\n const isLiteral = source === \"value\";\n // A literal value carries no resource kind; the loaders default to \"secret\".\n const refKind: SecretKind = isLiteral ? \"secret\" : source;\n const selectedName = value && value.kind !== \"value\" ? value.name : \"\";\n const selectedKey = value && value.kind !== \"value\" ? value.key : \"\";\n const literalValue = value && value.kind === \"value\" ? value.value : \"\";\n\n const [resources, setResources] = useState<SecretResource[]>([]);\n const [resourcesLoading, setResourcesLoading] = useState(false);\n const [previews, setPreviews] = useState<KeyPreview[]>([]);\n const [previewLoading, setPreviewLoading] = useState(false);\n\n useEffect(() => {\n // Literal mode references no resource, so skip the list fetch entirely.\n if (isLiteral) return;\n let cancelled = false;\n setResourcesLoading(true);\n loadResources(refKind)\n .then((res) => !cancelled && setResources(res))\n .finally(() => !cancelled && setResourcesLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadResources, refKind, isLiteral]);\n\n useEffect(() => {\n if (!selectedName) {\n setPreviews([]);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n loadKeyPreview(refKind, selectedName)\n .then((res) => !cancelled && setPreviews(res))\n .catch(() => !cancelled && setPreviews([]))\n .finally(() => !cancelled && setPreviewLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadKeyPreview, refKind, selectedName]);\n\n const nameOptions = useMemo<ComboboxOption[]>(\n () => resources.map((r) => ({ value: r.name, label: r.name })),\n [resources],\n );\n const selectedResource = useMemo(\n () => resources.find((r) => r.name === selectedName),\n [resources, selectedName],\n );\n const keyOptions = useMemo(\n () => buildKeyOptions(selectedResource?.keys ?? [], previews, selectedKey),\n [selectedResource, selectedKey, previews],\n );\n\n // Strict validity: the name is invalid when it names no loaded resource; the\n // key is invalid when, with a resolved resource, it isn't one of that\n // resource's keys. The key list comes from the resource list (not previews),\n // so both are gated on resourcesLoading only.\n const nameInvalid = strict && !!selectedName && !resourcesLoading && !selectedResource;\n const keyInvalid =\n strict &&\n !!selectedKey &&\n !!selectedResource &&\n !resourcesLoading &&\n !(selectedResource.keys ?? []).includes(selectedKey);\n\n // Switching source resets the selection: a resource kind clears name+key; the\n // literal mode seeds an empty string (so onChange emits a value-kind value).\n const setSource = (next: SecretValueSource) =>\n onChange(next === \"value\" ? { kind: \"value\", value: \"\" } : { kind: next, name: \"\", key: \"\" });\n const setName = (name: string) =>\n onChange(name ? { kind: refKind, name, key: selectedKey } : undefined);\n const setKey = (key: string) =>\n selectedName ? onChange({ kind: refKind, name: selectedName, key }) : undefined;\n const setLiteral = (v: string) => onChange({ kind: \"value\", value: v });\n\n const sources: SecretValueSource[] = allowLiteral\n ? [\"secret\", \"configmap\", \"value\"]\n : [\"secret\", \"configmap\"];\n\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <div className=\"flex shrink-0 overflow-hidden rounded border border-border text-xs\">\n {sources.map((k) => {\n const Glyph = SOURCE_ICON[k];\n return (\n <button\n key={k}\n type=\"button\"\n onClick={() => setSource(k)}\n className={cn(\n \"flex items-center gap-1 whitespace-nowrap px-2 py-1\",\n source === k\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted text-muted-foreground hover:bg-accent\",\n )}\n >\n <Glyph className=\"h-3.5 w-3.5\" />\n {SOURCE_LABEL[k]}\n </button>\n );\n })}\n </div>\n {isLiteral ? (\n <input\n type=\"text\"\n value={literalValue}\n onChange={(e) => setLiteral(e.target.value)}\n placeholder=\"Static value…\"\n className={cn(\n \"h-control-h min-w-0 flex-1 rounded border border-input bg-background px-control-px text-sm\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n />\n ) : (\n <>\n <div className=\"w-44 shrink-0\">\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder={`Select ${refKind}…`}\n />\n </div>\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={keyOptions}\n value={selectedKey}\n onChange={setKey}\n allowCustomValue\n loading={previewLoading}\n invalid={keyInvalid}\n placeholder={selectedName ? \"Key…\" : \"—\"}\n />\n </div>\n </>\n )}\n </div>\n );\n}\n\n// Icons share a className-only call shape; the flanksource FCs carry extra\n// static metadata that doesn't unify with ComponentType, so type the slot as a\n// plain render function instead.\nconst SOURCE_ICON: Record<SecretValueSource, (p: { className?: string }) => ReactNode> = {\n secret: K8SSecret,\n configmap: K8SConfigmap,\n value: UiEdit,\n};\n\nconst SOURCE_LABEL: Record<SecretValueSource, string> = {\n secret: \"Secret\",\n configmap: \"ConfigMap\",\n value: \"Value\",\n};\n\n// buildKeyOptions labels each key with its mid-masked preview value when one is\n// available. A selected key absent from the list is pinned so the selection\n// stays visible.\nexport function buildKeyOptions(\n keys: string[],\n previews: KeyPreview[],\n selectedKey: string,\n): ComboboxOption[] {\n const masked = new Map(previews.map((p) => [p.key, p.value]));\n const base = keys.map((k) => ({\n value: k,\n label: masked.has(k) ? `${k} — ${masked.get(k)}` : k,\n }));\n if (selectedKey && !base.some((o) => o.value === selectedKey)) {\n return [{ value: selectedKey, label: selectedKey }, ...base];\n }\n return base;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SecretKeySelector.js","sources":["../../src/components/SecretKeySelector.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { K8SSecret, K8SConfigmap } from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { UiEdit } from \"../icons\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// SecretKeySelector picks a Secret or ConfigMap and one of its keys, with a\n// mid-masked preview of every key's value so the operator can tell which key\n// holds the host vs the db vs a password. It also offers a \"Value\" mode (on by\n// default; opt out with `allowLiteral={false}`) for typing a static inline\n// string. It is presentational: it fetches nothing — the consumer supplies\n// async `loadResources` / `loadKeyPreview` getters. It emits a discriminated\n// value the consumer lowers into whatever reference shape it needs.\n\nexport type SecretKind = \"secret\" | \"configmap\";\n\n/** Toggle options: the two resource kinds plus the inline-literal mode. */\nexport type SecretValueSource = SecretKind | \"value\";\n\n/** One key's mid-masked preview. `value` is already masked by the consumer. */\nexport type KeyPreview = { key: string; value: string };\n\n/**\n * The selector's value: either a {kind,name,key} reference into a Secret /\n * ConfigMap, or a static inline literal ({kind:\"value\", value}).\n */\nexport type SecretKeyValue =\n | { kind: SecretKind; name: string; key: string }\n | { kind: \"value\"; value: string };\n\n/** A named secret/configmap and its data key names (values never returned). */\nexport type SecretResource = { name: string; keys?: string[] };\n\nexport type SecretKeySelectorProps = {\n value: SecretKeyValue | undefined;\n onChange: (next: SecretKeyValue | undefined) => void;\n /** Loads the named kind's resources (name + data key names). */\n loadResources: (kind: SecretKind) => Promise<SecretResource[]>;\n /** Loads mid-masked previews for the named resource's keys. */\n loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;\n /**\n * Offer a third \"Value\" toggle for typing a static inline literal. Enabled by\n * default; pass `false` to restrict the selector to Secret/ConfigMap\n * references only.\n */\n allowLiteral?: boolean;\n /**\n * When true, a chosen secret/configmap name absent from the loaded resources,\n * or a key absent from the chosen resource's keys, is flagged invalid (once\n * the respective load settles). The reference doesn't exist in the namespace.\n * The literal \"Value\" mode is never strict-flagged.\n */\n strict?: boolean;\n className?: string;\n};\n\nexport function SecretKeySelector({\n value,\n onChange,\n loadResources,\n loadKeyPreview,\n allowLiteral = true,\n strict = false,\n className,\n}: SecretKeySelectorProps) {\n const source: SecretValueSource = value?.kind ?? \"secret\";\n const isLiteral = source === \"value\";\n // A literal value carries no resource kind; the loaders default to \"secret\".\n const refKind: SecretKind = isLiteral ? \"secret\" : source;\n const selectedName = value && value.kind !== \"value\" ? value.name : \"\";\n const selectedKey = value && value.kind !== \"value\" ? value.key : \"\";\n const literalValue = value && value.kind === \"value\" ? value.value : \"\";\n\n const [resources, setResources] = useState<SecretResource[]>([]);\n const [resourcesLoading, setResourcesLoading] = useState(false);\n const [previews, setPreviews] = useState<KeyPreview[]>([]);\n const [previewLoading, setPreviewLoading] = useState(false);\n\n useEffect(() => {\n // Literal mode references no resource, so skip the list fetch entirely.\n if (isLiteral) return;\n let cancelled = false;\n setResourcesLoading(true);\n loadResources(refKind)\n .then((res) => !cancelled && setResources(res))\n .finally(() => !cancelled && setResourcesLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadResources, refKind, isLiteral]);\n\n useEffect(() => {\n if (!selectedName) {\n setPreviews([]);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n loadKeyPreview(refKind, selectedName)\n .then((res) => !cancelled && setPreviews(res))\n .catch(() => !cancelled && setPreviews([]))\n .finally(() => !cancelled && setPreviewLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadKeyPreview, refKind, selectedName]);\n\n const nameOptions = useMemo<ComboboxOption[]>(\n () => resources.map((r) => ({ value: r.name, label: r.name })),\n [resources],\n );\n const selectedResource = useMemo(\n () => resources.find((r) => r.name === selectedName),\n [resources, selectedName],\n );\n const keyOptions = useMemo(\n () => buildKeyOptions(selectedResource?.keys ?? [], previews, selectedKey),\n [selectedResource, selectedKey, previews],\n );\n\n // Strict validity: the name is invalid when it names no loaded resource; the\n // key is invalid when, with a resolved resource, it isn't one of that\n // resource's keys. The key list comes from the resource list (not previews),\n // so both are gated on resourcesLoading only.\n const nameInvalid = strict && !!selectedName && !resourcesLoading && !selectedResource;\n const keyInvalid =\n strict &&\n !!selectedKey &&\n !!selectedResource &&\n !resourcesLoading &&\n !(selectedResource.keys ?? []).includes(selectedKey);\n\n // Switching source resets the selection: a resource kind clears name+key; the\n // literal mode seeds an empty string (so onChange emits a value-kind value).\n const setSource = (next: SecretValueSource) =>\n onChange(next === \"value\" ? { kind: \"value\", value: \"\" } : { kind: next, name: \"\", key: \"\" });\n const setName = (name: string) =>\n onChange(name ? { kind: refKind, name, key: selectedKey } : undefined);\n const setKey = (key: string) =>\n selectedName ? onChange({ kind: refKind, name: selectedName, key }) : undefined;\n const setLiteral = (v: string) => onChange({ kind: \"value\", value: v });\n\n const sources: SecretValueSource[] = allowLiteral\n ? [\"secret\", \"configmap\", \"value\"]\n : [\"secret\", \"configmap\"];\n\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <div className=\"flex shrink-0 overflow-hidden rounded border border-border text-xs\">\n {sources.map((k) => {\n const Glyph = SOURCE_ICON[k];\n return (\n <button\n key={k}\n type=\"button\"\n onClick={() => setSource(k)}\n className={cn(\n \"flex items-center gap-1 whitespace-nowrap px-2 py-1\",\n source === k\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted text-muted-foreground hover:bg-accent\",\n )}\n >\n <Glyph className=\"h-3.5 w-3.5\" />\n {SOURCE_LABEL[k]}\n </button>\n );\n })}\n </div>\n {isLiteral ? (\n <input\n type=\"text\"\n value={literalValue}\n onChange={(e) => setLiteral(e.target.value)}\n placeholder=\"Static value…\"\n className={cn(\n \"h-control-h min-w-0 flex-1 rounded border border-input bg-background px-control-px text-sm\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n />\n ) : (\n <>\n <div className=\"w-44 shrink-0\">\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder={`Select ${refKind}…`}\n />\n </div>\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={keyOptions}\n value={selectedKey}\n onChange={setKey}\n allowCustomValue\n loading={previewLoading}\n invalid={keyInvalid}\n placeholder={selectedName ? \"Key…\" : \"—\"}\n />\n </div>\n </>\n )}\n </div>\n );\n}\n\n// Icons share a className-only call shape; the flanksource FCs carry extra\n// static metadata that doesn't unify with ComponentType, so type the slot as a\n// plain render function instead.\nconst SOURCE_ICON: Record<SecretValueSource, (p: { className?: string }) => ReactNode> = {\n secret: K8SSecret,\n configmap: K8SConfigmap,\n value: UiEdit,\n};\n\nconst SOURCE_LABEL: Record<SecretValueSource, string> = {\n secret: \"Secret\",\n configmap: \"ConfigMap\",\n value: \"Value\",\n};\n\n// buildKeyOptions labels each key with its mid-masked preview value when one is\n// available. A selected key absent from the list is pinned so the selection\n// stays visible.\nexport function buildKeyOptions(\n keys: string[],\n previews: KeyPreview[],\n selectedKey: string,\n): ComboboxOption[] {\n const masked = new Map(previews.map((p) => [p.key, p.value]));\n const base = keys.map((k) => ({\n value: k,\n label: masked.has(k) ? `${k} — ${masked.get(k)}` : k,\n }));\n if (selectedKey && !base.some((o) => o.value === selectedKey)) {\n return [{ value: selectedKey, label: selectedKey }, ...base];\n }\n return base;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -35,7 +35,14 @@ function deriveCoreBars(usageMilli, limitMilli) {
|
|
|
35
35
|
const bars = Array.from({ length: coreCount }, (_, i) => ({
|
|
36
36
|
fill: clamp01(usageCores - i)
|
|
37
37
|
}));
|
|
38
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
hasUsage,
|
|
40
|
+
usageCores,
|
|
41
|
+
coreCount,
|
|
42
|
+
pct,
|
|
43
|
+
bars,
|
|
44
|
+
...limitCores !== void 0 && { limitCores }
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
47
|
function CoreBarsIcon({ icon }) {
|
|
41
48
|
if (typeof icon === "string") {
|
|
@@ -1 +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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"}
|
|
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 // limitCores is an optional field under exactOptionalPropertyTypes — only set\n // the key when a limit resolved, rather than assigning an explicit undefined.\n return {\n hasUsage,\n usageCores,\n coreCount,\n pct,\n bars,\n ...(limitCores !== undefined && { limitCores }),\n };\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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"}
|
|
@@ -1 +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,
|
|
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,CA4Bf;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"}
|
|
@@ -33,7 +33,14 @@ function deriveCoreBars(usageMilli, limitMilli) {
|
|
|
33
33
|
const bars = Array.from({ length: coreCount }, (_, i) => ({
|
|
34
34
|
fill: clamp01(usageCores - i)
|
|
35
35
|
}));
|
|
36
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
hasUsage,
|
|
38
|
+
usageCores,
|
|
39
|
+
coreCount,
|
|
40
|
+
pct,
|
|
41
|
+
bars,
|
|
42
|
+
...limitCores !== void 0 && { limitCores }
|
|
43
|
+
};
|
|
37
44
|
}
|
|
38
45
|
function CoreBarsIcon({ icon }) {
|
|
39
46
|
if (typeof icon === "string") {
|
|
@@ -1 +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":";;;;;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,WAAO,oBAAC,QAAK,MAAM,MAAM,OAAO,IAAI,QAAQ,IAAI,WAAU,wBAAA,CAAwB;AAAA,EACpF;AACA,SAAO,oBAAC,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,MAAM,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,UAAU,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,8BACG,OAAA,EAAI,WAAW,GAAG,oCAAoC,SAAS,GAC9D,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,+BACZ,UAAA,MAAM,KAAK,IAAI,CAAC,KAAK,MAAM;AAC1B,YAAM,UAAU,KAAK,MAAM,IAAI,OAAO,GAAG;AACzC,aACE;AAAA,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,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,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,IACA,oBAAC,QAAA,EAAK,WAAU,yCAAyC,UAAA,SAAQ;AAAA,IACjE,qBAAC,OAAA,EAAI,WAAU,yDACZ,UAAA;AAAA,MAAA,OAAO,oBAAC,cAAA,EAAa,KAAA,CAAY,IAAK;AAAA,MACvC,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,IAAA,EAAA,CACf;AAAA,EAAA,GACF;AAEJ;"}
|
|
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 // limitCores is an optional field under exactOptionalPropertyTypes — only set\n // the key when a limit resolved, rather than assigning an explicit undefined.\n return {\n hasUsage,\n usageCores,\n coreCount,\n pct,\n bars,\n ...(limitCores !== undefined && { limitCores }),\n };\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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"}
|
|
@@ -15,9 +15,9 @@ function detectMode() {
|
|
|
15
15
|
}
|
|
16
16
|
function getVersionInfo() {
|
|
17
17
|
return {
|
|
18
|
-
commit: read(() => "
|
|
19
|
-
tag: read(() => "clicky-ui@0.2.
|
|
20
|
-
date: read(() => "2026-06-
|
|
18
|
+
commit: read(() => "e02faed", ""),
|
|
19
|
+
tag: read(() => "clicky-ui@0.2.13", ""),
|
|
20
|
+
date: read(() => "2026-06-10T10:39:58.174Z", ""),
|
|
21
21
|
dirty: read(() => true, false),
|
|
22
22
|
mode: detectMode()
|
|
23
23
|
};
|
|
@@ -13,9 +13,9 @@ function detectMode() {
|
|
|
13
13
|
}
|
|
14
14
|
function getVersionInfo() {
|
|
15
15
|
return {
|
|
16
|
-
commit: read(() => "
|
|
17
|
-
tag: read(() => "clicky-ui@0.2.
|
|
18
|
-
date: read(() => "2026-06-
|
|
16
|
+
commit: read(() => "e02faed", ""),
|
|
17
|
+
tag: read(() => "clicky-ui@0.2.13", ""),
|
|
18
|
+
date: read(() => "2026-06-10T10:39:47.086Z", ""),
|
|
19
19
|
dirty: read(() => true, false),
|
|
20
20
|
mode: detectMode()
|
|
21
21
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flanksource/clicky-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.",
|
|
5
5
|
"homepage": "https://github.com/flanksource/clicky-ui#readme",
|
|
6
6
|
"bugs": "https://github.com/flanksource/clicky-ui/issues",
|