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 text-primary" v-html="typeIcon(row.device_type)" />
          <div class="device-info">
            <div class="device-name-row">
              <strong>{{ row.name || row.alias || `Device #${row.id}` }}</strong>
              <GnBadge v-if="row.status && row.status !== 'active'" :variant="systemStatusVariant(row.status)" size="sm">
                {{ row.status }}
              </GnBadge>
              <span
                class="status-dot"
                :class="{
                  'text-success': row.connection_status === 'active',
                  'text-danger': row.connection_status !== 'active',
                }"
                aria-hidden="true"
                :title="row.connection_status === 'active' ? 'Online' : 'Offline'"
              />
            </div>
            <small class="text-muted">{{ 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, GnBadge, 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 systemStatusVariant(status) {
  const map = {
    active: "success",
    removed: "danger",
    freezed: "warning",
    setup: "primary",
  };
  return map[status] || "secondary";
}

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;
}

.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;
  background: currentColor;
  box-shadow: 0 0 4px currentColor;
}

.device-info small {
  font-size: 12px;
}

.device-link {
  color: inherit;
  text-decoration: none;
}

.device-link:hover strong {
  color: var(--color-primary);
}
</style>