Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/(main)/instance/_lib/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,32 @@ export async function performInstanceAction({
);
}
}

export async function updateInstance(
name: string,
config: Record<string, string | null>,
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}`,
);
}
}
139 changes: 136 additions & 3 deletions app/(main)/instance/configuration/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>,
initial: Record<string, string>,
) {
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<string, string>,
initial: Record<string, string>,
) {
const changes: Record<string, string | null> = {};
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<Record<string, string>>({});
const [initialConfig, setInitialConfig] = useState<Record<string, string>>(
{},
);
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<string, string>) => {
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 (
<div className="flex justify-center p-8">
<Spinner />
</div>
);
}

if (isError || !instance) {
return (
<div className="p-4 text-center text-destructive">
Failed to load instance configuration.
</div>
);
}

return (
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
Instance Configuration Under Construction
<div className="h-full flex flex-col space-y-4">
<div className="flex justify-end">
<Button onClick={handleSave} disabled={!isDirty || isSaving}>
{isSaving ? (
<Spinner className="mr-2 h-4 w-4" />
) : (
<SaveIcon className="mr-2 h-4 w-4" />
)}
Save Changes
</Button>
</div>
<div className="flex-1 border rounded-md overflow-hidden bg-background">
<GeneralConfiguration
config={config}
expandedConfig={instance.expanded_config}
onConfigChange={handleConfigChange}
instanceType={instance.type as 'container' | 'virtual-machine'}
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion instance.type as 'container' | 'virtual-machine' may fail silently if the instance type is an unexpected value. While the Instance interface allows type: 'container' | 'virtual-machine' | string, the GeneralConfiguration component expects only the two specific types.

Consider adding validation or a fallback: instanceType={(instance.type === 'virtual-machine' ? 'virtual-machine' : 'container')}

Suggested change
instanceType={instance.type as 'container' | 'virtual-machine'}
instanceType={instance.type === 'virtual-machine' ? 'virtual-machine' : 'container'}

Copilot uses AI. Check for mistakes.
/>
</div>
</div>
);
}
6 changes: 3 additions & 3 deletions app/(main)/instance/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ function InstanceLayoutContent({ children }: { children: React.ReactNode }) {
}

return (
<div className="flex flex-col gap-6 p-6">
<div className="flex flex-col h-[calc(100vh-var(--header-height))] gap-6 p-6">
<InstanceHeader instance={instance} onMutate={mutate} />

<div className="flex flex-col gap-4">
<div className="flex flex-col flex-1 min-h-0 gap-4">
<InstanceTabs />
<div className="mt-4">{children}</div>
<div className="flex-1 min-h-0 mt-4 overflow-auto">{children}</div>
</div>
</div>
);
Expand Down