feat(ui): ErrorDetailModal — context panel (task/queue/target/duration/started/retries/worker/celery-id/args) + contrast fix (background-token bg vs surface-variant pale-on-pale) + copyText helper for Copy button
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,20 @@
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center" style="gap: 12px;">
|
||||
<v-icon icon="mdi-alert-circle-outline" color="error" />
|
||||
<span>{{ title }}</span>
|
||||
<span>{{ displayTitle }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon variant="text" size="small" @click="close">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="fc-err-pre">{{ message || '(no error message)' }}</pre>
|
||||
<dl v-if="contextRows.length" class="fc-err-context">
|
||||
<template v-for="(row, idx) in contextRows" :key="idx">
|
||||
<dt>{{ row[0] }}</dt>
|
||||
<dd>{{ row[1] }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<pre class="fc-err-pre">{{ displayMessage || '(no error message)' }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
@@ -27,12 +33,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { copyText } from '../../utils/clipboard.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
// Legacy mode: pass title + message strings. Used by callers whose row
|
||||
// shape lacks structured context (e.g. ImportTaskList where `error` is
|
||||
// a plain string on the import_task row, not a TaskRun).
|
||||
title: { type: String, default: 'Error details' },
|
||||
message: { type: String, default: '' },
|
||||
// Row mode: pass the full TaskRun-shaped dict from /api/system_activity.
|
||||
// When set, displayTitle/displayMessage derive from the row and a context
|
||||
// panel of task_name/queue/target/duration/etc. renders above the error.
|
||||
row: { type: Object, default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@@ -40,6 +55,47 @@ const emit = defineEmits(['update:modelValue'])
|
||||
const copied = ref(false)
|
||||
let copiedTimer = null
|
||||
|
||||
const displayTitle = computed(() => {
|
||||
if (props.row) return props.row.error_type || 'Error details'
|
||||
return props.title
|
||||
})
|
||||
|
||||
const displayMessage = computed(() => {
|
||||
if (props.row) return props.row.error_message || ''
|
||||
return props.message
|
||||
})
|
||||
|
||||
function _shortTaskName (name) {
|
||||
if (!name) return ''
|
||||
const parts = String(name).split('.')
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
function _formatDuration (ms) {
|
||||
if (ms == null) return null
|
||||
if (ms < 1000) return `${ms} ms`
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`
|
||||
return `${(ms / 60_000).toFixed(1)} min`
|
||||
}
|
||||
|
||||
const contextRows = computed(() => {
|
||||
const r = props.row
|
||||
if (!r) return []
|
||||
const rows = []
|
||||
if (r.task_name) rows.push(['Task', _shortTaskName(r.task_name)])
|
||||
if (r.queue) rows.push(['Queue', r.queue])
|
||||
if (r.target_id != null) rows.push(['Target', r.target_id])
|
||||
const dur = _formatDuration(r.duration_ms)
|
||||
if (dur != null) rows.push(['Duration', dur])
|
||||
if (r.started_at) rows.push(['Started', r.started_at])
|
||||
if (r.finished_at) rows.push(['Finished', r.finished_at])
|
||||
if (r.retry_count) rows.push(['Retries', r.retry_count])
|
||||
if (r.worker_hostname) rows.push(['Worker', r.worker_hostname])
|
||||
if (r.celery_task_id) rows.push(['Celery ID', r.celery_task_id])
|
||||
if (r.args_summary) rows.push(['Args', r.args_summary])
|
||||
return rows
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (!open) {
|
||||
copied.value = false
|
||||
@@ -47,13 +103,18 @@ watch(() => props.modelValue, (open) => {
|
||||
}
|
||||
})
|
||||
|
||||
function close() {
|
||||
function close () {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
async function onCopy() {
|
||||
async function onCopy () {
|
||||
let text = displayMessage.value || ''
|
||||
if (contextRows.value.length) {
|
||||
const header = contextRows.value.map(([k, v]) => `${k}: ${v}`).join('\n')
|
||||
text = `${header}\n\nError: ${displayTitle.value}\n${text}`
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.message || '')
|
||||
await copyText(text)
|
||||
copied.value = true
|
||||
if (copiedTimer) clearTimeout(copiedTimer)
|
||||
copiedTimer = setTimeout(() => { copied.value = false }, 1500)
|
||||
@@ -64,19 +125,43 @@ async function onCopy() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* The full error often contains a SQLAlchemy statement + parameters
|
||||
block + multi-line traceback. Pre-wrap keeps long lines readable;
|
||||
monospace + tabular layout keeps the structure scannable. The
|
||||
max-height + overflow-auto prevents a 50-line traceback from
|
||||
pushing the Close button off-screen. Operator-flagged 2026-05-26:
|
||||
the prior :title="..." tooltip was unusable for content this long. */
|
||||
/* Context panel: muted labels in vellum, crisp values in parchment. The
|
||||
2-column key/value grid keeps rows visually scannable when there are
|
||||
many fields (Task, Queue, Target, Duration, Started, Finished, Retries,
|
||||
Worker, Celery ID, Args). */
|
||||
.fc-err-context {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
column-gap: 14px;
|
||||
row-gap: 4px;
|
||||
margin: 0 0 14px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fc-err-context dt {
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fc-err-context dd {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
margin: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Error pre block: high-contrast pairing. The page's `background` token
|
||||
(obsidian #14171A) is darker than the modal card's `surface` (iron
|
||||
#1E2228), so parchment text reads crisply against it. The prior pairing
|
||||
used `surface-variant` which Vuetify auto-derives to a near-parchment
|
||||
light value in this theme — pale-on-pale and unreadable. Operator-
|
||||
flagged 2026-05-26 ("ui contrast is poor"). */
|
||||
.fc-err-pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: rgb(var(--v-theme-surface-variant, 38 36 41));
|
||||
background: rgb(var(--v-theme-background));
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<td>
|
||||
<button
|
||||
type="button" class="fc-err-link"
|
||||
@click="openError(r.error_type, r.error_message)"
|
||||
@click="openError(r)"
|
||||
:title="'Click for full error'"
|
||||
>{{ r.error_type }}</button>
|
||||
</td>
|
||||
@@ -145,8 +145,7 @@
|
||||
|
||||
<ErrorDetailModal
|
||||
v-model="showErrorModal"
|
||||
:title="errorModalTitle"
|
||||
:message="errorModalMessage"
|
||||
:row="errorModalRow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -163,12 +162,10 @@ import QueuesTable from './QueuesTable.vue'
|
||||
// rollback + traceback content rendered as a cramped browser tooltip
|
||||
// you couldn't copy from or scroll within).
|
||||
const showErrorModal = ref(false)
|
||||
const errorModalTitle = ref('')
|
||||
const errorModalMessage = ref('')
|
||||
const errorModalRow = ref(null)
|
||||
|
||||
function openError(title, message) {
|
||||
errorModalTitle.value = title || 'Error details'
|
||||
errorModalMessage.value = message || ''
|
||||
function openError(row) {
|
||||
errorModalRow.value = row
|
||||
showErrorModal.value = true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user