feat: improve SAC UI layout, reports state, and hosts sorting
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,21 +14,21 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Hostname</th>
|
||||
<th>Product</th>
|
||||
<th>OS</th>
|
||||
<th>IPv4</th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('id')">ID {{ sortMark('id') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('display_name')">Имя {{ sortMark('display_name') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('hostname')">Hostname {{ sortMark('hostname') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('product')">Product {{ sortMark('product') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('os_family')">OS {{ sortMark('os_family') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('ipv4')">IPv4 {{ sortMark('ipv4') }}</button></th>
|
||||
<th>Агент</th>
|
||||
<th>Heartbeat</th>
|
||||
<th>Отчёт</th>
|
||||
<th>Last seen</th>
|
||||
<th>Events</th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('last_daily_report_at')">Отчёт {{ sortMark('last_daily_report_at') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('last_seen_at')">Last seen {{ sortMark('last_seen_at') }}</button></th>
|
||||
<th><button type="button" class="th-sort" @click="setSort('event_count')">Events {{ sortMark('event_count') }}</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="h in data?.items ?? []" :key="h.id">
|
||||
<tr v-for="h in sortedItems" :key="h.id">
|
||||
<td>{{ h.id }}</td>
|
||||
<td>{{ h.display_name || "—" }}</td>
|
||||
<td>{{ h.hostname }}</td>
|
||||
@@ -55,13 +55,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { apiFetch, type HostListResponse } from "../api";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { apiFetch, type HostListResponse, type HostSummary } from "../api";
|
||||
|
||||
const data = ref<HostListResponse | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const search = ref("");
|
||||
type SortKey =
|
||||
| "id"
|
||||
| "display_name"
|
||||
| "hostname"
|
||||
| "product"
|
||||
| "os_family"
|
||||
| "ipv4"
|
||||
| "last_seen_at"
|
||||
| "event_count"
|
||||
| "last_daily_report_at";
|
||||
const sortBy = ref<SortKey>("last_seen_at");
|
||||
const sortDir = ref<"asc" | "desc">("desc");
|
||||
|
||||
function formatDt(iso: string) {
|
||||
return new Date(iso).toLocaleString("ru-RU");
|
||||
@@ -73,6 +85,75 @@ function agentLabel(status: string) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function setSort(key: SortKey) {
|
||||
if (sortBy.value === key) {
|
||||
sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
|
||||
return;
|
||||
}
|
||||
sortBy.value = key;
|
||||
sortDir.value = key === "id" || key === "last_seen_at" || key === "event_count" ? "desc" : "asc";
|
||||
}
|
||||
|
||||
function sortMark(key: SortKey) {
|
||||
if (sortBy.value !== key) return "";
|
||||
return sortDir.value === "asc" ? "↑" : "↓";
|
||||
}
|
||||
|
||||
function compareNullableStrings(a: string | null, b: string | null) {
|
||||
const av = (a ?? "").trim().toLowerCase();
|
||||
const bv = (b ?? "").trim().toLowerCase();
|
||||
return av.localeCompare(bv, "ru");
|
||||
}
|
||||
|
||||
function compareNullableDates(a: string | null, b: string | null) {
|
||||
const av = a ? new Date(a).getTime() : 0;
|
||||
const bv = b ? new Date(b).getTime() : 0;
|
||||
return av - bv;
|
||||
}
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const items = [...(data.value?.items ?? [])];
|
||||
const key = sortBy.value;
|
||||
const dir = sortDir.value === "asc" ? 1 : -1;
|
||||
items.sort((a: HostSummary, b: HostSummary) => {
|
||||
let cmp = 0;
|
||||
switch (key) {
|
||||
case "id":
|
||||
cmp = a.id - b.id;
|
||||
break;
|
||||
case "display_name":
|
||||
cmp = compareNullableStrings(a.display_name, b.display_name);
|
||||
break;
|
||||
case "hostname":
|
||||
cmp = compareNullableStrings(a.hostname, b.hostname);
|
||||
break;
|
||||
case "product":
|
||||
cmp = compareNullableStrings(a.product, b.product);
|
||||
break;
|
||||
case "os_family":
|
||||
cmp = compareNullableStrings(a.os_family, b.os_family);
|
||||
break;
|
||||
case "ipv4":
|
||||
cmp = compareNullableStrings(a.ipv4, b.ipv4);
|
||||
break;
|
||||
case "event_count":
|
||||
cmp = (a.event_count ?? 0) - (b.event_count ?? 0);
|
||||
break;
|
||||
case "last_daily_report_at":
|
||||
cmp = compareNullableDates(a.last_daily_report_at, b.last_daily_report_at);
|
||||
break;
|
||||
case "last_seen_at":
|
||||
cmp = compareNullableDates(a.last_seen_at, b.last_seen_at);
|
||||
break;
|
||||
}
|
||||
if (cmp === 0) {
|
||||
cmp = a.hostname.localeCompare(b.hostname, "ru");
|
||||
}
|
||||
return cmp * dir;
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
async function loadHosts() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
Reference in New Issue
Block a user