feat(docker-watcher): phase 9 - SvelteKit dashboard & project views
SvelteKit project with Svelte 5, TypeScript, Tailwind CSS v4. Dashboard with project cards, project detail with stage/instance management, deploy history, instance controls. Shared API client and reusable components (StatusBadge, InstanceCard, ProjectCard, ConfirmDialog). Add Phase 14 (Volumes & Environment) to plan.
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
<script lang="ts">
|
||||
import type { Project } from '$lib/types';
|
||||
import * as api from '$lib/api';
|
||||
|
||||
let projects = $state<Project[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let showAddForm = $state(false);
|
||||
|
||||
// Add project form state.
|
||||
let formName = $state('');
|
||||
let formImage = $state('');
|
||||
let formRegistry = $state('');
|
||||
let formPort = $state(3000);
|
||||
let formHealthcheck = $state('');
|
||||
let formSubmitting = $state(false);
|
||||
let formError = $state('');
|
||||
|
||||
async function loadProjects() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
projects = await api.listProjects();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load projects';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddProject() {
|
||||
if (!formName.trim() || !formImage.trim()) {
|
||||
formError = 'Name and image are required.';
|
||||
return;
|
||||
}
|
||||
|
||||
formSubmitting = true;
|
||||
formError = '';
|
||||
try {
|
||||
await api.createProject({
|
||||
name: formName.trim(),
|
||||
image: formImage.trim(),
|
||||
registry: formRegistry.trim(),
|
||||
port: formPort,
|
||||
healthcheck: formHealthcheck.trim()
|
||||
});
|
||||
// Reset form.
|
||||
formName = '';
|
||||
formImage = '';
|
||||
formRegistry = '';
|
||||
formPort = 3000;
|
||||
formHealthcheck = '';
|
||||
showAddForm = false;
|
||||
await loadProjects();
|
||||
} catch (e) {
|
||||
formError = e instanceof Error ? e.message : 'Failed to create project';
|
||||
} finally {
|
||||
formSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadProjects();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Projects - Docker Watcher</title>
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Projects</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700"
|
||||
onclick={() => { showAddForm = !showAddForm; }}
|
||||
>
|
||||
{showAddForm ? 'Cancel' : 'Add Project'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add project form -->
|
||||
{#if showAddForm}
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="text-lg font-semibold text-gray-900">New Project</h2>
|
||||
|
||||
{#if formError}
|
||||
<div class="mt-3 rounded-md bg-red-50 p-3">
|
||||
<p class="text-sm text-red-700">{formError}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={formName}
|
||||
placeholder="my-web-app"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="image" class="block text-sm font-medium text-gray-700">Image *</label>
|
||||
<input
|
||||
id="image"
|
||||
type="text"
|
||||
bind:value={formImage}
|
||||
placeholder="registry.example.com/org/app"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="registry" class="block text-sm font-medium text-gray-700">Registry</label>
|
||||
<input
|
||||
id="registry"
|
||||
type="text"
|
||||
bind:value={formRegistry}
|
||||
placeholder="gitea"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="port" class="block text-sm font-medium text-gray-700">Port</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
bind:value={formPort}
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label for="healthcheck" class="block text-sm font-medium text-gray-700">Healthcheck Path</label>
|
||||
<input
|
||||
id="healthcheck"
|
||||
type="text"
|
||||
bind:value={formHealthcheck}
|
||||
placeholder="/api/health"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 disabled:opacity-50"
|
||||
disabled={formSubmitting}
|
||||
onclick={handleAddProject}
|
||||
>
|
||||
{formSubmitting ? 'Creating...' : 'Create Project'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Projects list -->
|
||||
{#if loading}
|
||||
<div class="mt-6 flex items-center justify-center py-12">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-indigo-600"></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="mt-6 rounded-md bg-red-50 p-4">
|
||||
<p class="text-sm text-red-700">{error}</p>
|
||||
<button type="button" class="mt-2 text-sm font-medium text-red-700 underline" onclick={loadProjects}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
{:else if projects.length === 0}
|
||||
<div class="mt-6 rounded-lg border-2 border-dashed border-gray-300 p-12 text-center">
|
||||
<p class="text-sm text-gray-500">No projects configured yet.</p>
|
||||
<p class="mt-1 text-sm text-gray-400">Click "Add Project" to get started.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-6 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">Name</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">Image</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">Port</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">Registry</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">Created</th>
|
||||
<th class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{#each projects as project (project.id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<a href="/projects/{project.id}" class="font-medium text-indigo-600 hover:text-indigo-800">
|
||||
{project.name}
|
||||
</a>
|
||||
</td>
|
||||
<td class="max-w-xs truncate px-6 py-4 font-mono text-sm text-gray-500">
|
||||
{project.image}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
{project.port || '-'}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
{project.registry || '-'}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
|
||||
{new Date(project.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-right text-sm">
|
||||
<a href="/projects/{project.id}" class="text-indigo-600 hover:text-indigo-800">
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user