UI configurator — maintainer's guide
This document explains the full internals of the configurator: how the form is built,
how state flows to values.yaml, and how to add, edit, or remove any piece of the UI —
including workload sections, individual fields, shared resources, and chart variants.
Architecture overview
Chart config file React state (App.jsx) values.yaml output
(charts/*.js) newWorkload() / useState toValues.js
│ │ │
▼ ▼ ▼
workloadSections[] ──► WorkloadForm.jsx ──► toValues(state) ──► YAML string
type:'section' SectionPanel.jsx
type:'custom' WorkloadForm.jsx
(CustomSection)
Data-flow in one sentence: the chart config describes what fields to show; React state holds the current values; toValues.js converts state into the values dict; js-yaml serialises it into the YAML preview and download.
Key files
| File | Role |
|---|---|
src/app/charts/base-chart-1.0.0.js | Section + field config for v1.0.0 |
src/app/charts/base-chart-*.js | Variant configs (extend the base) |
src/app/charts/index.js | Registry — CHARTS map + DEFAULT_CHART |
src/app/App.jsx | All React state, newWorkload(), prefillFromValues(), shared-resources UI |
src/app/WorkloadForm.jsx | Renders workload sections; CustomSection for complex ones |
src/app/components/SectionPanel.jsx | Generic accordion renderer for type:'section' entries |
src/app/components/FieldRenderer.jsx | Renders a single field descriptor; exports getPath / setPath |
src/app/toValues.js | Converts React state → plain JS object → written as YAML |
Part 1 — Workload sections
1.1 How sections are rendered
WorkloadForm.jsx iterates chart.workloadSections for the active workload:
chart.workloadSections
for each section:
if section.showFor set and wl.type not in showFor → skip
if section.type === 'custom' → CustomSection (hand-coded JSX in WorkloadForm.jsx)
if section.type === 'section' → SectionPanel (generic config-driven renderer)
SectionPanel renders:
- an
<details>accordion withsection.labelas the summary - an optional Enable checkbox if
section.enabledFieldis set - all
section.fields[]— non-YAML fields are grouped side-by-side in.rowdivs; YAML fields are always full-width
FieldRenderer handles the actual input elements. It reads/writes the workload object using getPath(wl, field.key) and setPath(wl, field.key, value) — dot-notation paths (e.g. 'hpa.yaml' → wl.hpa.yaml).
1.2 Editing an existing config-driven section
Goal: change a field label, placeholder, row count, or add an option to a select.
Open the relevant chart config file (src/app/charts/base-chart-1.0.0.js) and edit the
field descriptor inside fields[]. No other file needs to change.
// Before
{ key: 'pdb.yaml', label: 'PDB spec (YAML)', type: 'yaml', rows: 2,
placeholder: 'minAvailable: 1' }
// After — more rows, updated placeholder
{ key: 'pdb.yaml', label: 'PDB spec (YAML)', type: 'yaml', rows: 4,
placeholder: 'minAvailable: 1\n# or:\n# maxUnavailable: 1' }
Field descriptor properties:
| Property | Required | Values | Notes |
|---|---|---|---|
key | ✓ | dot-notation path | e.g. 'hpa.yaml', 'scc.name' |
label | ✓ | string | Shown above the input |
type | ✓ | text number select checkbox yaml | Controls which input is rendered |
placeholder | — | string | Shown when the field is empty |
tip | — | string | ? tooltip text |
rows | — | number | textarea height (yaml type only, default 3) |
options | ✓ for select | string[] | e.g. ['a', 'b', 'c'] |
min | — | number | Minimum value (number type only) |
advanced | — | boolean | If true, field appears under "Advanced" sub-accordion |
1.3 Adding a field to an existing config-driven section
Step 1 — add the field to the chart config
// src/app/charts/base-chart-1.0.0.js
{
key: 'labels', type: 'section',
label: '🏷 Labels',
fields: [
{ key: 'labels', label: 'Labels (YAML)', type: 'yaml', rows: 3 },
{ key: 'podLabels', label: 'Pod labels (YAML)', type: 'yaml', rows: 3 },
// ── new field ──────────────────────────────────────────────────────────
{ key: 'annotations', label: 'Annotations (YAML)', type: 'yaml', rows: 3,
placeholder: 'prometheus.io/scrape: "true"' },
],
},
Step 2 — add the field's initial value to newWorkload() in App.jsx
// App.jsx — newWorkload()
const newWorkload = (id) => ({
// ... existing fields ...
labels: '', podLabels: '',
annotations: '', // ← new
})
Step 3 — emit the value in toValues.js → buildWorkload()
// toValues.js — inside buildWorkload(wl)
const annotations = parseYaml(wl.annotations)
if (annotations) w.annotations = annotations
Step 4 — handle prefill in prefillFromValues() in App.jsx
Add the reverse mapping so "Upload values.yaml" repopulates the field:
// App.jsx — inside prefillWorkload(w, type, id)
annotations: w.annotations ? dumpOrEmpty(w.annotations) : '',
1.4 Removing a field from a config-driven section
- Delete the field descriptor from
fields[]in the chart config. - Remove the matching key from
newWorkload()inApp.jsx. - Remove the matching emit block from
buildWorkload()intoValues.js. - Remove the matching line from
prefillWorkload()inApp.jsx.
Tip: if a field is only used in one chart variant but not others, remove it only from that variant's config file. No JS or JSX changes needed.
1.5 Adding a brand-new config-driven section
This covers sections with an Enable checkbox that gates a YAML textarea — the most common pattern.
Step 1 — add the section to the chart config
// src/app/charts/base-chart-1.0.0.js — inside workloadSections[]
{
key: 'topologySpread', type: 'section',
label: '🌐 Topology Spread Constraints',
tip: 'Spread pods evenly across zones or nodes',
showFor: ['Deployment', 'StatefulSet'], // omit to show for all types
enabledField: 'topologySpread.enabled', // drives the Enable checkbox
fields: [
{ key: 'topologySpread.yaml', label: 'Constraints (YAML)', type: 'yaml', rows: 6,
placeholder: '- maxSkew: 1\n topologyKey: topology.kubernetes.io/zone\n whenUnsatisfiable: DoNotSchedule\n labelSelector:\n matchLabels: {}' },
],
},
Step 2 — add initial state to newWorkload() in App.jsx
const newWorkload = (id) => ({
// ... existing fields ...
topologySpread: { enabled: false, yaml: '' },
})
Step 3 — emit the output in buildWorkload() in toValues.js
// toValues.js — inside buildWorkload(wl)
if (wl.type !== 'CronJob' && wl.topologySpread.enabled) {
const spec = parseYaml(wl.topologySpread.yaml) || {}
w.topologySpreadConstraints = asList(spec) || []
}
Step 4 — add prefill in prefillFromValues() in App.jsx
// App.jsx — inside prefillWorkload(w, type, id)
const tsc = w.topologySpreadConstraints // whatever the Helm chart key is
topologySpread: {
enabled: Array.isArray(tsc) && tsc.length > 0,
yaml: tsc ? dumpOrEmpty(tsc) : '',
},
1.6 Removing an entire section
- Delete the section entry from
workloadSections[]in the chart config — the section disappears from the UI immediately. - Optionally remove the state keys from
newWorkload(), the emit block fromtoValues.js, and the prefill block fromApp.jsx(safe to leave as dead code).
To remove a section only from a specific chart variant without touching others:
// src/app/charts/base-chart-1.1.0.js
import baseChart from './base-chart-1.0.0.js'
export default {
...baseChart,
name: 'base-chart v1.1.0',
workloadSections: baseChart.workloadSections.filter(s => s.key !== 'pdb'),
}
1.7 Custom sections (type:'custom')
Custom sections are hand-coded JSX inside CustomSection in WorkloadForm.jsx.
They are used when the layout is too complex for the generic renderer (e.g. the image
section has two rows with flex ratios, the schedule section has a multi-key YAML blob).
Current custom sections and their state keys:
sectionKey | State keys used | Description |
|---|---|---|
header | name, type, replicas | Name/type/replicas row |
image | image.repository, image.tag, image.digest, image.pullPolicy | Image accordion |
schedule | scheduleYaml | CronJob schedule + policy YAML |
route | route.enabled, route.yaml | OpenShift Route |
healthProbes | probesYaml | Liveness/readiness/startup probes YAML |
advanced | podSec, secCtx, volMounts, extraVols, sidecars, initCtrs, nodeSel, tolerations, affinity, grace | Advanced accordion |
To add a new custom section:
- Add
{ key: 'mySectionKey', type: 'custom', showFor: [...] }toworkloadSections[]in the chart config. - Add a new
if (sectionKey === 'mySectionKey') { return <JSX /> }block insideCustomSectioninWorkloadForm.jsx. - Add state keys to
newWorkload()inApp.jsx. - Add emit logic to
buildWorkload()intoValues.js. - Add prefill logic to
prefillWorkload()inApp.jsx.
To modify an existing custom section (e.g. add a field to the image section):
Edit the matching if block inside CustomSection in WorkloadForm.jsx. Use
setNested(key, patch) to update a nested object (e.g. setNested('image', { newField: val }))
or set(patch) for top-level keys.
Part 2 — Shared resources
Shared resources are rendered directly in App.jsx (below the workload tabs).
Each resource type is a useState array of objects with a stable id counter.
2.1 How shared resources work
App.jsx toValues.js
─────────────────────────────────────────────────────
useState([]) ← configMaps configMaps[] → values.configMaps
useState([]) ← secrets secrets[] → values.secrets
useState([]) ← sealedSecrets sealedSecrets[] → values.sealedSecrets
useState([]) ← pvcs pvcs[] → values.persistentVolumeClaims
string ← persistentVolumes (raw) PVs YAML list → values.persistentVolumes
string ← extraManifests (raw) EM YAML list → values.extraManifests
Anatomy of a list resource:
- Each item has a stable
id(from a counter likecmNext). IDs are never reused. - Every item has a
nametext field. - Additional fields are either structured inputs (text/select) or a single YAML textarea.
- The "Remove" button filters the item out of state by
id. - The "+ Add …" button appends a
newXxx(counter)default object and increments the counter.
2.2 Current shared resource state shapes
| Resource | State shape | name → values key | Extra state → values key |
|---|---|---|---|
| ConfigMap | { id, name, data } | configMaps[].name | data (YAML) → data |
| Secret | { id, name, yaml } | secrets[].name | yaml (YAML) → spread into item |
| Sealed Secret | { id, name, data } | sealedSecrets[].name | data (YAML) → encryptedData |
| PVC | { id, name, yaml } | persistentVolumeClaims[].name | yaml (YAML) → spread into item |
Secrets YAML example — the textarea holds the full spec excluding name:
type: kubernetes.io/tls
stringData:
tls.crt: |
-----BEGIN CERTIFICATE-----
...
tls.key: |
-----BEGIN PRIVATE KEY-----
...
PVC YAML example:
size: 10Gi
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
2.3 Editing a field in an existing shared resource type
All shared-resource rendering is in App.jsx in the {/* Shared Resources */} section.
Find the <details> block for the resource (search for Secrets, PVCs, etc.)
and edit the JSX directly.
To change the placeholder for the PVC YAML textarea:
// App.jsx — inside the PVCs map
<textarea rows={3} value={p.yaml}
placeholder={"size: 1Gi\nstorageClassName: standard\naccessModes:\n - ReadWriteOnce"}
onChange={...} />
Change the placeholder string. No other file is affected.
2.4 Adding a field to an existing shared resource type
Example: add an optional immutable: true checkbox to Secrets.
Step 1 — update newSecret() in App.jsx
const newSecret = (id) => ({ id, name: '', yaml: '', immutable: false })
Step 2 — add the input to the Secrets JSX block in App.jsx
<label className="block" style={{ marginTop: '.5rem' }}>
<input type="checkbox" checked={s.immutable}
onChange={e => setSecrets(prev =>
prev.map(x => x.id === s.id ? { ...x, immutable: e.target.checked } : x)
)} />
{' '}Immutable
</label>
Step 3 — emit the value in toValues.js
// toValues.js — secrets map
.map(s => clean({ name: s.name, immutable: s.immutable || null, ...parseYaml(s.yaml) }))
Step 4 — handle prefill in prefillFromValues() in App.jsx
secrets: (v.secrets || []).map((s, i) => ({
id: i, name: s.name || '', immutable: !!s.immutable,
yaml: dumpOrEmpty({ type: s.type && s.type !== 'Opaque' ? s.type : undefined, stringData: s.stringData || undefined }),
})),
2.5 Adding a brand-new shared resource type
Example: add a NetworkPolicy list.
Step 1 — add state to App.jsx
// default factory
const newNetpol = (id) => ({ id, name: '', yaml: '' })
// inside App()
const [networkPolicies, setNetworkPolicies] = useState([])
const [netpolNext, setNetpolNext] = useState(0)
Step 2 — add the UI block in App.jsx (inside the shared-resources column)
<details>
<summary>🔒 Network Policies</summary>
<div className="dc">
{networkPolicies.map(np => (
<div key={np.id} className="resource-entry">
<div className="row" style={{ alignItems: 'flex-end' }}>
<div className="fg">
<label className="block">Name</label>
<input type="text" value={np.name} placeholder="allow-ingress"
onChange={e => setNetworkPolicies(prev =>
prev.map(x => x.id === np.id ? { ...x, name: e.target.value } : x))} />
</div>
<div className="fg" style={{ flex: '0 0 auto' }}>
<button className="danger"
onClick={() => setNetworkPolicies(prev => prev.filter(x => x.id !== np.id))}>
🗑 Remove
</button>
</div>
</div>
<div className="fg">
<label className="block">NetworkPolicy spec (YAML)</label>
<textarea rows={5} value={np.yaml}
placeholder={"podSelector:\n matchLabels:\n app: my-app\ningress:\n - from:\n - podSelector: {}"}
onChange={e => setNetworkPolicies(prev =>
prev.map(x => x.id === np.id ? { ...x, yaml: e.target.value } : x))} />
</div>
</div>
))}
<button onClick={() => {
setNetworkPolicies(prev => [...prev, newNetpol(netpolNext)])
setNetpolNext(n => n + 1)
}}>
+ Add Network Policy
</button>
</div>
</details>
Step 3 — pass it through toValues()
Add networkPolicies to the argument destructure and useMemo dependencies:
// App.jsx — useMemo
const values = useMemo(() => toValues({
workloads, configMaps, secrets, sealedSecrets, pvcs,
networkPolicies, // ← add
global, persistentVolumes, extraManifests,
}), [..., networkPolicies])
// App.jsx — handleApplyUpload
const handleApplyUpload = useCallback((valuesDict) => {
const state = prefillFromValues(valuesDict)
// ...
setNetworkPolicies(state.networkPolicies); setNetpolNext(state.networkPolicies.length)
}, [])
Step 4 — emit in toValues.js
// toValues.js — export function toValues(state)
export function toValues(state) {
const { workloads, configMaps, secrets, sealedSecrets, pvcs,
networkPolicies, // ← add
global, persistentVolumes, extraManifests } = state
// ...
const netpolList = networkPolicies
.filter(np => np.name.trim())
.map(np => clean({ name: np.name, ...parseYaml(np.yaml) }))
.filter(Boolean)
if (netpolList.length) values.networkPolicies = netpolList
Step 5 — add prefill in prefillFromValues() in App.jsx
// App.jsx — inside prefillFromValues(v)
return {
// ...
networkPolicies: (v.networkPolicies || []).map((np, i) => ({
id: i, name: np.name || '',
yaml: dumpOrEmpty({ podSelector: np.podSelector || undefined, ingress: np.ingress || undefined, egress: np.egress || undefined }),
})),
}
2.6 Removing a shared resource type
- Delete the
<details>…</details>block fromApp.jsx. - Remove the
useStateand counter for it. - Remove the matching entry from the
toValues()destructure + emit block intoValues.js. - Remove the matching entry from
prefillFromValues()return value inApp.jsx. - Remove it from the
useMemocall andhandleApplyUpload.
Part 3 — How state becomes values.yaml
3.1 The full pipeline
React state (strings/booleans/numbers)
│
▼ toValues(state) → toValues.js
│
▼ buildWorkload(wl) per workload
• parseYaml(wl.someSection.yaml) — YAML string → JS object
• asList(value) — single value or array → array
• clean({ ... }) — strips null/undefined/""/[]/{}
│
▼
plain JS object { deployments: [...], configMaps: [...], ... }
│
▼ yaml.dump(values) → Sidebar.jsx
│
▼ YAML string shown in preview / written to values.yaml
3.2 Helper functions in toValues.js
| Function | Signature | What it does |
|---|---|---|
clean(v) | (any) → any | undefined | Recursively removes null / undefined / '' / [] / {}. Used so only non-default values appear in the output YAML. |
parseYaml(str) | (string) → any | null | Parses a YAML string safely; returns null on empty or invalid input. |
asList(v) | (any) → array | null | Wraps a single value in an array; passes arrays through; null → null. |
3.3 The YAML textarea pattern (used everywhere)
Instead of individual structured fields, each section stores a single YAML string.
toValues.js parses it at serialisation time and spreads it into the output:
// state: wl.hpa = { enabled: true, yaml: 'minReplicas: 2\nmaxReplicas: 10' }
if (wl.type === 'Deployment' && wl.hpa.enabled) {
const spec = parseYaml(wl.hpa.yaml) || {}
w.autoscaling = clean({ enabled: true, ...spec })
// result: { enabled: true, minReplicas: 2, maxReplicas: 10 }
}
The user writes standard Kubernetes YAML; parseYaml turns it into a JS object;
spreading it into clean({}) keeps only valid, non-empty values.
3.4 The clean() guard
clean() prevents empty/default values from appearing in values.yaml. This means:
- An empty YAML textarea →
parseYamlreturnsnull→ spread is{}→cleanreturnsundefined→ the key is omitted from the output. replicaCountis omitted if it equals1(the chart default).pullPolicyis omitted if it equals'IfNotPresent'(the chart default).serviceAccount.createis omitted if it equalstrue(the chart default).
Always use clean() when emitting optional values so the generated values.yaml
only contains what the user actually changed.
3.5 prefillFromValues() — the reverse direction
When a user uploads an existing values.yaml, prefillFromValues() in App.jsx
converts the values dict back into React state. This is the mirror image of toValues.js.
values.yaml → yaml.load() → prefillFromValues(dict) → React state
For YAML textarea fields the reverse is dumpOrEmpty():
const dumpOrEmpty = (val) => val ? yaml.dump(val, { lineWidth: 120 }).trim() : ''
For sections with an enabled flag plus a spec blob, the dumpSpec helper strips
enabled before serialising:
const dumpSpec = (obj) => {
const { enabled, ...rest } = obj
return dumpOrEmpty(rest)
}
// usage:
hpa: { enabled: !!hpa.enabled, yaml: dumpSpec(hpa) },
Whenever you add a new field and emit it in toValues.js, you must also add the
matching prefill in prefillFromValues() or uploaded files will not restore that field.
Part 4 — Chart variants
4.1 Creating a new variant
Copy an existing config file and override what you need:
// src/app/charts/base-chart-custom.js
import baseChart from './base-chart-1.0.0.js'
export default {
...baseChart,
name: 'base-chart custom',
ociChartRef: 'oci://ghcr.io/my-org/helm/my-chart',
ociChartVersion: '3.0.0',
// Remove the PDB section, add a custom one
workloadSections: [
...baseChart.workloadSections.filter(s => s.key !== 'pdb'),
{
key: 'networkPolicy', type: 'section',
label: '🔒 Network Policy',
tip: 'Attach a NetworkPolicy to this workload',
showFor: ['Deployment', 'StatefulSet'],
enabledField: 'networkPolicy.enabled',
fields: [
{ key: 'networkPolicy.yaml', label: 'NetworkPolicy spec (YAML)', type: 'yaml', rows: 5 },
],
},
],
}
4.2 Registering a variant
Edit src/app/charts/index.js:
import baseChart_1_0_0 from './base-chart-1.0.0.js'
import baseChart_custom from './base-chart-custom.js'
export const CHARTS = {
'base-chart-1.0.0': baseChart_1_0_0,
'base-chart-custom': baseChart_custom,
}
export const DEFAULT_CHART = 'base-chart-1.0.0'
The chart selector in the sidebar appears automatically when CHARTS has more than one entry.
4.3 Variant config properties
| Property | Required | Description |
|---|---|---|
name | ✓ | Display name shown in the chart selector dropdown |
ociChartRef | ✓ | OCI reference used in the generated helm template command |
ociChartVersion | ✓ | Chart version for the helm template command |
workloadTypes | — | Array of allowed workload types (default: Deployment, StatefulSet, CronJob) |
workloadSections | ✓ | Ordered array of section descriptors shown in WorkloadForm |
Part 5 — Quick-reference checklist
Adding a field to a workload section
- Add field descriptor to
workloadSections[].fields[]in chart config - Add initial value to
newWorkload()inApp.jsx - Emit in
buildWorkload()intoValues.js - Prefill in
prefillWorkload()inApp.jsx
Adding a new config-driven workload section
- Add section descriptor to
workloadSections[]in chart config - Add initial state (e.g.
{ enabled: false, yaml: '' }) tonewWorkload()inApp.jsx - Emit in
buildWorkload()intoValues.js - Prefill in
prefillWorkload()inApp.jsx
Adding a new custom workload section
All of the above, plus:
- Add
if (sectionKey === '...')JSX block inCustomSectioninWorkloadForm.jsx
Adding a new shared resource type
- Add
newXxx()factory anduseState+ counter inApp.jsx - Add
<details>UI block inApp.jsx(shared-resources column) - Add to
useMemodeps andhandleApplyUploadinApp.jsx - Add emit block in
toValues()intoValues.js - Add prefill in
prefillFromValues()return value inApp.jsx
Removing anything
- Remove from chart config → section disappears immediately (no JS/JSX change if only in config)
- Remove state key from
newWorkload()→ field value is no longer stored - Remove from
toValues.js→ key no longer appears invalues.yaml - Remove from
prefillFromValues()→ uploaded files no longer restore the field
Part 6 — Common recipes
6.1 Add a workload section only for one chart
Goal: chart A has an extra section (e.g. NetworkPolicy), chart B does not.
-
Create/modify the chart file (e.g.
src/app/charts/base-chart-custom.js):import baseChart from './base-chart-1.0.0.js'
export default {
...baseChart,
name: 'base-chart custom',
workloadSections: [
// start from the base sections
...baseChart.workloadSections,
// append or override only what you need
{
key: 'networkPolicy',
type: 'section',
label: '🔒 Network Policy',
showFor: ['Deployment', 'StatefulSet'],
enabledField: 'networkPolicy.enabled',
fields: [
{ key: 'networkPolicy.yaml', label: 'NetworkPolicy spec (YAML)', type: 'yaml', rows: 5 },
],
},
],
} -
Wire state and mapping just like any other section:
newWorkload()inApp.jsx: addnetworkPolicy: { enabled: false, yaml: '' }.buildWorkload()intoValues.js: readwl.networkPolicyand emit tow.networkPolicy(or whatever key your chart uses).prefillFromValues()inApp.jsx: readw.networkPolicyand rebuild{ enabled, yaml }.
Only charts that import and register this custom config will see the section.
6.2 Add a new shared resource type (ResourceList-based)
Goal: add a new list under “Global & shared resources” (e.g. NetworkPolicies), using the same pattern as ConfigMaps/Secrets/PVCs.
-
Describe it in
resources-config.js:// src/app/resources-config.js
export const RESOURCE_SECTIONS = [
// ...existing sections...
{
key: 'networkPolicies',
label: '🔒 Network Policies',
tip: 'Kubernetes NetworkPolicies applied with this release',
addLabel: 'NetworkPolicy',
stateKey: 'networkPolicies',
nextIdKey: 'netpolNext',
createItem: (id) => ({ id, name: '', yaml: '' }),
fields: [
{ key: 'name', label: 'Name', type: 'text', placeholder: 'allow-ingress' },
{
key: 'yaml',
label: 'NetworkPolicy spec (YAML)',
type: 'yaml',
rows: 5,
placeholder: 'podSelector: {}\ningress: []',
},
],
},
] -
Add state and prefill in
App.jsx:// state
const [networkPolicies, setNetworkPolicies] = useState([])
const [netpolNext, setNetpolNext] = useState(0)
// in prefillFromValues(v) return:
networkPolicies: (v.networkPolicies || []).map((np, i) => ({
id: i,
name: np.name || '',
yaml: dumpOrEmpty({
podSelector: np.podSelector || undefined,
ingress: np.ingress || undefined,
egress: np.egress || undefined,
}),
})),App.jsxalready usesRESOURCE_SECTIONS+stateKey/nextIdKey, so you usually do not need to touch the JSX — the new resource will appear automatically. -
Emit it in
toValues.js:export function toValues(state) {
const { networkPolicies, /* ...existing keys... */ } = state
// ...
const netpolList = (networkPolicies || [])
.filter(np => np.name.trim())
.map(np => clean({ name: np.name, ...parseYaml(np.yaml) }))
.filter(Boolean)
if (netpolList.length) values.networkPolicies = netpolList
}
With this pattern, adding/editing/removing a shared resource type is:
- Config: one entry in
resources-config.js. - State/prefill: one mapping in
App.jsx. - values.yaml: one mapping in
toValues.js.