Newer
Older
hard-panel / panel / frontend / src / components / ServerCard.vue
<script setup>
import { computed } from 'vue'
import { GnBadge, GnButton, GnCard, GnProgress } from 'gnexus-ui-kit/vue'

import { SERIES_IN, SERIES_OUT } from '../chartTheme.js'
import { fmtAgo, fmtGiB, fmtMbs, fmtPercent, fmtUptime, ramPercent } from '../format.js'

const props = defineProps({
  server: { type: Object, required: true },
})

const emit = defineEmits(['open'])

const metrics = computed(() => props.server.metrics)

const cpu = computed(() => metrics.value?.cpu ?? null)
const ram = computed(() => ramPercent(metrics.value))

// Самый заполненный диск — по нему видно, когда пора вмешиваться
const worstDisk = computed(() => {
  const disks = metrics.value?.disks || []
  return disks.length ? disks.reduce((a, b) => (b.percent > a.percent ? b : a)) : null
})

function usageVariant(value) {
  if (value === null || value === undefined) return 'secondary'
  if (value >= 90) return 'danger'
  if (value >= 75) return 'warning'
  return 'secondary'
}

const statusVariant = computed(() => {
  switch (props.server.status) {
    case 'online': return 'success'
    case 'offline': return 'danger'
    default: return 'warning'
  }
})

const statusText = computed(() => {
  switch (props.server.status) {
    case 'online': return 'онлайн'
    case 'offline': return 'офлайн'
    default: return 'ждёт данных'
  }
})
</script>

<template>
  <GnCard class="server-card" @click="emit('open')">
    <template #title>
      <span class="card-title">
        <span class="card-name">{{ server.name }}</span>
        <GnBadge :variant="statusVariant">{{ statusText }}</GnBadge>
      </span>
    </template>

    <div class="card-sub">
      <span>{{ server.hostname }}</span>
      <span v-if="server.os" class="card-os">{{ server.os }}</span>
    </div>
    <div v-if="server.ips?.length" class="card-ips">{{ server.ips.join(' · ') }}</div>

    <div class="card-meters" v-if="metrics">
      <GnProgress label="CPU" :value="cpu ?? 0" :variant="usageVariant(cpu)" />
      <GnProgress label="RAM" :value="ram ?? 0" :variant="usageVariant(ram)" />
      <GnProgress
        v-if="worstDisk"
        :label="`Диск ${worstDisk.mount}`"
        :value="worstDisk.percent"
        :variant="usageVariant(worstDisk.percent)"
      />
    </div>

    <div v-else class="card-nodata">нет данных — установи агента</div>

    <div class="card-net" v-if="metrics">
      <span class="net-dir"><i class="ph ph-arrow-down" :style="{ color: SERIES_IN }" /> {{ fmtMbs(metrics.net_in_mbs) }}</span>
      <span class="net-dir"><i class="ph ph-arrow-up" :style="{ color: SERIES_OUT }" /> {{ fmtMbs(metrics.net_out_mbs) }}</span>
      <span class="card-uptime">аптайм {{ fmtUptime(metrics.uptime) }}</span>
    </div>

    <div class="card-note" v-if="server.note" :title="server.note">{{ server.note }}</div>

    <template #footer>
      <div class="card-footer">
        <span class="card-seen">{{ fmtAgo(server.last_seen) }}</span>
        <GnButton size="sm" variant="primary" icon="ph-arrow-right" @click.stop="emit('open')">
          Открыть
        </GnButton>
      </div>
    </template>
  </GnCard>
</template>

<style scoped>
/* layout-обёртки; стили компонентов — из кита */
.server-card {
  cursor: pointer;
}
.card-title {
  display: flex;
  align-items: center;
  gap: 10px;
}
.card-name {
  overflow: hidden;
  text-overflow: ellipsis;
}
.card-sub {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  opacity: 0.85;
  margin-bottom: 4px;
  font-size: 14px;
}
.card-os {
  opacity: 0.7;
}
.card-ips {
  opacity: 0.6;
  font-size: 13px;
  margin-bottom: 14px;
}
.card-meters {
  display: grid;
  gap: 12px;
}
.card-nodata {
  opacity: 0.6;
  font-size: 14px;
  padding: 8px 0;
}
.card-net {
  display: flex;
  gap: 16px;
  align-items: center;
  margin-top: 14px;
  font-size: 14px;
}
.net-dir,
.card-uptime {
  white-space: nowrap;
}
.net-dir {
  display: inline-flex;
  gap: 6px;
  align-items: center;
}
.card-uptime {
  margin-left: auto;
  opacity: 0.6;
}
.card-note {
  margin-top: 12px;
  font-size: 13px;
  opacity: 0.65;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.card-seen {
  opacity: 0.6;
  font-size: 13px;
}
</style>