feat: dry-run modal on non-compliant badge click
Replace the separate Dry Run tab with a modal triggered by clicking a non-compliant badge in the Compliance tab. Shows parameter diffs, firmware path, and a link to the device page.
This commit is contained in:
1 parent
0109bca236
commit
bf98c9f405
1 file changed
+97
-76
@@ -13,11 +13,12 @@ const configuration = ref<any>(null)
|
||||
const versionHistory = ref<any[]>([])
|
||||
const activeTab = ref('general')
|
||||
|
||||
// Dry run state
|
||||
// Dry run state (modal)
|
||||
const dryRunDeviceId = ref('')
|
||||
const dryRunLoading = ref(false)
|
||||
const dryRunResult = ref<any>(null)
|
||||
const dryRunError = ref('')
|
||||
const showDryRunModal = ref(false)
|
||||
|
||||
// Compliance state
|
||||
const complianceLoading = ref(false)
|
||||
@@ -30,7 +31,6 @@ const tabs = [
|
||||
{ key: 'inventory', label: t('configDetail.tabInventory') },
|
||||
{ key: 'parameters', label: t('configDetail.tabParameters') },
|
||||
{ key: 'compliance', label: t('configDetail.tabCompliance') },
|
||||
{ key: 'dryrun', label: t('configDetail.tabDryRun') },
|
||||
{ key: 'history', label: t('configDetail.tabHistory') },
|
||||
]
|
||||
|
||||
@@ -57,16 +57,21 @@ async function loadHistory() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function runDryRun() {
|
||||
if (!dryRunDeviceId.value.trim()) return
|
||||
async function runDryRun(deviceIdentifier?: string) {
|
||||
const targetDevice = deviceIdentifier || dryRunDeviceId.value.trim()
|
||||
if (!targetDevice) return
|
||||
|
||||
dryRunDeviceId.value = targetDevice
|
||||
dryRunLoading.value = true
|
||||
dryRunResult.value = null
|
||||
dryRunError.value = ''
|
||||
showDryRunModal.value = true
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/configurations/${props.id}/dry-run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ deviceIdentifier: dryRunDeviceId.value.trim() }),
|
||||
body: JSON.stringify({ deviceIdentifier: targetDevice }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
@@ -81,6 +86,12 @@ async function runDryRun() {
|
||||
}
|
||||
}
|
||||
|
||||
function closeDryRunModal() {
|
||||
showDryRunModal.value = false
|
||||
dryRunResult.value = null
|
||||
dryRunError.value = ''
|
||||
}
|
||||
|
||||
async function loadCompliance() {
|
||||
complianceLoading.value = true
|
||||
complianceReport.value = null
|
||||
@@ -307,7 +318,7 @@ function goToDevice(identifier: string) { router.get(`/devices/${identifier}`) }
|
||||
<td class="small font-monospace">{{ device.deviceId?.serialNumber ?? '—' }}</td>
|
||||
<td>
|
||||
<span v-if="device.compliant" class="badge bg-success">{{ t('configDetail.compliant') }}</span>
|
||||
<span v-else class="badge bg-danger">{{ t('configDetail.nonCompliant') }}</span>
|
||||
<span v-else class="badge bg-danger" style="cursor:pointer" @click.stop="runDryRun(device.identifier)">{{ t('configDetail.nonCompliant') }}</span>
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
<span v-if="device.parameterDiffs?.length">{{ device.parameterDiffs.length }} diff(s)</span>
|
||||
@@ -326,76 +337,6 @@ function goToDevice(identifier: string) { router.get(`/devices/${identifier}`) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dry Run -->
|
||||
<div v-show="activeTab === 'dryrun'" class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">{{ t('configDetail.dryRunDesc') }}</p>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">{{ t('labels.deviceIdentifier') }}</label>
|
||||
<input
|
||||
v-model="dryRunDeviceId"
|
||||
type="text"
|
||||
class="form-control form-control-sm font-monospace"
|
||||
:placeholder="t('configDetail.dryRunPlaceholder')"
|
||||
@keyup.enter="runDryRun"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-primary btn-sm w-100" :disabled="dryRunLoading || !dryRunDeviceId.trim()" @click="runDryRun">
|
||||
<span v-if="dryRunLoading" class="spinner-border spinner-border-sm me-1" />
|
||||
{{ t('configDetail.runDryRun') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dryRunError" class="alert alert-danger py-2 small">{{ dryRunError }}</div>
|
||||
|
||||
<template v-if="dryRunResult">
|
||||
<!-- Compliance status -->
|
||||
<div class="alert py-2 small mb-3"
|
||||
:class="dryRunResult.evaluation.isAlreadyCompliant ? 'alert-success' : 'alert-warning'">
|
||||
<strong>{{ dryRunResult.evaluation.isAlreadyCompliant ? t('configDetail.compliant') : t('configDetail.nonCompliant') }}</strong>
|
||||
<span v-if="dryRunResult.evaluation.skipFirmware" class="ms-2 text-muted">({{ t('configDetail.firmwareSkipped') }})</span>
|
||||
</div>
|
||||
|
||||
<!-- Parameter diffs -->
|
||||
<div v-if="dryRunResult.evaluation.parameterDiffs?.length" class="mb-3">
|
||||
<div class="fw-semibold small mb-2">{{ t('configDetail.paramDiffs') }}</div>
|
||||
<table class="table table-sm table-bordered font-monospace small mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{{ t('labels.parameterName') }}</th>
|
||||
<th>{{ t('configDetail.currentValue') }}</th>
|
||||
<th>{{ t('configDetail.targetValue') }}</th>
|
||||
<th>{{ t('labels.type') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="diff in dryRunResult.evaluation.parameterDiffs" :key="diff.name" class="table-warning">
|
||||
<td>{{ diff.name }}</td>
|
||||
<td class="text-danger">{{ diff.currentValue ?? t('configDetail.notSet') }}</td>
|
||||
<td class="text-success">{{ diff.targetValue }}</td>
|
||||
<td>{{ diff.type }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="text-muted small">{{ t('configDetail.noParamDiffs') }}</p>
|
||||
|
||||
<!-- Firmware migration -->
|
||||
<div v-if="dryRunResult.evaluation.firmwareMigrationPath?.length" class="mb-3">
|
||||
<div class="fw-semibold small mb-2">{{ t('configDetail.firmwarePath') }}</div>
|
||||
<ol class="small ps-3 mb-0">
|
||||
<li v-for="(step, i) in dryRunResult.evaluation.firmwareMigrationPath" :key="i">
|
||||
{{ step.from }} → {{ step.to }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History -->
|
||||
<div v-show="activeTab === 'history'" class="card border-0 shadow-sm"> <div class="card-body p-0">
|
||||
<div v-if="configuration || versionHistory.length">
|
||||
@@ -434,5 +375,85 @@ function goToDevice(identifier: string) { router.get(`/devices/${identifier}`) }
|
||||
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Dry Run Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showDryRunModal" class="modal-backdrop fade show" @click="closeDryRunModal"></div>
|
||||
<div
|
||||
v-if="showDryRunModal"
|
||||
class="modal fade show d-block"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="dryRunModalLabel"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="dryRunModalLabel" class="modal-title">
|
||||
Dry Run — <span class="font-monospace">{{ dryRunDeviceId }}</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" @click="closeDryRunModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div v-if="dryRunLoading" class="text-center py-4 text-muted">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>Evaluating…
|
||||
</div>
|
||||
|
||||
<div v-else-if="dryRunError" class="alert alert-danger py-2 small">{{ dryRunError }}</div>
|
||||
|
||||
<template v-else-if="dryRunResult">
|
||||
<!-- Compliance status -->
|
||||
<div class="alert py-2 small mb-3"
|
||||
:class="dryRunResult.evaluation.isAlreadyCompliant ? 'alert-success' : 'alert-warning'">
|
||||
<strong>{{ dryRunResult.evaluation.isAlreadyCompliant ? t('configDetail.compliant') : t('configDetail.nonCompliant') }}</strong>
|
||||
<span v-if="dryRunResult.evaluation.skipFirmware" class="ms-2 text-muted">({{ t('configDetail.firmwareSkipped') }})</span>
|
||||
</div>
|
||||
|
||||
<!-- Parameter diffs -->
|
||||
<div v-if="dryRunResult.evaluation.parameterDiffs?.length" class="mb-3">
|
||||
<div class="fw-semibold small mb-2">{{ t('configDetail.paramDiffs') }}</div>
|
||||
<table class="table table-sm table-bordered font-monospace small mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{{ t('labels.parameterName') }}</th>
|
||||
<th>{{ t('configDetail.currentValue') }}</th>
|
||||
<th>{{ t('configDetail.targetValue') }}</th>
|
||||
<th>{{ t('labels.type') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="diff in dryRunResult.evaluation.parameterDiffs" :key="diff.name" class="table-warning">
|
||||
<td>{{ diff.name }}</td>
|
||||
<td class="text-danger">{{ diff.currentValue ?? t('configDetail.notSet') }}</td>
|
||||
<td class="text-success">{{ diff.targetValue }}</td>
|
||||
<td>{{ diff.type }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="text-muted small">{{ t('configDetail.noParamDiffs') }}</p>
|
||||
|
||||
<!-- Firmware migration -->
|
||||
<div v-if="dryRunResult.evaluation.firmwareMigrationPath?.length" class="mb-3">
|
||||
<div class="fw-semibold small mb-2">{{ t('configDetail.firmwarePath') }}</div>
|
||||
<ol class="small ps-3 mb-0">
|
||||
<li v-for="(step, i) in dryRunResult.evaluation.firmwareMigrationPath" :key="i">
|
||||
{{ step.from }} → {{ step.to }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="goToDevice(dryRunDeviceId)">
|
||||
Voir le device
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" @click="closeDryRunModal">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
Reference in new issue
Block a user