536 lines
25 KiB
Vue
536 lines
25 KiB
Vue
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import { router, Link } from '@inertiajs/vue3'
|
|
import { useI18n } from 'vue-i18n'
|
|
import TitleBar from '../components/TitleBar.vue'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { device, modelPaths, commands } = defineProps<{
|
|
device: any
|
|
modelPaths: string[]
|
|
commands: any[]
|
|
}>()
|
|
|
|
const isLoading = ref(false)
|
|
const showDeleteConfirm = ref(false)
|
|
const isTraced = ref(false)
|
|
const tracingAction = ref(false)
|
|
|
|
// Check if device is already traced
|
|
async function checkTraceStatus() {
|
|
try {
|
|
const response = await fetch('/api/v1/tr069-trace')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
const entries = data.entries || []
|
|
isTraced.value = entries.some((e: any) => e.deviceId === device.identifier)
|
|
}
|
|
} catch {
|
|
// Silently ignore
|
|
}
|
|
}
|
|
|
|
async function toggleTrace() {
|
|
tracingAction.value = true
|
|
try {
|
|
if (isTraced.value) {
|
|
const response = await fetch(`/api/v1/tr069-trace/${encodeURIComponent(device.identifier)}`, {
|
|
method: 'DELETE',
|
|
})
|
|
if (response.ok || response.status === 204) {
|
|
isTraced.value = false
|
|
}
|
|
} else {
|
|
const response = await fetch('/api/v1/tr069-trace', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ deviceId: device.identifier }),
|
|
})
|
|
if (response.ok) {
|
|
isTraced.value = true
|
|
}
|
|
}
|
|
} catch {
|
|
// Silently ignore
|
|
} finally {
|
|
tracingAction.value = false
|
|
}
|
|
}
|
|
|
|
checkTraceStatus()
|
|
|
|
function deleteDevice() {
|
|
router.delete(`/devices/${encodeURIComponent(device.identifier)}`, {
|
|
onSuccess: () => router.visit('/devices'),
|
|
})
|
|
}
|
|
const showGetModelPopup = ref(false)
|
|
const getModelInput = ref('')
|
|
const activeSection = ref('identifiers')
|
|
|
|
// Collapsible sections
|
|
const openSections = ref<Set<string>>(new Set(['identifiers', 'commands']))
|
|
|
|
function toggleSection(section: string) {
|
|
if (openSections.value.has(section)) {
|
|
openSections.value.delete(section)
|
|
} else {
|
|
openSections.value.add(section)
|
|
}
|
|
}
|
|
|
|
function isSectionOpen(section: string) {
|
|
return openSections.value.has(section)
|
|
}
|
|
|
|
function scrollTo(section: string) {
|
|
activeSection.value = section
|
|
document.getElementById(section)?.scrollIntoView({ behavior: 'smooth' })
|
|
}
|
|
|
|
function refreshDevice() {
|
|
isLoading.value = true
|
|
router.post(`/devices/${encodeURIComponent(device.identifier)}/refresh`, {}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
setTimeout(() => {
|
|
isLoading.value = false
|
|
router.visit(`/devices/${encodeURIComponent(device.identifier)}`, { preserveScroll: true })
|
|
}, 2000)
|
|
},
|
|
onFinish: () => { isLoading.value = false },
|
|
})
|
|
}
|
|
|
|
function openGetModelPopup() {
|
|
getModelInput.value = modelPaths.length > 0 ? modelPaths[0] : ''
|
|
showGetModelPopup.value = true
|
|
}
|
|
|
|
function submitGetModel() {
|
|
if (!getModelInput.value.trim()) return
|
|
isLoading.value = true
|
|
router.post(`/devices/${encodeURIComponent(device.identifier)}/get-model`, {
|
|
modelPath: getModelInput.value.trim(),
|
|
}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
showGetModelPopup.value = false
|
|
setTimeout(() => {
|
|
isLoading.value = false
|
|
router.visit(`/devices/${encodeURIComponent(device.identifier)}`, { preserveScroll: true })
|
|
}, 2000)
|
|
},
|
|
onFinish: () => { isLoading.value = false },
|
|
})
|
|
}
|
|
|
|
function formatName(name: unknown): string {
|
|
if (name == null) return ''
|
|
const str = typeof name === 'string' ? name : String(name)
|
|
return str.replace(/\./g, '.<wbr>')
|
|
}
|
|
|
|
function getPropertyValue(name: string): string {
|
|
const prop = device.properties?.find((p: any) => p.name === name)
|
|
return prop?.value ?? ''
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<TitleBar
|
|
section="device"
|
|
:subtitle="`${device.deviceId?.oui}_${device.deviceId?.productClass}_${device.deviceId?.serialNumber}`"
|
|
/>
|
|
|
|
<div class="container-xxl py-4">
|
|
<div class="row g-4">
|
|
<!-- Side Navigation -->
|
|
<div class="col-lg-1 d-none d-lg-block">
|
|
<nav class="sidenav sticky-top pt-3">
|
|
<ul class="nav flex-column">
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeSection === 'identifiers' }" href="javascript:void(0);" @click="scrollTo('identifiers')">
|
|
Identité
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeSection === 'properties' }" href="javascript:void(0);" @click="scrollTo('properties')">
|
|
{{ t('labels.properties') }}
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeSection === 'parameters' }" href="javascript:void(0);" @click="scrollTo('parameters')">
|
|
{{ t('labels.parameters') }}
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeSection === 'actions' }" href="javascript:void(0);" @click="scrollTo('actions')">
|
|
Actions
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{ active: activeSection === 'commands' }" href="javascript:void(0);" @click="scrollTo('commands')">
|
|
{{ t('titles.commands') }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="col-lg-11">
|
|
|
|
<!-- Section: Identité — une seule carte englobante -->
|
|
<div id="identifiers" class="mb-3">
|
|
<div class="card border-0 shadow-sm" style="background:#fff;border-radius:10px!important">
|
|
|
|
<!-- En-tête de la carte -->
|
|
<div class="px-3 pt-3 pb-2">
|
|
<span class="text-uppercase fw-bold" style="font-size:0.65rem;letter-spacing:.1em;color:#888">Identité</span>
|
|
</div>
|
|
|
|
<div class="px-3 pb-3 d-flex flex-column gap-2">
|
|
|
|
<!-- Zone 1 : Identifiant -->
|
|
<div class="rounded-3 px-3 py-3" style="background:#f6f7f9">
|
|
<div class="text-muted fw-bold text-uppercase mb-3" style="font-size:0.62rem;letter-spacing:.08em">Identifiant</div>
|
|
<div class="row g-3">
|
|
<div class="col-sm-6 col-lg-3">
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.manufacturer') }}</div>
|
|
<div class="fw-semibold fs-6">{{ device.deviceId?.manufacturer || '—' }}</div>
|
|
</div>
|
|
<div class="col-sm-6 col-lg-3">
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">OUI</div>
|
|
<div class="fw-semibold fs-6 font-monospace">{{ device.deviceId?.oui || '—' }}</div>
|
|
</div>
|
|
<div class="col-sm-6 col-lg-3">
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.productClass') }}</div>
|
|
<div class="fw-semibold fs-6">{{ device.deviceId?.productClass || '—' }}</div>
|
|
</div>
|
|
<div class="col-sm-6 col-lg-3">
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.serialNumber') }}</div>
|
|
<div class="fw-semibold fs-6 font-monospace">{{ device.deviceId?.serialNumber || '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zones 2+3 : Versions + Système côte à côte -->
|
|
<div class="row g-2">
|
|
|
|
<!-- Zone 2 : Versions -->
|
|
<div class="col-lg-5">
|
|
<div class="rounded-3 px-3 py-3 h-100" style="background:#f6f7f9">
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 1000 1000" class="text-muted flex-shrink-0">
|
|
<path d="M500 75.5c-234.72 0-425 190.28-425 425s190.28 425 425 425 425-190.28 425-425-190.28-425-425-425m0 100a75 75 0 1 1-75 75 75 75 0 0 1 75-75m125 600H400v-25.737l5.287-.167c15.555-.5 27.115-4.919 34.368-13.136 2.576-2.972 6.956-13.1 6.956-47.153v-227.2c0-32.392-4.523-44.6-8.311-49.141-5.359-6.391-16.516-10.147-33.17-11.156l-5.13-.31v-26h175v313.807c0 32.415 8.457 44.618 12.24 49.142 5.34 6.389 15.68 10.142 32.625 11.152l5.135.3v25.6Z" style="fill-rule:evenodd"/>
|
|
</svg>
|
|
<span class="text-muted fw-bold text-uppercase" style="font-size:0.62rem;letter-spacing:.08em">Versions</span>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-6">
|
|
<div class="text-muted fw-bold text-uppercase mb-2" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.softwareVersion') }}</div>
|
|
<span class="font-monospace fw-medium small">{{ getPropertyValue('InternetGatewayDevice.DeviceInfo.SoftwareVersion') || getPropertyValue('Device.DeviceInfo.SoftwareVersion') || '—' }}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-muted fw-bold text-uppercase mb-2" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.hardwareVersion') }}</div>
|
|
<span class="font-monospace fw-medium small">{{ getPropertyValue('InternetGatewayDevice.DeviceInfo.HardwareVersion') || getPropertyValue('Device.DeviceInfo.HardwareVersion') || '—' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zone 3 : Système -->
|
|
<div class="col-lg-7">
|
|
<div class="rounded-3 px-3 py-3 h-100" style="background:#f6f7f9">
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 1000 1000" class="text-muted flex-shrink-0">
|
|
<path d="M827.568 500.5c0-110.61 120.715-26.921 64.516-162.6-56.231-135.681-82.391 8.823-160.606-69.384s66.333-104.365-69.36-160.562c-135.718-56.2-52.024 64.481-162.61 64.481-110.619 0-26.921-120.68-162.612-64.481s8.827 82.349-69.391 160.562-104.372-66.3-160.575 69.384c-56.23 135.679 64.486 51.99 64.486 162.6S50.7 527.419 106.93 663.1c56.2 135.681 82.359-8.823 160.575 69.352 78.218 78.214-66.3 104.4 69.391 160.6s51.993-64.483 162.612-64.483c110.586 0 26.892 120.678 162.61 64.483 135.693-56.2-8.854-82.384 69.36-160.6 78.215-78.175 104.375 66.329 160.606-69.352 56.199-135.681-64.516-51.99-64.516-162.6M500 301.015c110.457 0 200 89.537 200 199.985s-89.543 199.985-200 199.985S300 611.448 300 501s89.543-199.985 200-199.985" style="fill-rule:evenodd"/>
|
|
</svg>
|
|
<span class="text-muted fw-bold text-uppercase" style="font-size:0.62rem;letter-spacing:.08em">Système</span>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.identifier') }} (ACS)</div>
|
|
<div class="font-monospace small text-break">{{ device.identifier || '—' }}</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">Provisioning Code</div>
|
|
<div class="font-monospace small">{{ device.provisioningCode || '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Zones 4+5 : dates côte à côte -->
|
|
<div class="row g-2">
|
|
|
|
<!-- Zone 4 : Créé le -->
|
|
<div class="col-sm-6">
|
|
<div class="rounded-3 px-3 py-3 d-flex align-items-center gap-3" style="background:#f6f7f9">
|
|
<div class="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
|
|
style="width:34px;height:34px;background:#e4e6ea">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 1000 1000" class="text-muted">
|
|
<path d="M800 175H700v-38c0-20.71-16.789-37-37.5-37a37.5 37.5 0 0 0-37.5 37.5V175H350.011L350 137.5a37.5 37.5 0 0 0-75 0V175H100v625a75 75 0 0 0 75 75h700V250a75 75 0 0 0-75-75m25 650H187.5a37.5 37.5 0 0 1-37.5-37.5V400h637.5a37.5 37.5 0 0 1 37.5 37.5zM250 262.5a62.4 62.4 0 0 1 25-50v50a37.5 37.5 0 0 0 75 0V250h.011v-37.493A62.5 62.5 0 1 1 250 262.5m350 0a62.4 62.4 0 0 1 25-50v50a37.5 37.5 0 0 0 75 0l-.011-12.5H700v-37a61.47 61.47 0 0 1 25 49.5 62.5 62.5 0 0 1-125 0" style="fill-rule:evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.createdAt') }}</div>
|
|
<div class="font-monospace small fw-medium">{{ device.createdAt ? new Date(device.createdAt).toLocaleString() : '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zone 5 : Dernière connexion -->
|
|
<div class="col-sm-6">
|
|
<div class="rounded-3 px-3 py-3 d-flex align-items-center gap-3" style="background:#f6f7f9">
|
|
<div class="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
|
|
style="width:34px;height:34px;background:#e4e6ea">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 1000 1000" class="text-muted">
|
|
<path d="M800 175H700v-38c0-20.71-16.789-37-37.5-37a37.5 37.5 0 0 0-37.5 37.5V175H350.011L350 137.5a37.5 37.5 0 0 0-75 0V175H100v625a75 75 0 0 0 75 75h700V250a75 75 0 0 0-75-75M300 825H187.5a37.5 37.5 0 0 1-37.5-37.5V400h150zm-50-562.5a62.4 62.4 0 0 1 25-50v50a37.5 37.5 0 0 0 75 0V250h.011v-37.493A62.5 62.5 0 1 1 250 262.5M475 825H325V400h150zm175 0H500V400h150zm-50-562.5a62.4 62.4 0 0 1 25-50v50a37.5 37.5 0 0 0 75 0l-.011-12.5H700v-37a61.47 61.47 0 0 1 25 49.5 62.5 62.5 0 0 1-125 0M825 825H675V400h112.5a37.5 37.5 0 0 1 37.5 37.5z" style="fill-rule:evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="text-muted fw-bold text-uppercase mb-1" style="font-size:0.62rem;letter-spacing:.06em">{{ t('labels.lastSeen') }}</div>
|
|
<div class="font-monospace small fw-medium">{{ device.updatedAt ? new Date(device.updatedAt).toLocaleString() : '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section: Properties (collapsible) -->
|
|
<div class="accordion-section mb-3" id="properties">
|
|
<div class="section-header" @click="toggleSection('properties')">
|
|
<h2 class="h6 mb-0">{{ t('labels.properties') }} ({{ device.properties?.filter((p: any) => p.value != null).length || 0 }})</h2>
|
|
<span class="chevron" :class="{ open: isSectionOpen('properties') }">▾</span>
|
|
</div>
|
|
<div v-if="isSectionOpen('properties')" class="section-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{{ t('labels.name') }}</th>
|
|
<th>{{ t('labels.value') }}</th>
|
|
<th>{{ t('labels.type') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="prop in device.properties?.filter((p: any) => p.name != null && p.value != null)" :key="prop.name">
|
|
<td class="multi-line small" v-html="formatName(prop.name)"></td>
|
|
<td class="multi-line small font-monospace" v-html="formatName(prop.value)"></td>
|
|
<td class="small">{{ prop.type }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section: Parameters (collapsible) -->
|
|
<div class="accordion-section mb-3" id="parameters">
|
|
<div class="section-header" @click="toggleSection('parameters')">
|
|
<h2 class="h6 mb-0">{{ t('labels.parameters') }} ({{ device.parameters?.filter((p: any) => p.value != null).length || 0 }})</h2>
|
|
<span class="chevron" :class="{ open: isSectionOpen('parameters') }">▾</span>
|
|
</div>
|
|
<div v-if="isSectionOpen('parameters')" class="section-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{{ t('labels.name') }}</th>
|
|
<th>{{ t('labels.value') }}</th>
|
|
<th>{{ t('labels.type') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="param in device.parameters?.filter((p: any) => p.name != null && p.value != null)" :key="param.name">
|
|
<td class="multi-line small" v-html="formatName(param.name)"></td>
|
|
<td class="multi-line small font-monospace" v-html="formatName(param.value)"></td>
|
|
<td class="small">{{ param.type }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section: Actions -->
|
|
<div class="card border-0 shadow-sm mb-3" id="actions">
|
|
<div class="card-body p-4">
|
|
<h2 class="h6 fw-bold mb-3">Actions</h2>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<button class="btn btn-outline-dark btn-sm" @click="refreshDevice" :disabled="isLoading">
|
|
{{ t('button.refresh') }}
|
|
</button>
|
|
<button class="btn btn-outline-dark btn-sm" @click="openGetModelPopup" :disabled="isLoading">
|
|
{{ t('button.getModel') }}
|
|
</button>
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="isTraced ? 'btn-warning' : 'btn-outline-primary'"
|
|
:disabled="tracingAction"
|
|
@click="toggleTrace"
|
|
>
|
|
<svg v-if="!isTraced" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
|
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
|
|
</svg>
|
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293z"/>
|
|
</svg>
|
|
{{ tracingAction ? '…' : (isTraced ? 'Remove TR-069 monitoring' : 'Add to TR-069 monitoring') }}
|
|
</button>
|
|
<button class="btn btn-outline-danger btn-sm ms-auto" @click="showDeleteConfirm = true">
|
|
Supprimer l'équipement
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Confirmation suppression -->
|
|
<div v-if="showDeleteConfirm" class="alert alert-danger d-flex align-items-center gap-3 mt-3 mb-0">
|
|
<span>Confirmer la suppression de <strong>{{ device.identifier }}</strong> ?</span>
|
|
<button class="btn btn-danger btn-sm" @click="deleteDevice">Confirmer</button>
|
|
<button class="btn btn-outline-secondary btn-sm" @click="showDeleteConfirm = false">Annuler</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section: Commands for this device -->
|
|
<div class="accordion-section mb-3" id="commands">
|
|
<div class="section-header" @click="toggleSection('commands')">
|
|
<h2 class="h6 mb-0">{{ t('titles.commands') }} ({{ commands?.length || 0 }})</h2>
|
|
<span class="chevron" :class="{ open: isSectionOpen('commands') }">▾</span>
|
|
</div>
|
|
<div v-if="isSectionOpen('commands')" class="section-body">
|
|
<div v-if="!commands || commands.length === 0" class="text-muted small">
|
|
Aucune commande pour cet équipement.
|
|
</div>
|
|
<div v-else class="table-responsive">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{{ t('labels.action') }}</th>
|
|
<th>{{ t('labels.status') }}</th>
|
|
<th>{{ t('labels.mode') }}</th>
|
|
<th>{{ t('labels.creationDate') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="cmd in commands" :key="cmd._id">
|
|
<td class="small">{{ cmd.action }}</td>
|
|
<td>
|
|
<span class="badge" :class="{
|
|
'bg-success': cmd.status === 'COMPLETED',
|
|
'bg-danger': cmd.status === 'FAILED',
|
|
'bg-warning text-dark': cmd.status === 'PENDING',
|
|
'bg-info': cmd.status === 'SENT',
|
|
'bg-secondary': cmd.status === 'WAITING',
|
|
}">{{ cmd.status }}</span>
|
|
</td>
|
|
<td class="small">{{ cmd.mode }}</td>
|
|
<td class="small">{{ cmd.createdAt ? new Date(cmd.createdAt).toLocaleString() : '' }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-3">
|
|
<Link :href="`/commands?device=${encodeURIComponent(device.identifier)}`" class="btn btn-outline-dark btn-sm">
|
|
Voir tout
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Get Model Modal -->
|
|
<teleport to="body">
|
|
<div v-if="showGetModelPopup" class="modal-backdrop fade show"></div>
|
|
<div v-if="showGetModelPopup" class="modal fade show d-block" tabindex="-1" aria-modal="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{{ t('modal.getModel') }}</h5>
|
|
<button type="button" class="btn-close" @click="showGetModelPopup = false"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<select v-if="modelPaths.length > 0" class="form-select" v-model="getModelInput">
|
|
<option value="" disabled>{{ t('placeholder.modelPath') }}</option>
|
|
<option v-for="path in modelPaths" :key="path" :value="path">{{ path }}</option>
|
|
</select>
|
|
<input v-else type="text" class="form-control" v-model="getModelInput" :placeholder="t('placeholder.modelPath')" />
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" @click="showGetModelPopup = false" :disabled="isLoading">{{ t('button.cancel') }}</button>
|
|
<button class="btn btn-primary" @click="submitGetModel" :disabled="isLoading">{{ t('button.send') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.sidenav .nav-link {
|
|
color: #666;
|
|
padding: 0.4rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
border-left: 3px solid transparent;
|
|
}
|
|
|
|
.sidenav .nav-link.active {
|
|
color: #000;
|
|
font-weight: 600;
|
|
border-left-color: #ff7900;
|
|
}
|
|
|
|
.accordion-section {
|
|
background: #fff;
|
|
border-radius: 0.375rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.section-header:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.section-body {
|
|
padding: 0 1.5rem 1.5rem;
|
|
}
|
|
|
|
.chevron {
|
|
transition: transform 0.2s;
|
|
font-size: 1.2rem;
|
|
color: #666;
|
|
}
|
|
|
|
.chevron.open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
td.multi-line {
|
|
white-space: normal;
|
|
word-wrap: break-word;
|
|
max-width: 400px;
|
|
}
|
|
</style>
|