diff --git a/app/(main)/instance/_lib/instance.ts b/app/(main)/instance/_lib/instance.ts index 2f17e66..e51dc2b 100644 --- a/app/(main)/instance/_lib/instance.ts +++ b/app/(main)/instance/_lib/instance.ts @@ -34,3 +34,32 @@ export async function performInstanceAction({ ); } } + +export async function updateInstance( + name: string, + config: Record, + project: string | null = null, +) { + const projectSuffix = project + ? `?project=${encodeURIComponent(project)}` + : ''; + const res = await fetch( + `/1.0/instances/${encodeURIComponent(name)}${projectSuffix}`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + config, + }), + }, + ); + + if (!res.ok) { + const payload = await res.json().catch(() => ({ error: res.statusText })); + throw new Error( + payload?.error || `Unable to update instance ${name}`, + ); + } +} diff --git a/app/(main)/instance/configuration/page.tsx b/app/(main)/instance/configuration/page.tsx index 881e6e7..85b01c1 100644 --- a/app/(main)/instance/configuration/page.tsx +++ b/app/(main)/instance/configuration/page.tsx @@ -1,11 +1,144 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { SaveIcon } from 'lucide-react'; +import GeneralConfiguration from '@/app/(main)/instances/_components/general-configuration'; +import { Button } from 'ui-web/components/button'; +import { Spinner } from 'ui-web/components/spinner'; +import { toast } from 'sonner'; +import { updateInstance } from '../_lib/instance'; +import { useInstanceContext } from '../_context/instance'; + +function hasConfigChanged( + current: Record, + initial: Record, +) { + const keys = new Set([...Object.keys(current), ...Object.keys(initial)]); + + for (const key of keys) { + const hasCurrent = Object.prototype.hasOwnProperty.call(current, key); + const hasInitial = Object.prototype.hasOwnProperty.call(initial, key); + + if (hasCurrent !== hasInitial) { + return true; + } + + if (hasCurrent && hasInitial && current[key] !== initial[key]) { + return true; + } + } + + return false; +} + +function buildConfigChanges( + current: Record, + initial: Record, +) { + const changes: Record = {}; + const keys = new Set([...Object.keys(current), ...Object.keys(initial)]); + + for (const key of keys) { + const hasCurrent = Object.prototype.hasOwnProperty.call(current, key); + const hasInitial = Object.prototype.hasOwnProperty.call(initial, key); + + if (hasCurrent && (!hasInitial || current[key] !== initial[key])) { + changes[key] = current[key]; + continue; + } + + if (!hasCurrent && hasInitial) { + changes[key] = null; + } + } + + return changes; +} export default function ConfigurationPage() { + const { instance, isLoading, isError, mutate } = useInstanceContext(); + const [config, setConfig] = useState>({}); + const [initialConfig, setInitialConfig] = useState>( + {}, + ); + const [isSaving, setIsSaving] = useState(false); + const [isDirty, setIsDirty] = useState(false); + + // Initialize config from instance data + useEffect(() => { + const nextConfig = { ...(instance?.config ?? {}) }; + setConfig(nextConfig); + setInitialConfig({ ...nextConfig }); + setIsDirty(false); + }, [instance]); + + const handleConfigChange = (newConfig: Record) => { + setConfig(newConfig); + setIsDirty(hasConfigChanged(newConfig, initialConfig)); + }; + + const handleSave = async () => { + if (!instance) return; + + const changes = buildConfigChanges(config, initialConfig); + if (Object.keys(changes).length === 0) { + setIsDirty(false); + return; + } + + setIsSaving(true); + try { + await updateInstance(instance.name, changes, instance.project ?? null); + toast.success('Instance configuration updated'); + setInitialConfig({ ...config }); + setIsDirty(false); + // Revalidate instance data + mutate(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : 'Failed to update configuration', + ); + } finally { + setIsSaving(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError || !instance) { + return ( +
+ Failed to load instance configuration. +
+ ); + } + return ( -
- Instance Configuration Under Construction +
+
+ +
+
+ +
); } diff --git a/app/(main)/instance/layout.tsx b/app/(main)/instance/layout.tsx index 39068a5..beed65a 100644 --- a/app/(main)/instance/layout.tsx +++ b/app/(main)/instance/layout.tsx @@ -52,12 +52,12 @@ function InstanceLayoutContent({ children }: { children: React.ReactNode }) { } return ( -
+
-
+
-
{children}
+
{children}
);