Files
shopdb-flask/plugins/computers/frontend/views/AccessProtocolsList.vue
cproudlock 245f94d344
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s
Stop two dialogs going see-through in dark mode
--bg-card is deliberately translucent in dark mode (rgba(0,0,61,0.4)) so cards
glass over the page; --bg-card-solid exists for the things that must not. Two
hand-rolled modal panels used the former, leaving the notification-type and
access-protocol editors transparent over the overlay with the table legible
through them. The shared .modal in style.css already got this right.

Also writes down the page-vs-modal rule the codebase already follows, since
nothing stated it: a record with a detail page gets a routed form page, a lookup
row that only exists inside its list gets a modal over that list. Plus the modal
rules from the overlay-close fix - data entry never closes on a stray click,
confirmations may, and panels are painted solid.
2026-08-07 10:30:45 -04:00

223 lines
6.7 KiB
Vue

<template>
<div>
<div class="page-header">
<h1>PC Access Protocols</h1>
<div class="actions">
<router-link to="/settings" class="btn btn-secondary">Back to Settings</router-link>
<button class="btn btn-primary" @click="openNew">New Protocol</button>
</div>
</div>
<div class="card">
<p class="hint">
Remote-access protocols offered on PCs. Links are built as
<code>{{ '{scheme}://{hostname}.<pc_access_domain>:{port}' }}</code> from
each protocol's template. Set the domain in
<router-link to="/settings/site">Site &amp; Facility</router-link>.
</p>
<div class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Scheme</th>
<th>Default port</th>
<th>Link template</th>
<th>Active</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="p in protocols" :key="p.protocolid">
<td><strong>{{ p.name }}</strong></td>
<td class="mono">{{ p.scheme }}</td>
<td>{{ p.defaultport ?? '-' }}</td>
<td class="mono">{{ p.linktemplate }}</td>
<td>
<span class="badge" :class="p.isactive ? 'badge-success' : 'badge-secondary'">
{{ p.isactive ? 'yes' : 'no' }}
</span>
</td>
<td class="actions">
<button class="btn btn-sm btn-secondary" @click="openEdit(p)">Edit</button>
<button class="btn btn-sm btn-danger" @click="remove(p)">Delete</button>
</td>
</tr>
<tr v-if="!loading && !protocols.length">
<td colspan="6" class="muted" style="text-align:center;">No protocols.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="editing" class="modal-overlay">
<div class="modal-panel">
<h2>{{ form.protocolid ? 'Edit' : 'New' }} Protocol</h2>
<div class="form-grid">
<label class="field">
<span>Name</span>
<input v-model="form.name" type="text" maxlength="50" placeholder="e.g. VNC" />
</label>
<label class="field">
<span>Scheme</span>
<input v-model="form.scheme" type="text" maxlength="20" placeholder="vnc / rdp / https / ssh" />
</label>
<label class="field">
<span>Default port</span>
<input v-model.number="form.defaultport" type="number" min="1" max="65535" placeholder="5900" />
</label>
<label class="field">
<span>Link template</span>
<input v-model="form.linktemplate" type="text" maxlength="255" placeholder="vnc://{host}:{port}" />
<small class="muted">Placeholders: <code>{host}</code>, <code>{port}</code>, <code>{scheme}</code></small>
</label>
<label class="field checkbox">
<input v-model="form.isactive" type="checkbox" />
<span>Active</span>
</label>
</div>
<p v-if="error" class="error">{{ error }}</p>
<div class="modal-actions">
<button class="btn btn-secondary" @click="close">Cancel</button>
<button class="btn btn-primary" :disabled="saving || !formValid" @click="save">
{{ saving ? 'Saving...' : 'Save' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { computersApi } from '@/api'
import { apiError } from '@/utils/apiError'
const protocols = ref([])
const loading = ref(true)
const editing = ref(false)
const saving = ref(false)
const error = ref('')
const form = ref({})
const formValid = computed(() =>
form.value.name && form.value.scheme && form.value.linktemplate
)
async function load() {
loading.value = true
try {
const response = await computersApi.protocols.list({ active: false })
protocols.value = response.data.data || []
} catch (err) {
console.error('Error loading protocols:', err)
} finally {
loading.value = false
}
}
function openNew() {
error.value = ''
form.value = { name: '', scheme: '', defaultport: null, linktemplate: '', isactive: true }
editing.value = true
}
function openEdit(p) {
error.value = ''
form.value = {
protocolid: p.protocolid,
name: p.name,
scheme: p.scheme,
defaultport: p.defaultport ?? null,
linktemplate: p.linktemplate,
isactive: p.isactive !== false
}
editing.value = true
}
function close() {
editing.value = false
}
async function save() {
saving.value = true
error.value = ''
try {
if (form.value.protocolid) {
await computersApi.protocols.update(form.value.protocolid, form.value)
} else {
await computersApi.protocols.create(form.value)
}
editing.value = false
await load()
} catch (err) {
error.value = apiError(err, 'Save failed.')
} finally {
saving.value = false
}
}
async function remove(p) {
if (!confirm(`Delete protocol "${p.name}"? (kept but deactivated if any PC uses it)`)) return
try {
await computersApi.protocols.remove(p.protocolid)
await load()
} catch (err) {
console.error('Error deleting protocol:', err)
}
}
onMounted(load)
</script>
<style scoped>
.hint {
color: var(--text-light);
font-size: 0.9rem;
margin: 0 0 14px;
}
.mono { font-family: monospace; font-size: 0.85rem; }
.muted { color: var(--text-light); }
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-start;
justify-content: center;
padding: 40px 16px;
overflow-y: auto;
z-index: 1000;
}
.modal-panel {
/* Solid: var(--bg-card) is translucent in dark mode, which made this
dialog see-through. */
background: var(--bg-card-solid);
color: var(--text);
border: 1px solid var(--border);
border-radius: 10px;
padding: 24px;
width: 100%;
max-width: 520px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
.modal-panel h2 { margin: 0 0 18px; }
.form-grid { display: flex; flex-direction: column; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field > span { font-size: 0.85rem; color: var(--text-light); }
.field input[type="text"],
.field input[type="number"] {
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
}
.field.checkbox { flex-direction: row; align-items: center; gap: 8px; }
.field.checkbox > span { color: var(--text); font-size: 1rem; }
.error { color: var(--danger); margin: 12px 0 0; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }
</style>