feat: monolithic env var editing with bulk save #158
@@ -6,6 +6,68 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
|
// ============================================
|
||||||
|
// Environment Variable Editor Component
|
||||||
|
// ============================================
|
||||||
|
Alpine.data("envVarEditor", () => ({
|
||||||
|
vars: [],
|
||||||
|
editIdx: -1,
|
||||||
|
editKey: "",
|
||||||
|
editVal: "",
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.vars = Array.from(this.$el.querySelectorAll(".env-init")).map(
|
||||||
|
(span) => ({
|
||||||
|
key: span.dataset.key,
|
||||||
|
value: span.dataset.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
startEdit(i) {
|
||||||
|
this.editIdx = i;
|
||||||
|
this.editKey = this.vars[i].key;
|
||||||
|
this.editVal = this.vars[i].value;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveEdit(i) {
|
||||||
|
this.vars[i] = { key: this.editKey, value: this.editVal };
|
||||||
|
this.editIdx = -1;
|
||||||
|
this.submitAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeVar(i) {
|
||||||
|
if (!window.confirm("Delete this environment variable?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vars.splice(i, 1);
|
||||||
|
this.submitAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
addVar(keyEl, valEl) {
|
||||||
|
const k = keyEl.value.trim();
|
||||||
|
const v = valEl.value.trim();
|
||||||
|
|
||||||
|
if (!k) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vars.push({ key: k, value: v });
|
||||||
|
this.submitAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
submitAll() {
|
||||||
|
this.$refs.bulkData.value = this.vars
|
||||||
|
.map((e) => e.key + "=" + e.value)
|
||||||
|
.join("\n");
|
||||||
|
this.$refs.bulkForm.submit();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// App Detail Page Component
|
||||||
|
// ============================================
|
||||||
Alpine.data("appDetail", (config) => ({
|
Alpine.data("appDetail", (config) => ({
|
||||||
appId: config.appId,
|
appId: config.appId,
|
||||||
currentDeploymentId: config.initialDeploymentId,
|
currentDeploymentId: config.initialDeploymentId,
|
||||||
|
|||||||
@@ -101,91 +101,59 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Environment Variables -->
|
<!-- Environment Variables -->
|
||||||
<div class="card p-6 mb-6" x-data="{
|
<div class="card p-6 mb-6" x-data="envVarEditor()">
|
||||||
vars: [],
|
|
||||||
newKey: '',
|
|
||||||
newValue: '',
|
|
||||||
init() {
|
|
||||||
const text = this.$refs.envVarsField.value;
|
|
||||||
if (!text.trim()) return;
|
|
||||||
this.vars = text.split('\n')
|
|
||||||
.map(l => l.trim())
|
|
||||||
.filter(l => l !== '')
|
|
||||||
.map(l => {
|
|
||||||
const idx = l.indexOf('=');
|
|
||||||
if (idx === -1) return null;
|
|
||||||
return { key: l.substring(0, idx), value: l.substring(idx + 1), editing: false };
|
|
||||||
})
|
|
||||||
.filter(v => v !== null);
|
|
||||||
},
|
|
||||||
addVar() {
|
|
||||||
if (this.newKey.trim() === '') return;
|
|
||||||
this.vars.push({ key: this.newKey.trim(), value: this.newValue, editing: false });
|
|
||||||
this.newKey = '';
|
|
||||||
this.newValue = '';
|
|
||||||
},
|
|
||||||
removeVar(index) {
|
|
||||||
this.vars.splice(index, 1);
|
|
||||||
},
|
|
||||||
prepareSubmit() {
|
|
||||||
this.$refs.envVarsField.value = this.vars.map(v => v.key + '=' + v.value).join('\n');
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
<h2 class="section-title mb-4">Environment Variables</h2>
|
<h2 class="section-title mb-4">Environment Variables</h2>
|
||||||
<form method="POST" action="/apps/{{.App.ID}}/env" @submit="prepareSubmit()">
|
{{range .EnvVars}}<span class="env-init hidden" data-key="{{.Key}}" data-value="{{.Value}}"></span>{{end}}
|
||||||
|
<template x-if="vars.length > 0">
|
||||||
|
<div class="overflow-x-auto mb-4">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="table-header">
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th class="text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-body">
|
||||||
|
<template x-for="(env, idx) in vars" :key="idx">
|
||||||
|
<tr>
|
||||||
|
<template x-if="editIdx !== idx">
|
||||||
|
<td class="font-mono font-medium" x-text="env.key"></td>
|
||||||
|
</template>
|
||||||
|
<template x-if="editIdx !== idx">
|
||||||
|
<td class="font-mono text-gray-500" x-text="env.value"></td>
|
||||||
|
</template>
|
||||||
|
<template x-if="editIdx !== idx">
|
||||||
|
<td class="text-right">
|
||||||
|
<button @click="startEdit(idx)" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
|
||||||
|
<button @click="removeVar(idx)" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<template x-if="editIdx === idx">
|
||||||
|
<td colspan="3">
|
||||||
|
<form @submit.prevent="saveEdit(idx)" class="flex gap-2 items-center">
|
||||||
|
<input type="text" x-model="editKey" required class="input flex-1 font-mono text-sm">
|
||||||
|
<input type="text" x-model="editVal" required class="input flex-1 font-mono text-sm">
|
||||||
|
<button type="submit" class="btn-primary text-sm">Save</button>
|
||||||
|
<button type="button" @click="editIdx = -1" 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>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<form @submit.prevent="addVar($refs.newKey, $refs.newVal)" class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<input x-ref="newKey" type="text" placeholder="KEY" required class="input flex-1 font-mono text-sm">
|
||||||
|
<input x-ref="newVal" type="text" placeholder="value" required class="input flex-1 font-mono text-sm">
|
||||||
|
<button type="submit" class="btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
<form x-ref="bulkForm" method="POST" action="/apps/{{.App.ID}}/env" class="hidden">
|
||||||
{{ .CSRFField }}
|
{{ .CSRFField }}
|
||||||
<template x-if="vars.length > 0">
|
<textarea x-ref="bulkData" name="env_vars"></textarea>
|
||||||
<div class="overflow-x-auto mb-4">
|
|
||||||
<table class="table">
|
|
||||||
<thead class="table-header">
|
|
||||||
<tr>
|
|
||||||
<th>Key</th>
|
|
||||||
<th>Value</th>
|
|
||||||
<th class="text-right">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-body">
|
|
||||||
<template x-for="(v, index) in vars" :key="index">
|
|
||||||
<tr>
|
|
||||||
<template x-if="!v.editing">
|
|
||||||
<td class="font-mono font-medium" x-text="v.key"></td>
|
|
||||||
</template>
|
|
||||||
<template x-if="!v.editing">
|
|
||||||
<td class="font-mono text-gray-500" x-text="v.value"></td>
|
|
||||||
</template>
|
|
||||||
<template x-if="!v.editing">
|
|
||||||
<td class="text-right">
|
|
||||||
<button type="button" @click="v.editing = true" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
|
|
||||||
<button type="button" @click="removeVar(index)" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
<template x-if="v.editing">
|
|
||||||
<td colspan="3">
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<input type="text" x-model="v.key" required class="input flex-1 font-mono text-sm">
|
|
||||||
<input type="text" x-model="v.value" required class="input flex-1 font-mono text-sm">
|
|
||||||
<button type="button" @click="v.editing = false" class="btn-primary text-sm">Done</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-amber-600 mt-1">⚠ Container restart needed after env var changes.</p>
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="flex flex-col sm:flex-row gap-2 mb-3">
|
|
||||||
<input type="text" x-model="newKey" placeholder="KEY" class="input flex-1 font-mono text-sm">
|
|
||||||
<input type="text" x-model="newValue" placeholder="value" class="input flex-1 font-mono text-sm">
|
|
||||||
<button type="button" @click="addVar()" class="btn-secondary">Add</button>
|
|
||||||
</div>
|
|
||||||
<textarea name="env_vars" x-ref="envVarsField" class="hidden">{{range .EnvVars}}{{.Key}}={{.Value}}
|
|
||||||
{{end}}</textarea>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button type="submit" class="btn-primary">Save All</button>
|
|
||||||
<p class="text-xs text-amber-600">⚠ Container restart needed after env var changes.</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user