/** * Modify Modal Component * Modal for modifying an instance configuration * Supports Values Schema for dynamic form generation */ import React, { useState, useEffect } from "react"; import { Settings } from "lucide-react"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; import type { InstanceResponse, UpdateInstanceRequest } from "@/api"; import { getInstance, getInstanceValuesDiff } from "@/api"; import { Modal, Button, FormField, Input, Textarea, ErrorState, LoadingState, Badge, } from "@/shared/components"; interface ModifyModalProps { instance: InstanceResponse; onClose: () => void; onConfirm: (clusterId: string, instanceId: string, data: UpdateInstanceRequest) => Promise; } export const ModifyModal: React.FC = ({ instance, onClose, onConfirm, }) => { const [tag, setTag] = useState(""); const [description, setDescription] = useState(""); const [valuesYaml, setValuesYaml] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [modifiedKeys, setModifiedKeys] = useState([]); // Values Diff support const [showDiff, setShowDiff] = useState(false); const [loadingDiff] = useState(false); const [diffData, setDiffData] = useState<{ current: Record; defaults: Record; } | null>(null); const [diffError] = useState(null); // Fetch full Helm values (via values-diff API) and instance detail useEffect(() => { setTag(instance.version || ""); setDescription(""); const loadData = async () => { if (instance.clusterId && instance.id) { // Load values diff first — gives us the full current Helm values try { const data = await getInstanceValuesDiff(instance.clusterId, instance.id); if (data?.current && Object.keys(data.current).length > 0) { const currentYaml = stringifyYaml(data.current, { lineWidth: 0 }); setValuesYaml(currentYaml); setDiffData({ current: data.current, defaults: data.defaults ?? {} }); } } catch (err) { console.error('[ModifyModal] Failed to load values diff:', err); // Fallback: try instance detail try { const detail = await getInstance({ clusterId: instance.clusterId, instanceId: instance.id }); if (detail.values && Object.keys(detail.values).length > 0) { const y = stringifyYaml(detail.values, { lineWidth: 0 }); setValuesYaml(y); } } catch (err2) { console.error('[ModifyModal] Failed to load instance detail:', err2); } } } }; loadData(); }, [instance]); // Recompute modified keys when valuesYaml or diffData changes useEffect(() => { if (!diffData?.defaults || !valuesYaml) return; try { const current = parseYaml(valuesYaml); const defaults = diffData.defaults; const changed: string[] = []; const walkKeys = (curr: any, def: any, prefix: string) => { if (curr === null || curr === undefined) return; if (typeof curr !== 'object') return; for (const key of Object.keys(curr)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (JSON.stringify(curr[key]) !== JSON.stringify(def?.[key])) { changed.push(fullKey); } if (typeof curr[key] === 'object' && curr[key] !== null && !Array.isArray(curr[key])) { walkKeys(curr[key], def?.[key] ?? {}, fullKey); } } }; walkKeys(current, defaults, ''); setModifiedKeys(changed); } catch { /* ignore parse errors */ } }, [valuesYaml, diffData]); const applyDefaults = () => { if (!diffData?.defaults) return; setValuesYaml(stringifyYaml(diffData.defaults, { lineWidth: 0 })); }; /** * Render a values object as YAML lines, bolding keys that differ from defaults. */ const renderDiffValues = ( values: Record, compare: Record, ): React.ReactNode => { const yaml = stringifyYaml(values); const lines = yaml.split("\n"); return lines.map((line, i) => { // Extract the key name from a YAML line const keyMatch = line.match(/^(\s*)([a-zA-Z_][\w-]*)\s*:/); if (keyMatch) { const key = keyMatch[2]; const keyChanged = compare[key] !== undefined && JSON.stringify(values[key]) !== JSON.stringify(compare[key]); if (keyChanged) { return ( {keyMatch[1]}{key}:{line.slice(keyMatch[0].length)} ); } } return {line}; }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); try { if (valuesYaml.trim()) { parseValuesYaml(valuesYaml); } const payload: UpdateInstanceRequest = { version: tag && tag !== instance.version ? tag : undefined, description: description.trim() || undefined, valuesYaml: valuesYaml.trim() || undefined, }; if (!instance.clusterId || !instance.id) { setError("Instance identifier is missing"); setLoading(false); return; } await onConfirm(instance.clusterId, instance.id, payload); onClose(); } catch (err: unknown) { if (err instanceof Error && err.message.includes("YAML")) { setError(err.message); } else { setError((err as Error).message || "Failed to modify instance"); } } finally { setLoading(false); } }; return ( } >
{/* Error Alert */} {error && ( )} {/* Current Info */}

Current Version: {instance.version || "N/A"}

Cluster: {instance.clusterId || "N/A"}

Repository: {instance.repository || "N/A"}

{/* Tag */} setTag(e.target.value)} placeholder="e.g., v1.0.0" required /> {/* Description */} setDescription(e.target.value)} placeholder="Modification description" /> {/* Current Values — directly editable as YAML */}

Editing current deployed values. The full YAML is submitted so nested chart values stay intact.

{modifiedKeys.length > 0 && (
Modified: {modifiedKeys.map((k) => ( {k} ))}
)}