Admin UI: diagnostics in a panel attached below the dialog — no more layout shift; normal text color instead of dimmed

This commit is contained in:
2026-09-07 17:07:13 +00:00
parent 7a71688f26
commit 1718825577
3 changed files with 48 additions and 17 deletions
+21 -15
View File
@@ -95,8 +95,7 @@ const lockoutWarning = computed(() => {
return pageOriginAllowed(d.origins || [], rpId) ? null : window.location.host return pageOriginAllowed(d.origins || [], rpId) ? null : window.location.host
}) })
// Whether any origin diagnostic is present (guards the list against // Whether any origin diagnostic is present
// occupying dialog spacing when empty)
const hasOriginDiagnostics = computed(() => { const hasOriginDiagnostics = computed(() => {
const d = props.dialog?.data const d = props.dialog?.data
if (props.dialog?.type !== 'domain-edit' || !d) return false if (props.dialog?.type !== 'domain-edit' || !d) return false
@@ -104,6 +103,11 @@ const hasOriginDiagnostics = computed(() => {
return relatedEntries.value.length > 5 || !!lockoutWarning.value return relatedEntries.value.length > 5 || !!lockoutWarning.value
}) })
// Any runtime diagnostic to show in the dialog's attached feedback panel
const hasDiagnostics = computed(
() => hasOriginDiagnostics.value || !!props.dialog?.data?.wellKnownCheck
)
function pageOriginAllowed(rows, rpId) { function pageOriginAllowed(rows, rpId) {
const toUrl = key => (isWildcardEntry(key) || key.includes('://')) ? key : 'https://' + key const toUrl = key => (isWildcardEntry(key) || key.includes('://')) ? key : 'https://' + key
const inDomain = [] const inDomain = []
@@ -448,6 +452,21 @@ function onRemoveOrigin(i) {
<template> <template>
<Modal v-if="dialog.type" @close="$emit('closeDialog')"> <Modal v-if="dialog.type" @close="$emit('closeDialog')">
<template #attached>
<div v-if="dialog?.type === 'domain-edit' && hasDiagnostics" class="diag-panel" @click.stop>
<ul class="diag-list">
<li v-if="dialog.data.originValidation.some(v => v === 'invalid')" class="small error">Some entries are invalid check for typos in the hostname; a bare '*' or '**' is not allowed, and wildcards only within the domain.</li>
<li v-if="dialog.data.originValidation.some(v => v === 'unreachable')" class="small">Some sites are unreachable make sure they are routed to this instance.</li>
<li v-else-if="dialog.data.originValidation.some(v => v === 'mismatch')" class="small">Some sites are reachable but do not serve this domain.</li>
<li v-if="relatedEntries.length > 5" class="small error">At most 5 related origins are allowed ({{ relatedEntries.length }} listed) the save is rejected.</li>
<li v-if="lockoutWarning" class="small error">Saving would lock you out: {{ lockoutWarning }} could no longer run sign-in ceremonies for this domain. Keep it listed, or mark an auth host.</li>
<li v-if="dialog.data.wellKnownCheck === 'validating'" class="small">Checking the published document</li>
<li v-else-if="dialog.data.wellKnownCheck === 'valid'" class="small"> The published document lists all related origins.</li>
<li v-else-if="dialog.data.wellKnownCheck === 'missing'" class="small error">The published document does not list: {{ (dialog.data.wellKnownMissing || []).join(', ') }}</li>
<li v-else-if="dialog.data.wellKnownCheck === 'unreachable'" class="small">Could not fetch the published document to verify it.</li>
</ul>
</div>
</template>
<h3 class="modal-title"> <h3 class="modal-title">
<template v-if="dialog.type==='org-create'">Create Organization</template> <template v-if="dialog.type==='org-create'">Create Organization</template>
<template v-else-if="dialog.type==='org-update'">Rename Organization</template> <template v-else-if="dialog.type==='org-update'">Rename Organization</template>
@@ -556,13 +575,6 @@ function onRemoveOrigin(i) {
<p class="small muted"> <p class="small muted">
Only the listed sites may sign in with {{ dialog.data.rp_id }} passkeys. Wildcards may be used: <strong>**.{{ dialog.data.rp_id }}</strong> allows the whole domain, <strong>*.{{ dialog.data.rp_id }}</strong> only a single subdomain level.<template v-if="relatedEntries.length"> 🔗 means related host requiring WebAuthn ROR setup.</template><template v-if="dialog.data.auth_host"> 🔑 is the dedicated Paskia host for all account management.</template> Only the listed sites may sign in with {{ dialog.data.rp_id }} passkeys. Wildcards may be used: <strong>**.{{ dialog.data.rp_id }}</strong> allows the whole domain, <strong>*.{{ dialog.data.rp_id }}</strong> only a single subdomain level.<template v-if="relatedEntries.length"> 🔗 means related host requiring WebAuthn ROR setup.</template><template v-if="dialog.data.auth_host"> 🔑 is the dedicated Paskia host for all account management.</template>
</p> </p>
<ul v-if="hasOriginDiagnostics" class="diag-list">
<li v-if="dialog.data.originValidation.some(v => v === 'invalid')" class="small error">Some entries are invalid — check for typos in the hostname; a bare '*' or '**' is not allowed, and wildcards only within the domain.</li>
<li v-if="dialog.data.originValidation.some(v => v === 'unreachable')" class="small muted">Some sites are unreachable — make sure they are routed to this instance.</li>
<li v-else-if="dialog.data.originValidation.some(v => v === 'mismatch')" class="small muted">Some sites are reachable but do not serve this domain.</li>
<li v-if="relatedEntries.length > 5" class="small error">At most 5 related origins are allowed ({{ relatedEntries.length }} listed) — the save is rejected.</li>
<li v-if="lockoutWarning" class="small error">Saving would lock you out: {{ lockoutWarning }} could no longer run sign-in ceremonies for this domain. Keep it listed, or mark an auth host.</li>
</ul>
<template v-if="relatedEntries.length"> <template v-if="relatedEntries.length">
<p class="small muted"> <p class="small muted">
@@ -574,12 +586,6 @@ function onRemoveOrigin(i) {
<pre>{{ wellKnownJson }}</pre> <pre>{{ wellKnownJson }}</pre>
<button type="button" class="icon-btn" @click="copyText(wellKnownJson, 'Well-known document')" aria-label="Copy well-known document" title="Copy">📋</button> <button type="button" class="icon-btn" @click="copyText(wellKnownJson, 'Well-known document')" aria-label="Copy well-known document" title="Copy">📋</button>
</div> </div>
<ul v-if="dialog.data.wellKnownCheck" class="diag-list">
<li v-if="dialog.data.wellKnownCheck === 'validating'" class="small muted">Checking the published document…</li>
<li v-else-if="dialog.data.wellKnownCheck === 'valid'" class="small muted">✓ The published document lists all related origins.</li>
<li v-else-if="dialog.data.wellKnownCheck === 'missing'" class="small error">The published document does not list: {{ (dialog.data.wellKnownMissing || []).join(', ') }}</li>
<li v-else-if="dialog.data.wellKnownCheck === 'unreachable'" class="small muted">Could not fetch the published document to verify it.</li>
</ul>
</template> </template>
</template> </template>
<template v-else-if="dialog.type==='confirm'"> <template v-else-if="dialog.type==='confirm'">
+22
View File
@@ -490,6 +490,22 @@ th {
left: 0; left: 0;
} }
/* The diagnostics panel attaches below the dialog, so feedback appearing
or disappearing never shifts the dialog's layout or position */
.diag-panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
right: 0;
background: var(--color-dialog);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
padding: var(--space-md) var(--space-lg);
max-height: 30vh;
overflow-y: auto;
}
.icon-btn { .icon-btn {
background: none; background: none;
border: none; border: none;
@@ -599,6 +615,12 @@ th {
padding: 1.5rem; padding: 1.5rem;
} }
/* Positions attachments (e.g. the diagnostics panel) relative to the
dialog; shrink-wraps the panel in the overlay's flex layout */
.modal-wrap {
position: relative;
}
.device-dialog, .device-dialog,
.modal { .modal {
background: var(--color-dialog); background: var(--color-dialog);
+5 -2
View File
@@ -1,7 +1,10 @@
<template> <template>
<div class="dialog-overlay" @click="$emit('close')"> <div class="dialog-overlay" @click="$emit('close')">
<div ref="dialog" :class="['modal-panel', panelClass]" @keydown="handleDialogKeydown" @click.stop> <div class="modal-wrap">
<slot /> <div ref="dialog" :class="['modal-panel', panelClass]" @keydown="handleDialogKeydown" @click.stop>
<slot />
</div>
<slot name="attached" />
</div> </div>
</div> </div>
</template> </template>