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