<template>
<GnTable
:columns="columns"
:rows="devices"
:caption="caption"
>
<template #cell-device="{ row }">
<router-link
:to="{ name: 'device-detail', params: { id: String(row.id) } }"
class="device-link"
>
<div class="device-cell">
<div class="device-icon" v-html="typeIcon(row.device_type)" />
<div class="device-info">
<div class="device-name-row">
<strong>{{ row.name || row.alias || `Device #${row.id}` }}</strong>
<span class="status-dot" :class="connectionClass(row.connection_status)" aria-hidden="true" />
</div>
<small>{{ row.alias }} — {{ row.device_ip || '—' }}</small>
</div>
</div>
</router-link>
</template>
<template #cell-state="{ row }">
<DeviceChannelsState
:device-type="row.device_type"
:response="stateFor(row).response"
:loading="stateFor(row).status === 'loading'"
:error="stateFor(row).status === 'error' ? stateFor(row).message : null"
:connection-status="row.connection_status || 'unknown'"
/>
</template>
<template v-if="showActions" #cell-actions="{ row }">
<GnButton
variant="warning"
icon="ph-arrow-clockwise"
size="sm"
:loading="devicesStore.isRebooting(row.id)"
@click="reboot(row.id)"
>
Reboot
</GnButton>
</template>
</GnTable>
</template>
<script setup>
import { computed } from "vue";
import { useDevicesStore } from "../../stores/devices";
import { GnTable, GnButton, useToast } from "gnexus-ui-kit/vue";
import DeviceChannelsState from "../../features/devices/components/DeviceChannelsState.vue";
const props = defineProps({
devices: { type: Array, required: true },
showActions: { type: Boolean, default: true },
caption: { type: String, default: "Devices" },
});
const devicesStore = useDevicesStore();
const toast = useToast();
const TYPE_ICONS = {
relay: '<i class="ph ph-toggle-left"></i>',
button: '<i class="ph ph-hand-tap"></i>',
sensor: '<i class="ph ph-thermometer"></i>',
};
const columns = computed(() => {
const cols = [
{ key: "device", label: "Device" },
{ key: "state", label: "State" },
];
if (props.showActions) {
cols.push({ key: "actions", label: "Actions" });
}
return cols;
});
function typeIcon(type) {
return TYPE_ICONS[type] || TYPE_ICONS.relay;
}
function connectionClass(status) {
return status === "active" ? "is-online" : "is-offline";
}
function stateFor(device) {
return (
devicesStore.stateByDeviceId[String(device.id)] || {
status: "idle",
message: "Not loaded",
connectionStatus: device.connection_status || "unknown",
}
);
}
async function reboot(id) {
const device = props.devices.find((d) => String(d.id) === String(id));
const result = await devicesStore.rebootDevice(id);
if (!result.ok) {
toast.error({ title: "Reboot failed", text: result.error?.message || "Failed to reboot device" });
} else {
toast.success({ title: "Rebooting", text: `Device ${device?.name || device?.alias || "#" + id} is rebooting` });
}
}
</script>
<style scoped>
.device-cell {
display: flex;
align-items: center;
gap: 10px;
}
.device-icon {
font-size: 20px;
color: var(--color-primary);
}
.device-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.device-name-row {
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.is-online {
background: var(--color-accent);
box-shadow: 0 0 0 2px rgba(0, 245, 160, 0.25);
}
.status-dot.is-offline {
background: var(--color-danger);
box-shadow: 0 0 0 2px rgba(255, 61, 0, 0.25);
}
.device-info small {
color: var(--color-muted);
font-size: 12px;
}
.device-link {
color: inherit;
text-decoration: none;
}
.device-link:hover strong {
color: var(--color-primary);
}
</style>