Skip to main content

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

FileRole
src/app/charts/base-chart-1.0.0.jsSection + field config for v1.0.0
src/app/charts/base-chart-*.jsVariant configs (extend the base)
src/app/charts/index.jsRegistry — CHARTS map + DEFAULT_CHART
src/app/App.jsxAll React state, newWorkload(), prefillFromValues(), shared-resources UI
src/app/WorkloadForm.jsxRenders workload sections; CustomSection for complex ones
src/app/components/SectionPanel.jsxGeneric accordion renderer for type:'section' entries
src/app/components/FieldRenderer.jsxRenders a single field descriptor; exports getPath / setPath
src/app/toValues.jsConverts 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 with section.label as the summary
  • an optional Enable checkbox if section.enabledField is set
  • all section.fields[] — non-YAML fields are grouped side-by-side in .row divs; 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:

PropertyRequiredValuesNotes
keydot-notation pathe.g. 'hpa.yaml', 'scc.name'
labelstringShown above the input
typetext number select checkbox yamlControls which input is rendered
placeholderstringShown when the field is empty
tipstring? tooltip text
rowsnumbertextarea height (yaml type only, default 3)
options✓ for selectstring[]e.g. ['a', 'b', 'c']
minnumberMinimum value (number type only)
advancedbooleanIf 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.jsbuildWorkload()

// 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

  1. Delete the field descriptor from fields[] in the chart config.
  2. Remove the matching key from newWorkload() in App.jsx.
  3. Remove the matching emit block from buildWorkload() in toValues.js.
  4. Remove the matching line from prefillWorkload() in App.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

  1. Delete the section entry from workloadSections[] in the chart config — the section disappears from the UI immediately.
  2. Optionally remove the state keys from newWorkload(), the emit block from toValues.js, and the prefill block from App.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:

sectionKeyState keys usedDescription
headername, type, replicasName/type/replicas row
imageimage.repository, image.tag, image.digest, image.pullPolicyImage accordion
schedulescheduleYamlCronJob schedule + policy YAML
routeroute.enabled, route.yamlOpenShift Route
healthProbesprobesYamlLiveness/readiness/startup probes YAML
advancedpodSec, secCtx, volMounts, extraVols, sidecars, initCtrs, nodeSel, tolerations, affinity, graceAdvanced accordion

To add a new custom section:

  1. Add { key: 'mySectionKey', type: 'custom', showFor: [...] } to workloadSections[] in the chart config.
  2. Add a new if (sectionKey === 'mySectionKey') { return <JSX /> } block inside CustomSection in WorkloadForm.jsx.
  3. Add state keys to newWorkload() in App.jsx.
  4. Add emit logic to buildWorkload() in toValues.js.
  5. Add prefill logic to prefillWorkload() in App.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 like cmNext). IDs are never reused.
  • Every item has a name text 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

ResourceState shapename → values keyExtra state → values key
ConfigMap{ id, name, data }configMaps[].namedata (YAML) → data
Secret{ id, name, yaml }secrets[].nameyaml (YAML) → spread into item
Sealed Secret{ id, name, data }sealedSecrets[].namedata (YAML) → encryptedData
PVC{ id, name, yaml }persistentVolumeClaims[].nameyaml (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

  1. Delete the <details>…</details> block from App.jsx.
  2. Remove the useState and counter for it.
  3. Remove the matching entry from the toValues() destructure + emit block in toValues.js.
  4. Remove the matching entry from prefillFromValues() return value in App.jsx.
  5. Remove it from the useMemo call and handleApplyUpload.

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

FunctionSignatureWhat it does
clean(v)(any) → any | undefinedRecursively removes null / undefined / '' / [] / {}. Used so only non-default values appear in the output YAML.
parseYaml(str)(string) → any | nullParses a YAML string safely; returns null on empty or invalid input.
asList(v)(any) → array | nullWraps a single value in an array; passes arrays through; nullnull.

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 → parseYaml returns null → spread is {}clean returns undefined → the key is omitted from the output.
  • replicaCount is omitted if it equals 1 (the chart default).
  • pullPolicy is omitted if it equals 'IfNotPresent' (the chart default).
  • serviceAccount.create is omitted if it equals true (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

PropertyRequiredDescription
nameDisplay name shown in the chart selector dropdown
ociChartRefOCI reference used in the generated helm template command
ociChartVersionChart version for the helm template command
workloadTypesArray of allowed workload types (default: Deployment, StatefulSet, CronJob)
workloadSectionsOrdered 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() in App.jsx
  • Emit in buildWorkload() in toValues.js
  • Prefill in prefillWorkload() in App.jsx

Adding a new config-driven workload section

  • Add section descriptor to workloadSections[] in chart config
  • Add initial state (e.g. { enabled: false, yaml: '' }) to newWorkload() in App.jsx
  • Emit in buildWorkload() in toValues.js
  • Prefill in prefillWorkload() in App.jsx

Adding a new custom workload section

All of the above, plus:

  • Add if (sectionKey === '...') JSX block in CustomSection in WorkloadForm.jsx

Adding a new shared resource type

  • Add newXxx() factory and useState + counter in App.jsx
  • Add <details> UI block in App.jsx (shared-resources column)
  • Add to useMemo deps and handleApplyUpload in App.jsx
  • Add emit block in toValues() in toValues.js
  • Add prefill in prefillFromValues() return value in App.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 in values.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.

  1. 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 },
    ],
    },
    ],
    }
  2. Wire state and mapping just like any other section:

    • newWorkload() in App.jsx: add networkPolicy: { enabled: false, yaml: '' }.
    • buildWorkload() in toValues.js: read wl.networkPolicy and emit to w.networkPolicy (or whatever key your chart uses).
    • prefillFromValues() in App.jsx: read w.networkPolicy and 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.

  1. 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: []',
    },
    ],
    },
    ]
  2. 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.jsx already uses RESOURCE_SECTIONS + stateKey/nextIdKey, so you usually do not need to touch the JSX — the new resource will appear automatically.

  3. 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.