@flanksource/clicky-ui 0.2.11 → 0.2.13
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/Combobox.cjs +47 -33
- package/dist/components/Combobox.cjs.map +1 -1
- package/dist/components/Combobox.d.ts +12 -0
- package/dist/components/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox.js +48 -34
- package/dist/components/Combobox.js.map +1 -1
- package/dist/components/IconButton.cjs +26 -0
- package/dist/components/IconButton.cjs.map +1 -0
- package/dist/components/IconButton.d.ts +25 -0
- package/dist/components/IconButton.d.ts.map +1 -0
- package/dist/components/IconButton.js +26 -0
- package/dist/components/IconButton.js.map +1 -0
- package/dist/components/JsonSchemaForm.cjs +1 -1
- package/dist/components/JsonSchemaForm.cjs.map +1 -1
- package/dist/components/JsonSchemaForm.js +1 -1
- package/dist/components/JsonSchemaForm.js.map +1 -1
- package/dist/components/SecretKeySelector.cjs +151 -0
- package/dist/components/SecretKeySelector.cjs.map +1 -0
- package/dist/components/SecretKeySelector.d.ts +47 -0
- package/dist/components/SecretKeySelector.d.ts.map +1 -0
- package/dist/components/SecretKeySelector.js +151 -0
- package/dist/components/SecretKeySelector.js.map +1 -0
- package/dist/components/WorkloadPicker.cjs +144 -0
- package/dist/components/WorkloadPicker.cjs.map +1 -0
- package/dist/components/WorkloadPicker.d.ts +52 -0
- package/dist/components/WorkloadPicker.d.ts.map +1 -0
- package/dist/components/WorkloadPicker.js +144 -0
- package/dist/components/WorkloadPicker.js.map +1 -0
- package/dist/components/icon-menu-picker.cjs +83 -112
- package/dist/components/icon-menu-picker.cjs.map +1 -1
- package/dist/components/icon-menu-picker.d.ts.map +1 -1
- package/dist/components/icon-menu-picker.js +85 -114
- package/dist/components/icon-menu-picker.js.map +1 -1
- package/dist/components/json-schema-form-fields.cjs +9 -7
- package/dist/components/json-schema-form-fields.cjs.map +1 -1
- package/dist/components/json-schema-form-fields.d.ts.map +1 -1
- package/dist/components/json-schema-form-fields.js +9 -7
- package/dist/components/json-schema-form-fields.js.map +1 -1
- package/dist/components/json-schema-form-types.d.ts +8 -4
- package/dist/components/json-schema-form-types.d.ts.map +1 -1
- package/dist/components.cjs +10 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +11 -1
- package/dist/components.js.map +1 -1
- package/dist/data/Clicky.cjs +1 -1
- package/dist/data/Clicky.cjs.map +1 -1
- package/dist/data/Clicky.js +1 -1
- package/dist/data/Clicky.js.map +1 -1
- package/dist/data/LogDetails.cjs +4 -2
- package/dist/data/LogDetails.cjs.map +1 -1
- package/dist/data/LogDetails.js +4 -2
- package/dist/data/LogDetails.js.map +1 -1
- package/dist/data/Version.cjs +42 -0
- package/dist/data/Version.cjs.map +1 -0
- package/dist/data/Version.d.ts +36 -0
- package/dist/data/Version.d.ts.map +1 -0
- package/dist/data/Version.js +42 -0
- package/dist/data/Version.js.map +1 -0
- package/dist/data/test-runner/TestDetailPanel.cjs +165 -0
- package/dist/data/test-runner/TestDetailPanel.cjs.map +1 -0
- package/dist/data/test-runner/TestDetailPanel.d.ts +7 -0
- package/dist/data/test-runner/TestDetailPanel.d.ts.map +1 -0
- package/dist/data/test-runner/TestDetailPanel.js +165 -0
- package/dist/data/test-runner/TestDetailPanel.js.map +1 -0
- package/dist/data/test-runner/TestFailureDetail.cjs +78 -0
- package/dist/data/test-runner/TestFailureDetail.cjs.map +1 -0
- package/dist/data/test-runner/TestFailureDetail.d.ts +14 -0
- package/dist/data/test-runner/TestFailureDetail.d.ts.map +1 -0
- package/dist/data/test-runner/TestFailureDetail.js +78 -0
- package/dist/data/test-runner/TestFailureDetail.js.map +1 -0
- package/dist/data/test-runner/TestFilterBar.cjs +108 -0
- package/dist/data/test-runner/TestFilterBar.cjs.map +1 -0
- package/dist/data/test-runner/TestFilterBar.d.ts +15 -0
- package/dist/data/test-runner/TestFilterBar.d.ts.map +1 -0
- package/dist/data/test-runner/TestFilterBar.js +108 -0
- package/dist/data/test-runner/TestFilterBar.js.map +1 -0
- package/dist/data/test-runner/TestRunSummary.cjs +81 -0
- package/dist/data/test-runner/TestRunSummary.cjs.map +1 -0
- package/dist/data/test-runner/TestRunSummary.d.ts +19 -0
- package/dist/data/test-runner/TestRunSummary.d.ts.map +1 -0
- package/dist/data/test-runner/TestRunSummary.js +81 -0
- package/dist/data/test-runner/TestRunSummary.js.map +1 -0
- package/dist/data/test-runner/TestRunner.cjs +143 -0
- package/dist/data/test-runner/TestRunner.cjs.map +1 -0
- package/dist/data/test-runner/TestRunner.d.ts +42 -0
- package/dist/data/test-runner/TestRunner.d.ts.map +1 -0
- package/dist/data/test-runner/TestRunner.fixtures.d.ts +26 -0
- package/dist/data/test-runner/TestRunner.fixtures.d.ts.map +1 -0
- package/dist/data/test-runner/TestRunner.js +143 -0
- package/dist/data/test-runner/TestRunner.js.map +1 -0
- package/dist/data/test-runner/TestTree.cjs +42 -0
- package/dist/data/test-runner/TestTree.cjs.map +1 -0
- package/dist/data/test-runner/TestTree.d.ts +9 -0
- package/dist/data/test-runner/TestTree.d.ts.map +1 -0
- package/dist/data/test-runner/TestTree.js +42 -0
- package/dist/data/test-runner/TestTree.js.map +1 -0
- package/dist/data/test-runner/TestTreeNode.cjs +99 -0
- package/dist/data/test-runner/TestTreeNode.cjs.map +1 -0
- package/dist/data/test-runner/TestTreeNode.d.ts +12 -0
- package/dist/data/test-runner/TestTreeNode.d.ts.map +1 -0
- package/dist/data/test-runner/TestTreeNode.js +99 -0
- package/dist/data/test-runner/TestTreeNode.js.map +1 -0
- package/dist/data/test-runner/adapter.cjs +57 -0
- package/dist/data/test-runner/adapter.cjs.map +1 -0
- package/dist/data/test-runner/adapter.d.ts +60 -0
- package/dist/data/test-runner/adapter.d.ts.map +1 -0
- package/dist/data/test-runner/adapter.js +57 -0
- package/dist/data/test-runner/adapter.js.map +1 -0
- package/dist/data/test-runner/context.cjs +15 -0
- package/dist/data/test-runner/context.cjs.map +1 -0
- package/dist/data/test-runner/context.d.ts +40 -0
- package/dist/data/test-runner/context.d.ts.map +1 -0
- package/dist/data/test-runner/context.js +15 -0
- package/dist/data/test-runner/context.js.map +1 -0
- package/dist/data/test-runner/filterState.cjs +47 -0
- package/dist/data/test-runner/filterState.cjs.map +1 -0
- package/dist/data/test-runner/filterState.d.ts +14 -0
- package/dist/data/test-runner/filterState.d.ts.map +1 -0
- package/dist/data/test-runner/filterState.js +47 -0
- package/dist/data/test-runner/filterState.js.map +1 -0
- package/dist/data/test-runner/frameworkIcon.cjs +27 -0
- package/dist/data/test-runner/frameworkIcon.cjs.map +1 -0
- package/dist/data/test-runner/frameworkIcon.d.ts +3 -0
- package/dist/data/test-runner/frameworkIcon.d.ts.map +1 -0
- package/dist/data/test-runner/frameworkIcon.js +27 -0
- package/dist/data/test-runner/frameworkIcon.js.map +1 -0
- package/dist/data/test-runner/index.d.ts +14 -0
- package/dist/data/test-runner/index.d.ts.map +1 -0
- package/dist/data/test-runner/status.cjs +234 -0
- package/dist/data/test-runner/status.cjs.map +1 -0
- package/dist/data/test-runner/status.d.ts +52 -0
- package/dist/data/test-runner/status.d.ts.map +1 -0
- package/dist/data/test-runner/status.js +234 -0
- package/dist/data/test-runner/status.js.map +1 -0
- package/dist/data/test-runner/types.d.ts +148 -0
- package/dist/data/test-runner/types.d.ts.map +1 -0
- package/dist/data/version-info.cjs +26 -0
- package/dist/data/version-info.cjs.map +1 -0
- package/dist/data/version-info.d.ts +11 -0
- package/dist/data/version-info.d.ts.map +1 -0
- package/dist/data/version-info.js +26 -0
- package/dist/data/version-info.js.map +1 -0
- package/dist/data.cjs +49 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +3 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +50 -1
- package/dist/data.js.map +1 -1
- package/dist/index.cjs +59 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +60 -1
- package/dist/index.js.map +1 -1
- package/dist/layout/SplitPane.cjs +34 -27
- package/dist/layout/SplitPane.cjs.map +1 -1
- package/dist/layout/SplitPane.d.ts.map +1 -1
- package/dist/layout/SplitPane.js +34 -27
- package/dist/layout/SplitPane.js.map +1 -1
- package/dist/overlay/DropdownMenu.cjs +57 -30
- package/dist/overlay/DropdownMenu.cjs.map +1 -1
- package/dist/overlay/DropdownMenu.d.ts.map +1 -1
- package/dist/overlay/DropdownMenu.js +57 -30
- package/dist/overlay/DropdownMenu.js.map +1 -1
- package/dist/overlay/Modal.cjs +62 -6
- package/dist/overlay/Modal.cjs.map +1 -1
- package/dist/overlay/Modal.d.ts +19 -1
- package/dist/overlay/Modal.d.ts.map +1 -1
- package/dist/overlay/Modal.js +62 -6
- package/dist/overlay/Modal.js.map +1 -1
- package/dist/rpc/CommandForm.cjs +2 -2
- package/dist/rpc/CommandForm.cjs.map +1 -1
- package/dist/rpc/CommandForm.d.ts.map +1 -1
- package/dist/rpc/CommandForm.js +3 -3
- package/dist/rpc/CommandForm.js.map +1 -1
- package/dist/rpc/formMetadata.cjs +5 -4
- package/dist/rpc/formMetadata.cjs.map +1 -1
- package/dist/rpc/formMetadata.d.ts.map +1 -1
- package/dist/rpc/formMetadata.js +6 -5
- package/dist/rpc/formMetadata.js.map +1 -1
- package/dist/rpc/types.cjs +4 -0
- package/dist/rpc/types.cjs.map +1 -1
- package/dist/rpc/types.d.ts +10 -0
- package/dist/rpc/types.d.ts.map +1 -1
- package/dist/rpc/types.js +5 -1
- package/dist/rpc/types.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const react = require("react");
|
|
5
|
+
const mi = require("@flanksource/icons/mi");
|
|
6
|
+
const utils = require("../lib/utils.cjs");
|
|
7
|
+
const Combobox = require("./Combobox.cjs");
|
|
8
|
+
const WORKLOAD_META = {
|
|
9
|
+
service: { label: "Service", Icon: mi.K8SService },
|
|
10
|
+
ingress: { label: "Ingress", Icon: mi.K8SIngress },
|
|
11
|
+
deployment: { label: "Deployment", Icon: mi.K8SDeployment },
|
|
12
|
+
statefulset: { label: "StatefulSet", Icon: mi.K8SStatefulset }
|
|
13
|
+
};
|
|
14
|
+
const ALL_KINDS = ["service", "ingress", "deployment", "statefulset"];
|
|
15
|
+
function WorkloadPicker({
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
loadWorkloads,
|
|
19
|
+
namespace,
|
|
20
|
+
kinds = ALL_KINDS,
|
|
21
|
+
strict = false,
|
|
22
|
+
placeholder = "Select workload / service…",
|
|
23
|
+
className
|
|
24
|
+
}) {
|
|
25
|
+
const [byKind, setByKind] = react.useState({});
|
|
26
|
+
const [loading, setLoading] = react.useState(false);
|
|
27
|
+
const kindsKey = kinds.join(",");
|
|
28
|
+
react.useEffect(() => {
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
setLoading(true);
|
|
31
|
+
loadWorkloads(kinds).then((res) => {
|
|
32
|
+
if (!cancelled) setByKind(res);
|
|
33
|
+
}).finally(() => {
|
|
34
|
+
if (!cancelled) setLoading(false);
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true;
|
|
38
|
+
};
|
|
39
|
+
}, [loadWorkloads, kindsKey]);
|
|
40
|
+
const options = react.useMemo(
|
|
41
|
+
() => buildWorkloadOptions(namespace, byKind, kinds, value),
|
|
42
|
+
[namespace, byKind, kinds, value]
|
|
43
|
+
);
|
|
44
|
+
const invalid = react.useMemo(() => {
|
|
45
|
+
if (!strict || !value || loading) return false;
|
|
46
|
+
const { keys, names } = loadedWorkloads(namespace, byKind, kinds);
|
|
47
|
+
return !keys.has(value) && !names.has(value);
|
|
48
|
+
}, [strict, value, loading, namespace, byKind, kinds]);
|
|
49
|
+
const selectedKind = kindForValue(kinds, value);
|
|
50
|
+
const leadMeta = WORKLOAD_META[selectedKind];
|
|
51
|
+
const LeadIcon = leadMeta.Icon;
|
|
52
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: utils.cn("flex items-center gap-2", className), children: [
|
|
53
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
54
|
+
LeadIcon,
|
|
55
|
+
{
|
|
56
|
+
className: "h-4 w-4 shrink-0 text-muted-foreground",
|
|
57
|
+
title: leadMeta.label,
|
|
58
|
+
"aria-label": leadMeta.label
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
62
|
+
Combobox.Combobox,
|
|
63
|
+
{
|
|
64
|
+
options,
|
|
65
|
+
value,
|
|
66
|
+
onChange,
|
|
67
|
+
allowCustomValue: true,
|
|
68
|
+
loading,
|
|
69
|
+
invalid,
|
|
70
|
+
placeholder
|
|
71
|
+
}
|
|
72
|
+
) })
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
function ingressHost(kind, r) {
|
|
76
|
+
var _a;
|
|
77
|
+
return kind === "ingress" ? (_a = r.hosts) == null ? void 0 : _a[0] : void 0;
|
|
78
|
+
}
|
|
79
|
+
function workloadName(kind, r) {
|
|
80
|
+
return ingressHost(kind, r) ?? r.name;
|
|
81
|
+
}
|
|
82
|
+
const KNOWN_KINDS = /* @__PURE__ */ new Set(["service", "ingress", "deployment", "statefulset"]);
|
|
83
|
+
function workloadKey(namespace, kind, r) {
|
|
84
|
+
const name = workloadName(kind, r);
|
|
85
|
+
return namespace ? `${namespace}/${kind}/${name}` : `${kind}/${name}`;
|
|
86
|
+
}
|
|
87
|
+
function parseWorkloadKey(value) {
|
|
88
|
+
const parts = value.split("/");
|
|
89
|
+
const name = parts.at(-1);
|
|
90
|
+
const maybeKind = parts.at(-2);
|
|
91
|
+
if (name != null && maybeKind != null && KNOWN_KINDS.has(maybeKind)) {
|
|
92
|
+
const namespace = parts.slice(0, -2).join("/");
|
|
93
|
+
const parsed = { kind: maybeKind, name };
|
|
94
|
+
if (namespace) parsed.namespace = namespace;
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
return { name: value };
|
|
98
|
+
}
|
|
99
|
+
function loadedWorkloads(namespace, byKind, kinds) {
|
|
100
|
+
const keys = /* @__PURE__ */ new Set();
|
|
101
|
+
const names = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const kind of kinds) {
|
|
103
|
+
for (const r of byKind[kind] ?? []) {
|
|
104
|
+
keys.add(workloadKey(namespace, kind, r));
|
|
105
|
+
names.add(workloadName(kind, r));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { keys, names };
|
|
109
|
+
}
|
|
110
|
+
function kindForValue(kinds, value) {
|
|
111
|
+
const kind = value ? parseWorkloadKey(value).kind : void 0;
|
|
112
|
+
return kind ?? kinds[0] ?? "service";
|
|
113
|
+
}
|
|
114
|
+
function buildWorkloadOptions(namespace, byKind, kinds, value) {
|
|
115
|
+
const opts = [];
|
|
116
|
+
for (const kind of kinds) {
|
|
117
|
+
const meta = WORKLOAD_META[kind];
|
|
118
|
+
const Icon = meta.Icon;
|
|
119
|
+
for (const r of byKind[kind] ?? []) {
|
|
120
|
+
const host = ingressHost(kind, r);
|
|
121
|
+
opts.push({
|
|
122
|
+
value: workloadKey(namespace, kind, r),
|
|
123
|
+
// The label is the human name; an ingress pairs its host with the
|
|
124
|
+
// ingress name for context.
|
|
125
|
+
label: host ? `${host} (${r.name})` : r.name,
|
|
126
|
+
group: meta.label,
|
|
127
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4" })
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (value && !opts.some((o) => o.value === value)) {
|
|
132
|
+
const parsed = parseWorkloadKey(value);
|
|
133
|
+
const Icon = WORKLOAD_META[parsed.kind ?? kinds[0] ?? "service"].Icon;
|
|
134
|
+
return [{ value, label: parsed.name, icon: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4" }) }, ...opts];
|
|
135
|
+
}
|
|
136
|
+
return opts;
|
|
137
|
+
}
|
|
138
|
+
exports.WorkloadPicker = WorkloadPicker;
|
|
139
|
+
exports.buildWorkloadOptions = buildWorkloadOptions;
|
|
140
|
+
exports.kindForValue = kindForValue;
|
|
141
|
+
exports.loadedWorkloads = loadedWorkloads;
|
|
142
|
+
exports.parseWorkloadKey = parseWorkloadKey;
|
|
143
|
+
exports.workloadKey = workloadKey;
|
|
144
|
+
//# sourceMappingURL=WorkloadPicker.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkloadPicker.cjs","sources":["../../src/components/WorkloadPicker.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ComponentType } from \"react\";\nimport {\n K8SService,\n K8SIngress,\n K8SDeployment,\n K8SStatefulset,\n} from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// WorkloadPicker selects a backing Kubernetes workload (Service / Ingress /\n// Deployment / StatefulSet) for an endpoint. Options from every kind are merged\n// into one Combobox, grouped by kind via the Combobox group-header support and\n// labelled with the kind's icon.\n//\n// The emitted value is a `[namespace/]kind/name` key (see workloadKey /\n// parseWorkloadKey) so two workloads of different kinds that share a name don't\n// collide. An ingress's name-part is its first host (the routable address),\n// labelled with the ingress name for context; every other kind uses its name.\n//\n// The component is presentational: it owns the kind→group/icon mapping and the\n// key encoding but fetches nothing — the consumer supplies an async\n// `loadWorkloads` getter.\n\nexport type WorkloadKind = \"service\" | \"ingress\" | \"deployment\" | \"statefulset\";\n\n/** One workload returned by the loader. Ingresses may carry rule hostnames. */\nexport type WorkloadResource = { name: string; hosts?: string[] };\n\ntype KindIconProps = { className?: string; title?: string; \"aria-label\"?: string };\ntype KindMeta = { label: string; Icon: ComponentType<KindIconProps> };\n\n// WORKLOAD_META maps each kind to its group header label and icon, in display\n// order. The order of the keys drives the order options/groups appear in.\nconst WORKLOAD_META: Record<WorkloadKind, KindMeta> = {\n service: { label: \"Service\", Icon: K8SService },\n ingress: { label: \"Ingress\", Icon: K8SIngress },\n deployment: { label: \"Deployment\", Icon: K8SDeployment },\n statefulset: { label: \"StatefulSet\", Icon: K8SStatefulset },\n};\n\nconst ALL_KINDS: WorkloadKind[] = [\"service\", \"ingress\", \"deployment\", \"statefulset\"];\n\nexport type WorkloadPickerProps = {\n /**\n * Controlled selected value — a `[namespace/]kind/name` key (see\n * {@link workloadKey} / {@link parseWorkloadKey}).\n */\n value: string;\n /** Called with the chosen `[namespace/]kind/name` key (or \"\" when cleared). */\n onChange: (value: string) => void;\n /**\n * Async getter the component calls to load the requested kinds' workloads.\n * Returns a map keyed by kind. The consumer owns fetching/caching.\n */\n loadWorkloads: (kinds: WorkloadKind[]) => Promise<Record<WorkloadKind, WorkloadResource[]>>;\n /**\n * Namespace the workloads live in. When set it prefixes the emitted key\n * (`namespace/kind/name`), so values stay distinct across namespaces.\n */\n namespace?: string;\n /** Kinds to offer, in display order. Defaults to all four. */\n kinds?: WorkloadKind[];\n /**\n * When true, a non-empty value that does not match any loaded workload (once\n * loading settles) is flagged invalid — the picked workload doesn't exist in\n * the namespace. The value is still shown (pinned) so the user can see and\n * fix it; freeform entry is not rejected, only marked.\n */\n strict?: boolean;\n placeholder?: string;\n className?: string;\n};\n\nexport function WorkloadPicker({\n value,\n onChange,\n loadWorkloads,\n namespace,\n kinds = ALL_KINDS,\n strict = false,\n placeholder = \"Select workload / service…\",\n className,\n}: WorkloadPickerProps) {\n const [byKind, setByKind] = useState<Partial<Record<WorkloadKind, WorkloadResource[]>>>({});\n const [loading, setLoading] = useState(false);\n\n const kindsKey = kinds.join(\",\");\n useEffect(() => {\n let cancelled = false;\n setLoading(true);\n loadWorkloads(kinds)\n .then((res) => {\n if (!cancelled) setByKind(res);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n // loadWorkloads is expected to be stable (memoized by the consumer); kindsKey\n // is the joined `kinds` so a changed selection reloads without depending on\n // the array's identity.\n }, [loadWorkloads, kindsKey]);\n\n const options = useMemo(\n () => buildWorkloadOptions(namespace, byKind, kinds, value),\n [namespace, byKind, kinds, value],\n );\n\n // In strict mode a non-empty value is invalid once loading settles unless it\n // matches a loaded workload — either by its full `[namespace/]kind/name` key\n // (what the picker emits) or by its bare name-part (what consumers that\n // persist only the resolved name, e.g. a Service name or ingress host, feed\n // back in). buildWorkloadOptions pins an unmatched value first, so test\n // membership against the loaded set, not `options`.\n const invalid = useMemo(() => {\n if (!strict || !value || loading) return false;\n const { keys, names } = loadedWorkloads(namespace, byKind, kinds);\n return !keys.has(value) && !names.has(value);\n }, [strict, value, loading, namespace, byKind, kinds]);\n\n // The lead icon reflects the selected workload's kind (read from the value's\n // key), falling back to the first offered kind when nothing is selected or the\n // value carries no recognised kind (e.g. freeform input).\n const selectedKind = kindForValue(kinds, value);\n const leadMeta = WORKLOAD_META[selectedKind];\n const LeadIcon = leadMeta.Icon;\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <LeadIcon\n className=\"h-4 w-4 shrink-0 text-muted-foreground\"\n title={leadMeta.label}\n aria-label={leadMeta.label}\n />\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={options}\n value={value}\n onChange={onChange}\n allowCustomValue\n loading={loading}\n invalid={invalid}\n placeholder={placeholder}\n />\n </div>\n </div>\n );\n}\n\n// ingressHost is the first host an ingress routes, or undefined for non-ingress\n// kinds and host-less ingresses. An ingress's name-part is its host.\nfunction ingressHost(kind: WorkloadKind, r: WorkloadResource): string | undefined {\n return kind === \"ingress\" ? r.hosts?.[0] : undefined;\n}\n\n// workloadName is the name-part of a resource's key: an ingress's first host\n// (the routable address callers want), else the resource's own name.\nfunction workloadName(kind: WorkloadKind, r: WorkloadResource): string {\n return ingressHost(kind, r) ?? r.name;\n}\n\n// ParsedWorkloadKey is the structured form of a `[namespace/]kind/name` value.\nexport type ParsedWorkloadKey = { namespace?: string; kind?: WorkloadKind; name: string };\n\nconst KNOWN_KINDS = new Set<string>([\"service\", \"ingress\", \"deployment\", \"statefulset\"]);\n\n// workloadKey is the value a resource contributes (what onChange emits): a\n// `[namespace/]kind/name` composite so two workloads of different kinds sharing\n// a name don't collide. namespace is omitted when not supplied.\nexport function workloadKey(\n namespace: string | undefined,\n kind: WorkloadKind,\n r: WorkloadResource,\n): string {\n const name = workloadName(kind, r);\n return namespace ? `${namespace}/${kind}/${name}` : `${kind}/${name}`;\n}\n\n// parseWorkloadKey splits a `[namespace/]kind/name` value back into its parts.\n// The trailing segment is always the name (a host may itself contain no slash);\n// the segment before it is the kind when it is a known workload kind, and any\n// remaining leading segment is the namespace. A bare value (no recognised\n// kind/name shape) is returned as just its name so freeform input survives.\nexport function parseWorkloadKey(value: string): ParsedWorkloadKey {\n const parts = value.split(\"/\");\n const name = parts.at(-1);\n const maybeKind = parts.at(-2);\n if (name != null && maybeKind != null && KNOWN_KINDS.has(maybeKind)) {\n const namespace = parts.slice(0, -2).join(\"/\");\n const parsed: ParsedWorkloadKey = { kind: maybeKind as WorkloadKind, name };\n if (namespace) parsed.namespace = namespace;\n return parsed;\n }\n return { name: value };\n}\n\n// loadedWorkloads collapses the loaded resources into the two membership sets a\n// strict picker validates a value against: `keys` are the full\n// `[namespace/]kind/name` keys the picker emits, `names` are the bare name-parts\n// (service name / ingress host) a consumer may persist instead. A value in\n// neither set names a workload that doesn't exist in the namespace.\nexport function loadedWorkloads(\n namespace: string | undefined,\n byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>,\n kinds: WorkloadKind[],\n): { keys: Set<string>; names: Set<string> } {\n const keys = new Set<string>();\n const names = new Set<string>();\n for (const kind of kinds) {\n for (const r of byKind[kind] ?? []) {\n keys.add(workloadKey(namespace, kind, r));\n names.add(workloadName(kind, r));\n }\n }\n return { keys, names };\n}\n\n// kindForValue returns the kind encoded in the selected value, so the lead icon\n// can mirror it without consulting the loaded resources. Falls back to the first\n// offered kind when nothing is selected or the value carries no recognised kind.\nexport function kindForValue(kinds: WorkloadKind[], value: string): WorkloadKind {\n const kind = value ? parseWorkloadKey(value).kind : undefined;\n return kind ?? kinds[0] ?? \"service\";\n}\n\n// buildWorkloadOptions flattens the per-kind resources into grouped Combobox\n// options in kind order. Each option's value is its `[namespace/]kind/name` key\n// and its label the human name; a current value not present in any kind is\n// pinned first (labelled with just its name-part) so the selection stays visible.\nexport function buildWorkloadOptions(\n namespace: string | undefined,\n byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>,\n kinds: WorkloadKind[],\n value: string,\n): ComboboxOption[] {\n const opts: ComboboxOption[] = [];\n for (const kind of kinds) {\n const meta = WORKLOAD_META[kind];\n const Icon = meta.Icon;\n for (const r of byKind[kind] ?? []) {\n const host = ingressHost(kind, r);\n opts.push({\n value: workloadKey(namespace, kind, r),\n // The label is the human name; an ingress pairs its host with the\n // ingress name for context.\n label: host ? `${host} (${r.name})` : r.name,\n group: meta.label,\n icon: <Icon className=\"h-4 w-4\" />,\n });\n }\n }\n if (value && !opts.some((o) => o.value === value)) {\n const parsed = parseWorkloadKey(value);\n const Icon = WORKLOAD_META[parsed.kind ?? kinds[0] ?? \"service\"].Icon;\n return [{ value, label: parsed.name, icon: <Icon className=\"h-4 w-4\" /> }, ...opts];\n }\n return opts;\n}\n"],"names":["K8SService","K8SIngress","K8SDeployment","K8SStatefulset","useState","useEffect","useMemo","cn","jsx","Combobox"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ComboboxOption } from './Combobox';
|
|
2
|
+
export type WorkloadKind = "service" | "ingress" | "deployment" | "statefulset";
|
|
3
|
+
/** One workload returned by the loader. Ingresses may carry rule hostnames. */
|
|
4
|
+
export type WorkloadResource = {
|
|
5
|
+
name: string;
|
|
6
|
+
hosts?: string[];
|
|
7
|
+
};
|
|
8
|
+
export type WorkloadPickerProps = {
|
|
9
|
+
/**
|
|
10
|
+
* Controlled selected value — a `[namespace/]kind/name` key (see
|
|
11
|
+
* {@link workloadKey} / {@link parseWorkloadKey}).
|
|
12
|
+
*/
|
|
13
|
+
value: string;
|
|
14
|
+
/** Called with the chosen `[namespace/]kind/name` key (or "" when cleared). */
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Async getter the component calls to load the requested kinds' workloads.
|
|
18
|
+
* Returns a map keyed by kind. The consumer owns fetching/caching.
|
|
19
|
+
*/
|
|
20
|
+
loadWorkloads: (kinds: WorkloadKind[]) => Promise<Record<WorkloadKind, WorkloadResource[]>>;
|
|
21
|
+
/**
|
|
22
|
+
* Namespace the workloads live in. When set it prefixes the emitted key
|
|
23
|
+
* (`namespace/kind/name`), so values stay distinct across namespaces.
|
|
24
|
+
*/
|
|
25
|
+
namespace?: string;
|
|
26
|
+
/** Kinds to offer, in display order. Defaults to all four. */
|
|
27
|
+
kinds?: WorkloadKind[];
|
|
28
|
+
/**
|
|
29
|
+
* When true, a non-empty value that does not match any loaded workload (once
|
|
30
|
+
* loading settles) is flagged invalid — the picked workload doesn't exist in
|
|
31
|
+
* the namespace. The value is still shown (pinned) so the user can see and
|
|
32
|
+
* fix it; freeform entry is not rejected, only marked.
|
|
33
|
+
*/
|
|
34
|
+
strict?: boolean;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function WorkloadPicker({ value, onChange, loadWorkloads, namespace, kinds, strict, placeholder, className, }: WorkloadPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export type ParsedWorkloadKey = {
|
|
40
|
+
namespace?: string;
|
|
41
|
+
kind?: WorkloadKind;
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function workloadKey(namespace: string | undefined, kind: WorkloadKind, r: WorkloadResource): string;
|
|
45
|
+
export declare function parseWorkloadKey(value: string): ParsedWorkloadKey;
|
|
46
|
+
export declare function loadedWorkloads(namespace: string | undefined, byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>, kinds: WorkloadKind[]): {
|
|
47
|
+
keys: Set<string>;
|
|
48
|
+
names: Set<string>;
|
|
49
|
+
};
|
|
50
|
+
export declare function kindForValue(kinds: WorkloadKind[], value: string): WorkloadKind;
|
|
51
|
+
export declare function buildWorkloadOptions(namespace: string | undefined, byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>, kinds: WorkloadKind[], value: string): ComboboxOption[];
|
|
52
|
+
//# sourceMappingURL=WorkloadPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkloadPicker.d.ts","sourceRoot":"","sources":["../../src/components/WorkloadPicker.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAgB3D,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,aAAa,CAAC;AAEhF,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAgBlE,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;OAGG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAC5F;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,SAAS,EACT,KAAiB,EACjB,MAAc,EACd,WAA0C,EAC1C,SAAS,GACV,EAAE,mBAAmB,2CAkErB;AAeD,MAAM,MAAM,iBAAiB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAO1F,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,EAAE,YAAY,EAClB,CAAC,EAAE,gBAAgB,GAClB,MAAM,CAGR;AAOD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAWjE;AAOD,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC,CAAC,EACzD,KAAK,EAAE,YAAY,EAAE,GACpB;IAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAU3C;AAKD,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAG/E;AAMD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC,CAAC,EACzD,KAAK,EAAE,YAAY,EAAE,EACrB,KAAK,EAAE,MAAM,GACZ,cAAc,EAAE,CAuBlB"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { K8SStatefulset, K8SDeployment, K8SIngress, K8SService } from "@flanksource/icons/mi";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Combobox } from "./Combobox.js";
|
|
6
|
+
const WORKLOAD_META = {
|
|
7
|
+
service: { label: "Service", Icon: K8SService },
|
|
8
|
+
ingress: { label: "Ingress", Icon: K8SIngress },
|
|
9
|
+
deployment: { label: "Deployment", Icon: K8SDeployment },
|
|
10
|
+
statefulset: { label: "StatefulSet", Icon: K8SStatefulset }
|
|
11
|
+
};
|
|
12
|
+
const ALL_KINDS = ["service", "ingress", "deployment", "statefulset"];
|
|
13
|
+
function WorkloadPicker({
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
loadWorkloads,
|
|
17
|
+
namespace,
|
|
18
|
+
kinds = ALL_KINDS,
|
|
19
|
+
strict = false,
|
|
20
|
+
placeholder = "Select workload / service…",
|
|
21
|
+
className
|
|
22
|
+
}) {
|
|
23
|
+
const [byKind, setByKind] = useState({});
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const kindsKey = kinds.join(",");
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
loadWorkloads(kinds).then((res) => {
|
|
30
|
+
if (!cancelled) setByKind(res);
|
|
31
|
+
}).finally(() => {
|
|
32
|
+
if (!cancelled) setLoading(false);
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true;
|
|
36
|
+
};
|
|
37
|
+
}, [loadWorkloads, kindsKey]);
|
|
38
|
+
const options = useMemo(
|
|
39
|
+
() => buildWorkloadOptions(namespace, byKind, kinds, value),
|
|
40
|
+
[namespace, byKind, kinds, value]
|
|
41
|
+
);
|
|
42
|
+
const invalid = useMemo(() => {
|
|
43
|
+
if (!strict || !value || loading) return false;
|
|
44
|
+
const { keys, names } = loadedWorkloads(namespace, byKind, kinds);
|
|
45
|
+
return !keys.has(value) && !names.has(value);
|
|
46
|
+
}, [strict, value, loading, namespace, byKind, kinds]);
|
|
47
|
+
const selectedKind = kindForValue(kinds, value);
|
|
48
|
+
const leadMeta = WORKLOAD_META[selectedKind];
|
|
49
|
+
const LeadIcon = leadMeta.Icon;
|
|
50
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
|
|
51
|
+
/* @__PURE__ */ jsx(
|
|
52
|
+
LeadIcon,
|
|
53
|
+
{
|
|
54
|
+
className: "h-4 w-4 shrink-0 text-muted-foreground",
|
|
55
|
+
title: leadMeta.label,
|
|
56
|
+
"aria-label": leadMeta.label
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
60
|
+
Combobox,
|
|
61
|
+
{
|
|
62
|
+
options,
|
|
63
|
+
value,
|
|
64
|
+
onChange,
|
|
65
|
+
allowCustomValue: true,
|
|
66
|
+
loading,
|
|
67
|
+
invalid,
|
|
68
|
+
placeholder
|
|
69
|
+
}
|
|
70
|
+
) })
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
function ingressHost(kind, r) {
|
|
74
|
+
var _a;
|
|
75
|
+
return kind === "ingress" ? (_a = r.hosts) == null ? void 0 : _a[0] : void 0;
|
|
76
|
+
}
|
|
77
|
+
function workloadName(kind, r) {
|
|
78
|
+
return ingressHost(kind, r) ?? r.name;
|
|
79
|
+
}
|
|
80
|
+
const KNOWN_KINDS = /* @__PURE__ */ new Set(["service", "ingress", "deployment", "statefulset"]);
|
|
81
|
+
function workloadKey(namespace, kind, r) {
|
|
82
|
+
const name = workloadName(kind, r);
|
|
83
|
+
return namespace ? `${namespace}/${kind}/${name}` : `${kind}/${name}`;
|
|
84
|
+
}
|
|
85
|
+
function parseWorkloadKey(value) {
|
|
86
|
+
const parts = value.split("/");
|
|
87
|
+
const name = parts.at(-1);
|
|
88
|
+
const maybeKind = parts.at(-2);
|
|
89
|
+
if (name != null && maybeKind != null && KNOWN_KINDS.has(maybeKind)) {
|
|
90
|
+
const namespace = parts.slice(0, -2).join("/");
|
|
91
|
+
const parsed = { kind: maybeKind, name };
|
|
92
|
+
if (namespace) parsed.namespace = namespace;
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
return { name: value };
|
|
96
|
+
}
|
|
97
|
+
function loadedWorkloads(namespace, byKind, kinds) {
|
|
98
|
+
const keys = /* @__PURE__ */ new Set();
|
|
99
|
+
const names = /* @__PURE__ */ new Set();
|
|
100
|
+
for (const kind of kinds) {
|
|
101
|
+
for (const r of byKind[kind] ?? []) {
|
|
102
|
+
keys.add(workloadKey(namespace, kind, r));
|
|
103
|
+
names.add(workloadName(kind, r));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { keys, names };
|
|
107
|
+
}
|
|
108
|
+
function kindForValue(kinds, value) {
|
|
109
|
+
const kind = value ? parseWorkloadKey(value).kind : void 0;
|
|
110
|
+
return kind ?? kinds[0] ?? "service";
|
|
111
|
+
}
|
|
112
|
+
function buildWorkloadOptions(namespace, byKind, kinds, value) {
|
|
113
|
+
const opts = [];
|
|
114
|
+
for (const kind of kinds) {
|
|
115
|
+
const meta = WORKLOAD_META[kind];
|
|
116
|
+
const Icon = meta.Icon;
|
|
117
|
+
for (const r of byKind[kind] ?? []) {
|
|
118
|
+
const host = ingressHost(kind, r);
|
|
119
|
+
opts.push({
|
|
120
|
+
value: workloadKey(namespace, kind, r),
|
|
121
|
+
// The label is the human name; an ingress pairs its host with the
|
|
122
|
+
// ingress name for context.
|
|
123
|
+
label: host ? `${host} (${r.name})` : r.name,
|
|
124
|
+
group: meta.label,
|
|
125
|
+
icon: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (value && !opts.some((o) => o.value === value)) {
|
|
130
|
+
const parsed = parseWorkloadKey(value);
|
|
131
|
+
const Icon = WORKLOAD_META[parsed.kind ?? kinds[0] ?? "service"].Icon;
|
|
132
|
+
return [{ value, label: parsed.name, icon: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" }) }, ...opts];
|
|
133
|
+
}
|
|
134
|
+
return opts;
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
WorkloadPicker,
|
|
138
|
+
buildWorkloadOptions,
|
|
139
|
+
kindForValue,
|
|
140
|
+
loadedWorkloads,
|
|
141
|
+
parseWorkloadKey,
|
|
142
|
+
workloadKey
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=WorkloadPicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkloadPicker.js","sources":["../../src/components/WorkloadPicker.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ComponentType } from \"react\";\nimport {\n K8SService,\n K8SIngress,\n K8SDeployment,\n K8SStatefulset,\n} from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// WorkloadPicker selects a backing Kubernetes workload (Service / Ingress /\n// Deployment / StatefulSet) for an endpoint. Options from every kind are merged\n// into one Combobox, grouped by kind via the Combobox group-header support and\n// labelled with the kind's icon.\n//\n// The emitted value is a `[namespace/]kind/name` key (see workloadKey /\n// parseWorkloadKey) so two workloads of different kinds that share a name don't\n// collide. An ingress's name-part is its first host (the routable address),\n// labelled with the ingress name for context; every other kind uses its name.\n//\n// The component is presentational: it owns the kind→group/icon mapping and the\n// key encoding but fetches nothing — the consumer supplies an async\n// `loadWorkloads` getter.\n\nexport type WorkloadKind = \"service\" | \"ingress\" | \"deployment\" | \"statefulset\";\n\n/** One workload returned by the loader. Ingresses may carry rule hostnames. */\nexport type WorkloadResource = { name: string; hosts?: string[] };\n\ntype KindIconProps = { className?: string; title?: string; \"aria-label\"?: string };\ntype KindMeta = { label: string; Icon: ComponentType<KindIconProps> };\n\n// WORKLOAD_META maps each kind to its group header label and icon, in display\n// order. The order of the keys drives the order options/groups appear in.\nconst WORKLOAD_META: Record<WorkloadKind, KindMeta> = {\n service: { label: \"Service\", Icon: K8SService },\n ingress: { label: \"Ingress\", Icon: K8SIngress },\n deployment: { label: \"Deployment\", Icon: K8SDeployment },\n statefulset: { label: \"StatefulSet\", Icon: K8SStatefulset },\n};\n\nconst ALL_KINDS: WorkloadKind[] = [\"service\", \"ingress\", \"deployment\", \"statefulset\"];\n\nexport type WorkloadPickerProps = {\n /**\n * Controlled selected value — a `[namespace/]kind/name` key (see\n * {@link workloadKey} / {@link parseWorkloadKey}).\n */\n value: string;\n /** Called with the chosen `[namespace/]kind/name` key (or \"\" when cleared). */\n onChange: (value: string) => void;\n /**\n * Async getter the component calls to load the requested kinds' workloads.\n * Returns a map keyed by kind. The consumer owns fetching/caching.\n */\n loadWorkloads: (kinds: WorkloadKind[]) => Promise<Record<WorkloadKind, WorkloadResource[]>>;\n /**\n * Namespace the workloads live in. When set it prefixes the emitted key\n * (`namespace/kind/name`), so values stay distinct across namespaces.\n */\n namespace?: string;\n /** Kinds to offer, in display order. Defaults to all four. */\n kinds?: WorkloadKind[];\n /**\n * When true, a non-empty value that does not match any loaded workload (once\n * loading settles) is flagged invalid — the picked workload doesn't exist in\n * the namespace. The value is still shown (pinned) so the user can see and\n * fix it; freeform entry is not rejected, only marked.\n */\n strict?: boolean;\n placeholder?: string;\n className?: string;\n};\n\nexport function WorkloadPicker({\n value,\n onChange,\n loadWorkloads,\n namespace,\n kinds = ALL_KINDS,\n strict = false,\n placeholder = \"Select workload / service…\",\n className,\n}: WorkloadPickerProps) {\n const [byKind, setByKind] = useState<Partial<Record<WorkloadKind, WorkloadResource[]>>>({});\n const [loading, setLoading] = useState(false);\n\n const kindsKey = kinds.join(\",\");\n useEffect(() => {\n let cancelled = false;\n setLoading(true);\n loadWorkloads(kinds)\n .then((res) => {\n if (!cancelled) setByKind(res);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n // loadWorkloads is expected to be stable (memoized by the consumer); kindsKey\n // is the joined `kinds` so a changed selection reloads without depending on\n // the array's identity.\n }, [loadWorkloads, kindsKey]);\n\n const options = useMemo(\n () => buildWorkloadOptions(namespace, byKind, kinds, value),\n [namespace, byKind, kinds, value],\n );\n\n // In strict mode a non-empty value is invalid once loading settles unless it\n // matches a loaded workload — either by its full `[namespace/]kind/name` key\n // (what the picker emits) or by its bare name-part (what consumers that\n // persist only the resolved name, e.g. a Service name or ingress host, feed\n // back in). buildWorkloadOptions pins an unmatched value first, so test\n // membership against the loaded set, not `options`.\n const invalid = useMemo(() => {\n if (!strict || !value || loading) return false;\n const { keys, names } = loadedWorkloads(namespace, byKind, kinds);\n return !keys.has(value) && !names.has(value);\n }, [strict, value, loading, namespace, byKind, kinds]);\n\n // The lead icon reflects the selected workload's kind (read from the value's\n // key), falling back to the first offered kind when nothing is selected or the\n // value carries no recognised kind (e.g. freeform input).\n const selectedKind = kindForValue(kinds, value);\n const leadMeta = WORKLOAD_META[selectedKind];\n const LeadIcon = leadMeta.Icon;\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <LeadIcon\n className=\"h-4 w-4 shrink-0 text-muted-foreground\"\n title={leadMeta.label}\n aria-label={leadMeta.label}\n />\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={options}\n value={value}\n onChange={onChange}\n allowCustomValue\n loading={loading}\n invalid={invalid}\n placeholder={placeholder}\n />\n </div>\n </div>\n );\n}\n\n// ingressHost is the first host an ingress routes, or undefined for non-ingress\n// kinds and host-less ingresses. An ingress's name-part is its host.\nfunction ingressHost(kind: WorkloadKind, r: WorkloadResource): string | undefined {\n return kind === \"ingress\" ? r.hosts?.[0] : undefined;\n}\n\n// workloadName is the name-part of a resource's key: an ingress's first host\n// (the routable address callers want), else the resource's own name.\nfunction workloadName(kind: WorkloadKind, r: WorkloadResource): string {\n return ingressHost(kind, r) ?? r.name;\n}\n\n// ParsedWorkloadKey is the structured form of a `[namespace/]kind/name` value.\nexport type ParsedWorkloadKey = { namespace?: string; kind?: WorkloadKind; name: string };\n\nconst KNOWN_KINDS = new Set<string>([\"service\", \"ingress\", \"deployment\", \"statefulset\"]);\n\n// workloadKey is the value a resource contributes (what onChange emits): a\n// `[namespace/]kind/name` composite so two workloads of different kinds sharing\n// a name don't collide. namespace is omitted when not supplied.\nexport function workloadKey(\n namespace: string | undefined,\n kind: WorkloadKind,\n r: WorkloadResource,\n): string {\n const name = workloadName(kind, r);\n return namespace ? `${namespace}/${kind}/${name}` : `${kind}/${name}`;\n}\n\n// parseWorkloadKey splits a `[namespace/]kind/name` value back into its parts.\n// The trailing segment is always the name (a host may itself contain no slash);\n// the segment before it is the kind when it is a known workload kind, and any\n// remaining leading segment is the namespace. A bare value (no recognised\n// kind/name shape) is returned as just its name so freeform input survives.\nexport function parseWorkloadKey(value: string): ParsedWorkloadKey {\n const parts = value.split(\"/\");\n const name = parts.at(-1);\n const maybeKind = parts.at(-2);\n if (name != null && maybeKind != null && KNOWN_KINDS.has(maybeKind)) {\n const namespace = parts.slice(0, -2).join(\"/\");\n const parsed: ParsedWorkloadKey = { kind: maybeKind as WorkloadKind, name };\n if (namespace) parsed.namespace = namespace;\n return parsed;\n }\n return { name: value };\n}\n\n// loadedWorkloads collapses the loaded resources into the two membership sets a\n// strict picker validates a value against: `keys` are the full\n// `[namespace/]kind/name` keys the picker emits, `names` are the bare name-parts\n// (service name / ingress host) a consumer may persist instead. A value in\n// neither set names a workload that doesn't exist in the namespace.\nexport function loadedWorkloads(\n namespace: string | undefined,\n byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>,\n kinds: WorkloadKind[],\n): { keys: Set<string>; names: Set<string> } {\n const keys = new Set<string>();\n const names = new Set<string>();\n for (const kind of kinds) {\n for (const r of byKind[kind] ?? []) {\n keys.add(workloadKey(namespace, kind, r));\n names.add(workloadName(kind, r));\n }\n }\n return { keys, names };\n}\n\n// kindForValue returns the kind encoded in the selected value, so the lead icon\n// can mirror it without consulting the loaded resources. Falls back to the first\n// offered kind when nothing is selected or the value carries no recognised kind.\nexport function kindForValue(kinds: WorkloadKind[], value: string): WorkloadKind {\n const kind = value ? parseWorkloadKey(value).kind : undefined;\n return kind ?? kinds[0] ?? \"service\";\n}\n\n// buildWorkloadOptions flattens the per-kind resources into grouped Combobox\n// options in kind order. Each option's value is its `[namespace/]kind/name` key\n// and its label the human name; a current value not present in any kind is\n// pinned first (labelled with just its name-part) so the selection stays visible.\nexport function buildWorkloadOptions(\n namespace: string | undefined,\n byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>,\n kinds: WorkloadKind[],\n value: string,\n): ComboboxOption[] {\n const opts: ComboboxOption[] = [];\n for (const kind of kinds) {\n const meta = WORKLOAD_META[kind];\n const Icon = meta.Icon;\n for (const r of byKind[kind] ?? []) {\n const host = ingressHost(kind, r);\n opts.push({\n value: workloadKey(namespace, kind, r),\n // The label is the human name; an ingress pairs its host with the\n // ingress name for context.\n label: host ? `${host} (${r.name})` : r.name,\n group: meta.label,\n icon: <Icon className=\"h-4 w-4\" />,\n });\n }\n }\n if (value && !opts.some((o) => o.value === value)) {\n const parsed = parseWorkloadKey(value);\n const Icon = WORKLOAD_META[parsed.kind ?? kinds[0] ?? \"service\"].Icon;\n return [{ value, label: parsed.name, icon: <Icon className=\"h-4 w-4\" /> }, ...opts];\n }\n return opts;\n}\n"],"names":[],"mappings":"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"}
|