- DeviceDetail: display compliance status with badge + parameter diffs - Home: show compliance badge in device list - ConfigurationDetail: compliance report tab with device list + diffs - ConfigurationEdit: bumpType selector (patch/minor/major), remove version field - ConfigurationsList: tag filter, name search, compliance column - ComplianceBadge: pending/compliant/in_progress/failed/non_compliant states - CriteriaEditor: parameterCriteria editor with autocomplete on parameter names - AutocompleteInput: new reusable autocomplete component - i18n: compliance labels in fr/en Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
168 lines
5.3 KiB
Vue
168 lines
5.3 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
import { router } from '@inertiajs/vue3'
|
|
import { useI18n } from 'vue-i18n'
|
|
import TitleBar from '../components/TitleBar.vue'
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { devices } = defineProps<{
|
|
message: string
|
|
devices: any[]
|
|
}>()
|
|
|
|
const selected = ref<boolean[]>([])
|
|
|
|
onMounted(() => {
|
|
const saved = localStorage.getItem('selectedDevices')
|
|
if (saved) {
|
|
try {
|
|
const savedSelectedIds: string[] = JSON.parse(saved)
|
|
selected.value = devices.map((device) => savedSelectedIds.includes(device.identifier))
|
|
return
|
|
} catch { /* ignore */ }
|
|
}
|
|
selected.value = devices.map(() => false)
|
|
})
|
|
|
|
watch(selected, (newSelected) => {
|
|
const selectedIds = devices
|
|
.filter((_, index) => newSelected[index])
|
|
.map((device) => device.identifier)
|
|
localStorage.setItem('selectedDevices', JSON.stringify(selectedIds))
|
|
}, { deep: true })
|
|
|
|
const allSelected = computed({
|
|
get() {
|
|
return selected.value.length > 0 && selected.value.every(Boolean)
|
|
},
|
|
set(value: boolean) {
|
|
selected.value = selected.value.map(() => value)
|
|
},
|
|
})
|
|
|
|
function toggleSelectAll() {
|
|
allSelected.value = !allSelected.value
|
|
}
|
|
|
|
function getParameterValue(params: any[], name: string): string {
|
|
const param = params?.find((p: any) => p.name === name)
|
|
return param?.value ?? ''
|
|
}
|
|
|
|
function getPropertyValue(props: any[], name: string): string {
|
|
const prop = props?.find((p: any) => p.name === name)
|
|
return prop?.value ?? ''
|
|
}
|
|
|
|
function goToDevice(identifier: string) {
|
|
router.visit(`/devices/${encodeURIComponent(identifier)}`)
|
|
}
|
|
|
|
const selectedIdentifiers = computed(() => {
|
|
return devices
|
|
.filter((_, index) => selected.value[index])
|
|
.map((device) => device.identifier)
|
|
})
|
|
|
|
function goToFleetManagement() {
|
|
if (selectedIdentifiers.value.length === 0) return
|
|
router.post('/commands/fleet', { devices: selectedIdentifiers.value })
|
|
}
|
|
|
|
const isDeleting = ref(false)
|
|
|
|
async function deleteSelected() {
|
|
if (selectedIdentifiers.value.length === 0 || isDeleting.value) return
|
|
isDeleting.value = true
|
|
try {
|
|
await Promise.all(
|
|
selectedIdentifiers.value.map((id) =>
|
|
fetch(`/api/v1/devices/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
)
|
|
)
|
|
router.reload()
|
|
} finally {
|
|
isDeleting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<TitleBar section="devices" />
|
|
|
|
<div class="container-xxl py-4">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover has-checkbox">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">
|
|
<div class="form-check mb-0">
|
|
<input
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
id="selectAll"
|
|
v-model="allSelected"
|
|
/>
|
|
<label class="form-check-label" for="selectAll">
|
|
<span class="visually-hidden">{{ t('checkbox.selectAll') }}</span>
|
|
</label>
|
|
</div>
|
|
</th>
|
|
<th scope="col">{{ t('labels.manufacturer') }}</th>
|
|
<th scope="col">{{ t('labels.productClass') }}</th>
|
|
<th scope="col">{{ t('labels.serialNumber') }}</th>
|
|
<th scope="col">{{ t('labels.externalIP') }}</th>
|
|
<th scope="col">{{ t('labels.softwareVersion') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="(device, index) in devices"
|
|
:key="device.identifier"
|
|
:class="{ 'table-active': selected[index] }"
|
|
class="clickable-row"
|
|
@click="goToDevice(device.identifier)"
|
|
>
|
|
<td @click.stop>
|
|
<div class="form-check mb-0">
|
|
<input
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
:id="'select' + index"
|
|
v-model="selected[index]"
|
|
/>
|
|
<label class="form-check-label" :for="'select' + index">
|
|
<span class="visually-hidden">{{ t('checkbox.selectRow', { index: index + 1 }) }}</span>
|
|
</label>
|
|
</div>
|
|
</td>
|
|
<td>{{ device.deviceId?.manufacturer }}</td>
|
|
<td>{{ device.deviceId?.productClass }}</td>
|
|
<td>{{ device.deviceId?.serialNumber }}</td>
|
|
<td>{{ getParameterValue(device.parameters, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress') || getPropertyValue(device.properties, 'Device.IP.Interface.1.IPv4Address.1.IPAddress') || '—' }}</td>
|
|
<td>{{ getPropertyValue(device.properties, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion') || getPropertyValue(device.properties, 'Device.DeviceInfo.SoftwareVersion') || '—' }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-3 d-flex gap-2">
|
|
<button
|
|
class="btn btn-primary"
|
|
@click="goToFleetManagement"
|
|
:disabled="selectedIdentifiers.length === 0"
|
|
>
|
|
{{ t('button.fleetManagement', { count: selectedIdentifiers.length }) }}
|
|
</button>
|
|
<button
|
|
class="btn btn-outline-danger"
|
|
@click="deleteSelected"
|
|
:disabled="selectedIdentifiers.length === 0 || isDeleting"
|
|
>
|
|
{{ isDeleting ? 'Suppression…' : `Supprimer (${selectedIdentifiers.length})` }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|