feat: inline project editing (name, image, port, healthcheck)

This commit is contained in:
2026-03-28 14:43:27 +03:00
parent d3dd2be421
commit c64f1e5363
+95 -18
View File
@@ -7,7 +7,7 @@
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import EmptyState from '$lib/components/EmptyState.svelte'; import EmptyState from '$lib/components/EmptyState.svelte';
import Skeleton from '$lib/components/Skeleton.svelte'; import Skeleton from '$lib/components/Skeleton.svelte';
import { IconTrash, IconKey, IconHardDrive, IconDeploy, IconChevronRight, IconClock, IconTag, IconLoader, IconPlus } from '$lib/components/icons'; import { IconTrash, IconKey, IconHardDrive, IconDeploy, IconChevronRight, IconClock, IconTag, IconLoader, IconPlus, IconEdit, IconCheck, IconX } from '$lib/components/icons';
import FormField from '$lib/components/FormField.svelte'; import FormField from '$lib/components/FormField.svelte';
import { toasts } from '$lib/stores/toast'; import { toasts } from '$lib/stores/toast';
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
@@ -55,6 +55,43 @@
} }
} }
// Edit project
let editing = $state(false);
let editName = $state('');
let editImage = $state('');
let editPort = $state('');
let editHealthcheck = $state('');
let saving = $state(false);
function startEditing() {
if (!project) return;
editName = project.name;
editImage = project.image;
editPort = String(project.port || '');
editHealthcheck = project.healthcheck || '';
editing = true;
}
async function saveProject() {
if (!editName.trim() || !editImage.trim()) return;
saving = true;
try {
await api.updateProject(projectId, {
name: editName.trim(),
image: editImage.trim(),
port: parseInt(editPort) || 0,
healthcheck: editHealthcheck.trim(),
});
toasts.success('Project updated');
editing = false;
await loadProject();
} catch (e) {
toasts.error(e instanceof Error ? e.message : 'Failed to update project');
} finally {
saving = false;
}
}
async function handleDeleteStage(stageId: string, name: string) { async function handleDeleteStage(stageId: string, name: string) {
try { try {
await api.deleteStage(projectId, stageId); await api.deleteStage(projectId, stageId);
@@ -225,23 +262,63 @@
</div> </div>
<!-- Project info --> <!-- Project info -->
<div class="grid grid-cols-2 gap-4 rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)] sm:grid-cols-4"> <div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)]">
<div> {#if editing}
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.port')}</p> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<p class="mt-1 text-sm text-[var(--text-primary)]">{project.port || '-'}</p> <FormField label="Name *" name="editName" bind:value={editName} />
</div> <FormField label="Image *" name="editImage" bind:value={editImage} />
<div> <FormField label="Port" name="editPort" type="number" bind:value={editPort} />
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.registry')}</p> <FormField label="Healthcheck Path" name="editHealthcheck" bind:value={editHealthcheck} placeholder="/api/health" />
<p class="mt-1 text-sm text-[var(--text-primary)]">{project.registry || '-'}</p> </div>
</div> <div class="mt-4 flex items-center gap-2 justify-end">
<div> <button
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.healthcheck')}</p> type="button"
<p class="mt-1 text-sm text-[var(--text-primary)]">{project.healthcheck || '-'}</p> onclick={() => { editing = false; }}
</div> class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--border-primary)] px-3 py-1.5 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--surface-card-hover)] transition-colors"
<div> >
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.created')}</p> <IconX size={14} />
<p class="mt-1 text-sm text-[var(--text-primary)]">{new Date(project.created_at).toLocaleDateString()}</p> {$t('projects.cancel')}
</div> </button>
<button
type="button"
onclick={saveProject}
disabled={saving || !editName.trim() || !editImage.trim()}
class="inline-flex items-center gap-1.5 rounded-lg bg-[var(--color-brand-600)] px-3 py-1.5 text-sm font-medium text-white hover:bg-[var(--color-brand-700)] disabled:opacity-50 transition-colors"
>
<IconCheck size={14} />
{saving ? 'Saving...' : $t('common.save')}
</button>
</div>
{:else}
<div class="flex items-start justify-between">
<div class="grid grid-cols-2 gap-4 flex-1 sm:grid-cols-4">
<div>
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.port')}</p>
<p class="mt-1 text-sm text-[var(--text-primary)]">{project.port || 'Auto'}</p>
</div>
<div>
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.healthcheck')}</p>
<p class="mt-1 text-sm text-[var(--text-primary)]">{project.healthcheck || 'Auto'}</p>
</div>
<div>
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.registry')}</p>
<p class="mt-1 text-sm text-[var(--text-primary)]">{project.registry || '-'}</p>
</div>
<div>
<p class="text-xs font-medium text-[var(--text-tertiary)] uppercase">{$t('projects.created')}</p>
<p class="mt-1 text-sm text-[var(--text-primary)]">{new Date(project.created_at).toLocaleDateString()}</p>
</div>
</div>
<button
type="button"
onclick={startEditing}
title={$t('common.edit')}
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-[var(--surface-card-hover)] hover:text-[var(--text-link)] transition-colors"
>
<IconEdit size={16} />
</button>
</div>
{/if}
</div> </div>
<!-- Stages & Instances --> <!-- Stages & Instances -->