Newer
Older
smart-home-server / webclient-vue / src / components / device / DeviceTable.vue
<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">
            <strong>{{ row.name || row.alias || `Device #${row.id}` }}</strong>
            <small>{{ row.alias }} — {{ row.device_ip || '—' }}</small>
          </div>
        </div>
      </router-link>
    </template>

    <template #cell-connect="{ row }">
      <DeviceConnectionBadge :status="row.connection_status || 'unknown'" />
    </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 DeviceConnectionBadge from "../../features/devices/components/DeviceConnectionBadge.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: "connect", label: "Connect" },
    { 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 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-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>