web: start: simple form component
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
b479493ac1
commit
9980e03449
7 changed files with 513 additions and 28 deletions
|
|
@ -4,5 +4,6 @@
|
||||||
"components/side-nav/side-nav.js",
|
"components/side-nav/side-nav.js",
|
||||||
"components/logout-button/logout-button.js",
|
"components/logout-button/logout-button.js",
|
||||||
"components/side-panel/side-panel.js",
|
"components/side-panel/side-panel.js",
|
||||||
"components/date-display/date-display.js"
|
"components/date-display/date-display.js",
|
||||||
|
"components/form-vm/form-vm.js"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ local_components:
|
||||||
- logout-button
|
- logout-button
|
||||||
- side-panel
|
- side-panel
|
||||||
- date-display
|
- date-display
|
||||||
|
- form-vm
|
||||||
|
|
||||||
# Remote components fetched from git at build time.
|
# Remote components fetched from git at build time.
|
||||||
# Each is its own repo; its root must contain <name>.js and manifest.json.
|
# Each is its own repo; its root must contain <name>.js and manifest.json.
|
||||||
|
|
|
||||||
138
web/components/form-vm/form-vm.css
Normal file
138
web/components/form-vm/form-vm.css
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
color: #cdd6f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sections ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #6c7086;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fields ───────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #a6adc8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
background: #181825;
|
||||||
|
border: 1px solid #313244;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #cdd6f4;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus { border-color: #89b4fa; }
|
||||||
|
input::placeholder { color: #45475a; }
|
||||||
|
input[readonly] { color: #6c7086; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* ── Disk list ────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.disk-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk-row input { min-width: 0; }
|
||||||
|
|
||||||
|
/* ── Buttons ──────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, opacity 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary { background: #89b4fa; color: #1e1e2e; }
|
||||||
|
.btn-primary:hover { background: #b4d0fa; }
|
||||||
|
.btn-danger { background: transparent; border: 1px solid #f38ba8; color: #f38ba8; }
|
||||||
|
.btn-danger:hover { background: rgba(243,139,168,0.1); }
|
||||||
|
.btn-ghost { background: #313244; color: #a6adc8; padding: 6px 10px; font-size: 12px; }
|
||||||
|
.btn-ghost:hover { background: #45475a; }
|
||||||
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status ───────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.status.error { background: #1e1e2e; border: 1px solid #f38ba8; color: #f38ba8; display: block; }
|
||||||
|
.status.success { background: #1e1e2e; border: 1px solid #a6e3a1; color: #a6e3a1; display: block; }
|
||||||
|
.status.loading { color: #6c7086; display: block; }
|
||||||
|
|
||||||
|
/* ── Toggle ───────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
accent-color: #89b4fa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label { font-size: 13px; color: #a6adc8; text-transform: none; letter-spacing: 0; }
|
||||||
311
web/components/form-vm/form-vm.js
Normal file
311
web/components/form-vm/form-vm.js
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
// VM create / edit form.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// <form-vm></form-vm> → create mode
|
||||||
|
// <form-vm vm-name="i-test1"></form-vm> → edit mode (loads VM from agent)
|
||||||
|
//
|
||||||
|
// Events dispatched on the element:
|
||||||
|
// vm-saved → { detail: { name, mode: 'create'|'edit' } }
|
||||||
|
// vm-error → { detail: { message } }
|
||||||
|
//
|
||||||
|
// Edit mode stops the VM then recreates it with the new parameters.
|
||||||
|
// The name field is read-only in edit mode.
|
||||||
|
|
||||||
|
const CSS = new URL('./form-vm.css', import.meta.url).href
|
||||||
|
|
||||||
|
const PLUS_ICON = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`
|
||||||
|
const TRASH_ICON = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg>`
|
||||||
|
|
||||||
|
class FormVm extends HTMLElement {
|
||||||
|
#api = null
|
||||||
|
#mode = 'create'
|
||||||
|
#vmData = null // loaded in edit mode
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
this.attachShadow({ mode: 'open' })
|
||||||
|
this.shadowRoot.innerHTML = `<link rel="stylesheet" href="${CSS}"><div id="root"></div>`
|
||||||
|
|
||||||
|
this.#api = document.querySelector('api-client')
|
||||||
|
this.#mode = this.hasAttribute('vm-name') ? 'edit' : 'create'
|
||||||
|
|
||||||
|
if (this.#mode === 'edit') {
|
||||||
|
this.#setStatus('loading', 'Chargement…')
|
||||||
|
try {
|
||||||
|
this.#vmData = await this.#api.agent.get(`/vms/${this.getAttribute('vm-name')}`)
|
||||||
|
} catch (e) {
|
||||||
|
this.#setStatus('error', `Impossible de charger la VM : ${e.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#render() {
|
||||||
|
const d = this.#vmData
|
||||||
|
const edit = this.#mode === 'edit'
|
||||||
|
|
||||||
|
this.#root().innerHTML = `
|
||||||
|
<form id="vm-form">
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Général</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Nom</label>
|
||||||
|
<input name="name" placeholder="i-mon-vm" value="${d?.name ?? ''}"
|
||||||
|
${edit ? 'readonly' : 'required'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Mémoire (MB)</label>
|
||||||
|
<input name="memory" type="number" min="128" step="128"
|
||||||
|
placeholder="1024" value="${d?.memory ?? ''}" required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>vCPUs</label>
|
||||||
|
<input name="cpus" type="number" min="1" max="32"
|
||||||
|
placeholder="2" value="${d?.cpus ?? ''}" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field toggle-row">
|
||||||
|
<input name="uefi" type="checkbox" id="uefi-cb"
|
||||||
|
${d?.uefi ? 'checked' : ''} />
|
||||||
|
<label for="uefi-cb" class="toggle-label">UEFI (OVMF)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Authentification</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Clé SSH</label>
|
||||||
|
<input name="sshkey" placeholder="ssh-ed25519 AAAA…"
|
||||||
|
value="${d?.sshkey ?? ''}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Mot de passe (optionnel)</label>
|
||||||
|
<input name="password" type="password" placeholder="laisser vide = désactivé" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Interface réseau</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Subnet</label>
|
||||||
|
<input name="subnet" placeholder="sn-000000"
|
||||||
|
value="${d?.interfaces?.[0]?.subnet ?? ''}" required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>IP</label>
|
||||||
|
<input name="ip" placeholder="192.168.14.x"
|
||||||
|
value="${d?.interfaces?.[0]?.ip ?? ''}" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Stockage</div>
|
||||||
|
<div id="disks">
|
||||||
|
${this.#renderDisks(d?.storage ?? [{ dev: 'vda', path: '' }])}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost" id="add-disk">
|
||||||
|
${PLUS_ICON} Ajouter un disque
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||||||
|
${edit ? 'Mettre à jour' : 'Créer'}
|
||||||
|
</button>
|
||||||
|
${edit ? `<button type="button" class="btn btn-danger" id="delete-btn">Supprimer</button>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
`
|
||||||
|
|
||||||
|
this.#bindEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderDisks(disks) {
|
||||||
|
return disks.map((disk, i) => `
|
||||||
|
<div class="disk-row" data-disk="${i}">
|
||||||
|
<input name="dev_${i}" placeholder="vda" value="${disk.dev ?? ''}" required />
|
||||||
|
<input name="path_${i}" placeholder="/vm/nom.qcow2" value="${disk.path ?? ''}" required />
|
||||||
|
<button type="button" class="btn btn-ghost" data-remove-disk="${i}">${TRASH_ICON}</button>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#bindEvents() {
|
||||||
|
const form = this.#root().querySelector('#vm-form')
|
||||||
|
|
||||||
|
form.addEventListener('submit', e => { e.preventDefault(); this.#submit() })
|
||||||
|
|
||||||
|
this.#root().querySelector('#add-disk')?.addEventListener('click', () => {
|
||||||
|
this.#addDisk()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.#root().querySelector('#delete-btn')?.addEventListener('click', () => {
|
||||||
|
this.#delete()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.#root().addEventListener('click', e => {
|
||||||
|
const btn = e.target.closest('[data-remove-disk]')
|
||||||
|
if (btn) this.#removeDisk(Number(btn.dataset.removeDisk))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Disk list helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#currentDisks() {
|
||||||
|
return [...this.#root().querySelectorAll('[data-disk]')].map(row => {
|
||||||
|
const i = row.dataset.disk
|
||||||
|
return {
|
||||||
|
dev: row.querySelector(`[name="dev_${i}"]`).value.trim(),
|
||||||
|
path: row.querySelector(`[name="path_${i}"]`).value.trim(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#addDisk() {
|
||||||
|
const container = this.#root().querySelector('#disks')
|
||||||
|
const idx = container.querySelectorAll('[data-disk]').length
|
||||||
|
const row = document.createElement('div')
|
||||||
|
row.dataset.disk = idx
|
||||||
|
row.className = 'disk-row'
|
||||||
|
row.innerHTML = `
|
||||||
|
<input name="dev_${idx}" placeholder="vda" required />
|
||||||
|
<input name="path_${idx}" placeholder="/vm/nom.qcow2" required />
|
||||||
|
<button type="button" class="btn btn-ghost" data-remove-disk="${idx}">${TRASH_ICON}</button>
|
||||||
|
`
|
||||||
|
container.appendChild(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
#removeDisk(idx) {
|
||||||
|
this.#root().querySelector(`[data-disk="${idx}"]`)?.remove()
|
||||||
|
this.#reindexDisks()
|
||||||
|
}
|
||||||
|
|
||||||
|
#reindexDisks() {
|
||||||
|
this.#root().querySelectorAll('[data-disk]').forEach((row, i) => {
|
||||||
|
row.dataset.disk = i
|
||||||
|
row.querySelector('[name^="dev_"]').name = `dev_${i}`
|
||||||
|
row.querySelector('[name^="path_"]').name = `path_${i}`
|
||||||
|
const trash = row.querySelector('[data-remove-disk]')
|
||||||
|
if (trash) trash.dataset.removeDisk = i
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build payload ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#buildPayload() {
|
||||||
|
const f = this.#root().querySelector('#vm-form')
|
||||||
|
const data = new FormData(f)
|
||||||
|
const val = k => data.get(k)?.trim() ?? ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: val('name'),
|
||||||
|
memory: Number(val('memory')),
|
||||||
|
cpus: Number(val('cpus')),
|
||||||
|
uefi: f.querySelector('[name="uefi"]').checked,
|
||||||
|
sshkey: val('sshkey'),
|
||||||
|
password: val('password'),
|
||||||
|
interfaces: [{ subnet: val('subnet'), ip: val('ip'), primary: true }],
|
||||||
|
storage: this.#currentDisks(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Submit ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async #submit() {
|
||||||
|
const btn = this.#root().querySelector('#submit-btn')
|
||||||
|
btn.disabled = true
|
||||||
|
this.#clearStatus()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = this.#buildPayload()
|
||||||
|
|
||||||
|
if (this.#mode === 'edit') {
|
||||||
|
// Stop existing VM, then recreate with new params
|
||||||
|
this.#setStatus('loading', 'Arrêt de la VM…')
|
||||||
|
await this.#api.agent.delete(`/vms/${payload.name}`)
|
||||||
|
await this.#api.agent.waitFor(`/vms/${payload.name}`, 'stopped')
|
||||||
|
this.#setStatus('loading', 'Recréation…')
|
||||||
|
} else {
|
||||||
|
this.#setStatus('loading', 'Création…')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#api.agent.post('/vms', payload)
|
||||||
|
await this.#api.agent.waitFor(`/vms/${payload.name}`, 'started')
|
||||||
|
|
||||||
|
this.#setStatus('success', this.#mode === 'edit' ? 'VM mise à jour.' : 'VM créée.')
|
||||||
|
this.dispatchEvent(new CustomEvent('vm-saved', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: { name: payload.name, mode: this.#mode },
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Auto-close parent side-panel after 1s
|
||||||
|
setTimeout(() => this.closest('side-panel')?.close(), 1000)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.#setStatus('error', e.message)
|
||||||
|
this.dispatchEvent(new CustomEvent('vm-error', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: { message: e.message },
|
||||||
|
}))
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Delete ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async #delete() {
|
||||||
|
if (!confirm(`Supprimer la VM ${this.getAttribute('vm-name')} ?`)) return
|
||||||
|
const btn = this.#root().querySelector('#delete-btn')
|
||||||
|
btn.disabled = true
|
||||||
|
this.#setStatus('loading', 'Suppression…')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = this.getAttribute('vm-name')
|
||||||
|
await this.#api.agent.delete(`/vms/${name}`)
|
||||||
|
await this.#api.agent.waitFor(`/vms/${name}`, 'stopped')
|
||||||
|
this.#setStatus('success', 'VM supprimée.')
|
||||||
|
this.dispatchEvent(new CustomEvent('vm-saved', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: { name, mode: 'delete' },
|
||||||
|
}))
|
||||||
|
setTimeout(() => this.closest('side-panel')?.close(), 1000)
|
||||||
|
} catch (e) {
|
||||||
|
this.#setStatus('error', e.message)
|
||||||
|
btn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#root() { return this.shadowRoot.getElementById('root') }
|
||||||
|
|
||||||
|
#setStatus(type, msg) {
|
||||||
|
const el = this.#root().querySelector('#status')
|
||||||
|
if (!el) return
|
||||||
|
el.className = `status ${type}`
|
||||||
|
el.textContent = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
#clearStatus() {
|
||||||
|
const el = this.#root().querySelector('#status')
|
||||||
|
if (el) { el.className = 'status'; el.textContent = '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('form-vm', FormVm)
|
||||||
5
web/components/form-vm/manifest.json
Normal file
5
web/components/form-vm/manifest.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"tag": "form-vm",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Create or edit a VM. Auto-detects mode via vm-name attribute."
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,46 @@
|
||||||
// Dashboard page — called by the router each time this route is activated.
|
import { SidePanel } from '../../components/side-panel/side-panel.js'
|
||||||
// Export init(main) to run logic after the HTML fragment is injected.
|
|
||||||
//
|
|
||||||
// `main` is the <main> DOM element containing the injected HTML.
|
|
||||||
|
|
||||||
export function init(main) {
|
export function init(main) {
|
||||||
// Page is ready — add event listeners, fetch data, etc.
|
main.querySelectorAll('[data-panel-title]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
SidePanel.open({
|
||||||
|
title: btn.dataset.panelTitle,
|
||||||
|
width: btn.dataset.panelWidth ?? '480px',
|
||||||
|
content: `<p style="color:#a6adc8;font-size:14px;line-height:1.6">
|
||||||
|
Contenu du panneau <strong style="color:#cdd6f4">${btn.dataset.panelTitle}</strong>.<br>
|
||||||
|
Largeur : ${btn.dataset.panelWidth}.<br><br>
|
||||||
|
Tu peux ouvrir plusieurs panneaux — ils s'empilent vers la gauche.
|
||||||
|
Ferme avec ✕, Échap, ou en cliquant le fond.
|
||||||
|
</p>`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
main.querySelectorAll('[data-action="create-vm"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
SidePanel.open({
|
||||||
|
title: 'Nouvelle VM',
|
||||||
|
width: '520px',
|
||||||
|
content: `<form-vm></form-vm>`,
|
||||||
|
})
|
||||||
|
// form-vm gère son propre submit et dispatche vm-saved — pas besoin de querySelector
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
main.querySelectorAll('[data-action="edit-vm-i-test1"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
SidePanel.open({
|
||||||
|
title: 'Modifier i-test1',
|
||||||
|
width: '520px',
|
||||||
|
content: `<form-vm vm-name="i-test1"></form-vm>`
|
||||||
|
})
|
||||||
|
// form-vm gère son propre submit et dispatche vm-saved — pas besoin de querySelector
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// vm-saved est attaché sur main (pas document) → nettoyé automatiquement
|
||||||
|
// quand le router remplace le contenu de <main>
|
||||||
|
main.addEventListener('vm-saved', e => {
|
||||||
|
console.log('vm-saved', e.detail)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,24 @@
|
||||||
<date-display></date-display>
|
<date-display></date-display>
|
||||||
|
|
||||||
<!-- Dev test: open stacked panels -->
|
<div style="gap:8px;margin-top:0px;">
|
||||||
<div style="display:flex;gap:8px;margin-top:8px;">
|
<button data-panel-title="Panel A" data-panel-width="400px"
|
||||||
<button onclick="openTestPanel('Panel A', '400px')"
|
|
||||||
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
||||||
padding:7px 14px;color:#89b4fa;font-size:13px;cursor:pointer;">
|
padding:7px 14px;color:#89b4fa;font-size:13px;cursor:pointer;">
|
||||||
Open panel A
|
Open panel A
|
||||||
</button>
|
</button>
|
||||||
<button onclick="openTestPanel('Panel B', '520px')"
|
<button data-panel-title="Panel B" data-panel-width="520px"
|
||||||
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
||||||
padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;">
|
padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;">
|
||||||
Open panel B
|
Open panel B
|
||||||
</button>
|
</button>
|
||||||
|
<button data-action="create-vm"
|
||||||
|
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
||||||
|
padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;">
|
||||||
|
Create-vm
|
||||||
|
</button>
|
||||||
|
<button data-action="edit-vm-i-test1"
|
||||||
|
style="background:#313244;border:1px solid #45475a;border-radius:6px;
|
||||||
|
padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;">
|
||||||
|
Edition
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
import { SidePanel } from '../components/side-panel/side-panel.js'
|
|
||||||
|
|
||||||
window.openTestPanel = (title, width) => {
|
|
||||||
SidePanel.open({
|
|
||||||
title,
|
|
||||||
width,
|
|
||||||
content: `<p style="color:#a6adc8;font-size:14px;line-height:1.6">
|
|
||||||
Contenu du panneau <strong style="color:#cdd6f4">${title}</strong>.<br>
|
|
||||||
Largeur : ${width}.<br><br>
|
|
||||||
Tu peux ouvrir plusieurs panneaux — ils s'empilent vers la gauche.
|
|
||||||
Ferme avec ✕, Échap, ou en cliquant le fond.
|
|
||||||
</p>`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue