feat: edit existing env vars, labels, and volume mounts

Add inline edit functionality for environment variables, labels, and
volume mounts on the app detail page. Each entity row now has an Edit
button that reveals an inline form using Alpine.js.

- POST /apps/{id}/env-vars/{varID}/edit
- POST /apps/{id}/labels/{labelID}/edit
- POST /apps/{id}/volumes/{volumeID}/edit
- Path validation for volume host and container paths
- Warning banner about container restart after env var changes
- Tests for ValidateVolumePath

fixes #67
This commit is contained in:
2026-02-16 00:26:07 -08:00
parent e31666ab5c
commit e9d284698a
4 changed files with 335 additions and 34 deletions

View File

@@ -106,15 +106,34 @@
</thead>
<tbody class="table-body">
{{range .EnvVars}}
<tr>
<td class="font-mono font-medium">{{.Key}}</td>
<td class="font-mono text-gray-500">{{.Value}}</td>
<td class="text-right">
<form method="POST" action="/apps/{{$.App.ID}}/env/{{.ID}}/delete" class="inline" x-data="confirmAction('Delete this environment variable?')" @submit="confirm($event)">
{{ .CSRFField }}
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
<tr x-data="{ editing: false }">
<template x-if="!editing">
<td class="font-mono font-medium">{{.Key}}</td>
</template>
<template x-if="!editing">
<td class="font-mono text-gray-500">{{.Value}}</td>
</template>
<template x-if="!editing">
<td class="text-right">
<button @click="editing = true" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
<form method="POST" action="/apps/{{$.App.ID}}/env-vars/{{.ID}}/delete" class="inline" x-data="confirmAction('Delete this environment variable?')" @submit="confirm($event)">
{{ $.CSRFField }}
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
</template>
<template x-if="editing">
<td colspan="3">
<form method="POST" action="/apps/{{$.App.ID}}/env-vars/{{.ID}}/edit" class="flex gap-2 items-center">
{{ $.CSRFField }}
<input type="text" name="key" value="{{.Key}}" required class="input flex-1 font-mono text-sm">
<input type="text" name="value" value="{{.Value}}" required class="input flex-1 font-mono text-sm">
<button type="submit" class="btn-primary text-sm">Save</button>
<button type="button" @click="editing = false" class="text-gray-500 hover:text-gray-700 text-sm">Cancel</button>
</form>
<p class="text-xs text-amber-600 mt-1">⚠ Container restart needed after env var changes.</p>
</td>
</template>
</tr>
{{end}}
</tbody>
@@ -151,15 +170,33 @@
</td>
</tr>
{{range .Labels}}
<tr>
<td class="font-mono font-medium">{{.Key}}</td>
<td class="font-mono text-gray-500">{{.Value}}</td>
<td class="text-right">
<form method="POST" action="/apps/{{$.App.ID}}/labels/{{.ID}}/delete" class="inline" x-data="confirmAction('Delete this label?')" @submit="confirm($event)">
{{ .CSRFField }}
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
<tr x-data="{ editing: false }">
<template x-if="!editing">
<td class="font-mono font-medium">{{.Key}}</td>
</template>
<template x-if="!editing">
<td class="font-mono text-gray-500">{{.Value}}</td>
</template>
<template x-if="!editing">
<td class="text-right">
<button @click="editing = true" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
<form method="POST" action="/apps/{{$.App.ID}}/labels/{{.ID}}/delete" class="inline" x-data="confirmAction('Delete this label?')" @submit="confirm($event)">
{{ $.CSRFField }}
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
</template>
<template x-if="editing">
<td colspan="3">
<form method="POST" action="/apps/{{$.App.ID}}/labels/{{.ID}}/edit" class="flex gap-2 items-center">
{{ $.CSRFField }}
<input type="text" name="key" value="{{.Key}}" required class="input flex-1 font-mono text-sm">
<input type="text" name="value" value="{{.Value}}" required class="input flex-1 font-mono text-sm">
<button type="submit" class="btn-primary text-sm">Save</button>
<button type="button" @click="editing = false" class="text-gray-500 hover:text-gray-700 text-sm">Cancel</button>
</form>
</td>
</template>
</tr>
{{end}}
</tbody>
@@ -189,22 +226,46 @@
</thead>
<tbody class="table-body">
{{range .Volumes}}
<tr>
<td class="font-mono">{{.HostPath}}</td>
<td class="font-mono">{{.ContainerPath}}</td>
<td>
{{if .ReadOnly}}
<span class="badge-neutral">Read-only</span>
{{else}}
<span class="badge-info">Read-write</span>
{{end}}
</td>
<td class="text-right">
<form method="POST" action="/apps/{{$.App.ID}}/volumes/{{.ID}}/delete" class="inline" x-data="confirmAction('Delete this volume mount?')" @submit="confirm($event)">
{{ .CSRFField }}
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
<tr x-data="{ editing: false }">
<template x-if="!editing">
<td class="font-mono">{{.HostPath}}</td>
</template>
<template x-if="!editing">
<td class="font-mono">{{.ContainerPath}}</td>
</template>
<template x-if="!editing">
<td>
{{if .ReadOnly}}
<span class="badge-neutral">Read-only</span>
{{else}}
<span class="badge-info">Read-write</span>
{{end}}
</td>
</template>
<template x-if="!editing">
<td class="text-right">
<button @click="editing = true" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
<form method="POST" action="/apps/{{$.App.ID}}/volumes/{{.ID}}/delete" class="inline" x-data="confirmAction('Delete this volume mount?')" @submit="confirm($event)">
{{ $.CSRFField }}
<button type="submit" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</form>
</td>
</template>
<template x-if="editing">
<td colspan="4">
<form method="POST" action="/apps/{{$.App.ID}}/volumes/{{.ID}}/edit" class="flex gap-2 items-center">
{{ $.CSRFField }}
<input type="text" name="host_path" value="{{.HostPath}}" required class="input flex-1 font-mono text-sm" placeholder="/host/path">
<input type="text" name="container_path" value="{{.ContainerPath}}" required class="input flex-1 font-mono text-sm" placeholder="/container/path">
<label class="flex items-center gap-1 text-sm text-gray-600 whitespace-nowrap">
<input type="checkbox" name="readonly" value="1" {{if .ReadOnly}}checked{{end}} class="rounded border-gray-300 text-primary-600 focus:ring-primary-500">
RO
</label>
<button type="submit" class="btn-primary text-sm">Save</button>
<button type="button" @click="editing = false" class="text-gray-500 hover:text-gray-700 text-sm">Cancel</button>
</form>
</td>
</template>
</tr>
{{end}}
</tbody>