feat(widgets): implémentation Phase 4 — widgets Glance complets
- widget-network-scan : liste équipements avec état (online/offline), hostname, IP, vendor, badges services, tri online en premier - widget-agent-metrics : barres CPU/RAM/disque/température par agent, code couleur ok (vert) / warn (orange) / crit (rouge) - sentinelmesh.css : styles custom (points statut, badges, barres de progression animées) compatibles thèmes Glance - glance-page-example.yaml : page Infrastructure prête à l'emploi - Backend widgets enrichi : mac, ports, offline count, net_rx/tx_bps - ROADMAP Phase 4 marquée complète Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+9
-7
@@ -30,14 +30,16 @@
|
||||
- [x] Push vers /api/v1/metrics et /api/v1/events
|
||||
- [ ] `widget-agent-metrics` Glance — Phase 4
|
||||
|
||||
## Phase 4 — UX & Personnalisation
|
||||
## Phase 4 — Widgets Glance ✅
|
||||
|
||||
- [ ] `widget-network-scan` Glance (tuile + popup)
|
||||
- [ ] `widget-agent-metrics` Glance
|
||||
- [ ] Popups détaillés widgets
|
||||
- [ ] Filtres, tri, favoris
|
||||
- [ ] Icônes locales (Heroicons / selfh.st)
|
||||
- [ ] Personnalisation par équipement
|
||||
- [x] `widget-network-scan` : liste équipements (état, IP, hostname, vendor, services, tri online/offline)
|
||||
- [x] `widget-agent-metrics` : barres CPU/RAM/disque/température par agent, code couleur (ok/warn/crit)
|
||||
- [x] CSS custom (`sentinelmesh.css`) : badges, barres de progression, points de statut
|
||||
- [x] Page Glance exemple complète (`glance-page-example.yaml`)
|
||||
- [x] Backend widgets enrichi : mac, ports, offline count, net_rx/tx_bps
|
||||
- [ ] Popups détaillés — nécessite widget `extension` (serveur HTTP séparé) — Phase 4+
|
||||
- [ ] Icônes locales par type d'équipement — Phase 4+
|
||||
- [ ] Favoris / personnalisation par équipement — Phase 4+
|
||||
|
||||
## Phase 5 — Déploiement & Distribution
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct NetworkWidget {
|
||||
pub last_scan_at: Option<String>,
|
||||
pub total: usize,
|
||||
pub online: usize,
|
||||
pub offline: usize,
|
||||
pub devices: Vec<DeviceSummary>,
|
||||
}
|
||||
|
||||
@@ -21,37 +22,43 @@ pub struct DeviceSummary {
|
||||
pub ip: String,
|
||||
pub hostname: Option<String>,
|
||||
pub vendor: Option<String>,
|
||||
pub mac: Option<String>,
|
||||
pub state: String,
|
||||
pub services: Value,
|
||||
pub open_ports: Value,
|
||||
pub last_seen: String,
|
||||
}
|
||||
|
||||
#[oapath(get, path = "/api/v1/widgets/network",
|
||||
responses((status = 200, description = "Données widget réseau pour Glance")))]
|
||||
pub async fn network(State(db): State<SqlitePool>) -> Result<Json<NetworkWidget>> {
|
||||
let devices = sqlx::query_as::<_, Device>("SELECT * FROM devices ORDER BY state, ip")
|
||||
// Online en premier, puis tri IP
|
||||
let devices = sqlx::query_as::<_, Device>(
|
||||
"SELECT * FROM devices ORDER BY CASE state WHEN 'online' THEN 0 ELSE 1 END, ip"
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let online = devices.iter().filter(|d| d.state == "online").count();
|
||||
let offline = devices.len() - online;
|
||||
|
||||
let summary = devices
|
||||
.iter()
|
||||
.map(|d| DeviceSummary {
|
||||
ip: d.ip.clone(),
|
||||
hostname: d.hostname.clone(),
|
||||
vendor: d.vendor.clone(),
|
||||
mac: d.mac.clone(),
|
||||
state: d.state.clone(),
|
||||
services: serde_json::from_str(&d.services).unwrap_or(Value::Array(vec![])),
|
||||
open_ports: serde_json::from_str(&d.open_ports).unwrap_or(Value::Array(vec![])),
|
||||
last_seen: d.last_seen.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let last_scan = devices.iter().map(|d| d.last_seen.clone()).max();
|
||||
|
||||
Ok(Json(NetworkWidget {
|
||||
last_scan_at: last_scan,
|
||||
total: devices.len(),
|
||||
online,
|
||||
devices: summary,
|
||||
}))
|
||||
Ok(Json(NetworkWidget { last_scan_at: last_scan, total: devices.len(), online, offline, devices: summary }))
|
||||
}
|
||||
|
||||
// --- Widget métriques ---
|
||||
@@ -66,10 +73,12 @@ pub struct AgentMetricSummary {
|
||||
pub agent_id: String,
|
||||
pub hostname: String,
|
||||
pub status: String,
|
||||
pub cpu_percent: Option<f64>,
|
||||
pub ram_percent: Option<f64>,
|
||||
pub disk_percent: Option<f64>,
|
||||
pub temperature_c: Option<f64>,
|
||||
pub cpu_percent: f64,
|
||||
pub ram_percent: f64,
|
||||
pub disk_percent: f64,
|
||||
pub temperature_c: f64,
|
||||
pub net_rx_bps: i64,
|
||||
pub net_tx_bps: i64,
|
||||
pub last_seen: String,
|
||||
}
|
||||
|
||||
@@ -77,10 +86,15 @@ pub struct AgentMetricSummary {
|
||||
responses((status = 200, description = "Données widget métriques pour Glance")))]
|
||||
pub async fn metrics(State(db): State<SqlitePool>) -> Result<Json<MetricsWidget>> {
|
||||
let rows = sqlx::query_as::<_, (
|
||||
String, String, String, Option<f64>, Option<f64>, Option<f64>, Option<f64>, String,
|
||||
String, String, String,
|
||||
Option<f64>, Option<f64>, Option<f64>, Option<f64>,
|
||||
Option<i64>, Option<i64>,
|
||||
String,
|
||||
)>(
|
||||
"SELECT a.id, a.hostname, a.status, m.cpu_percent, m.ram_percent,
|
||||
m.disk_percent, m.temperature_c, a.last_seen
|
||||
"SELECT a.id, a.hostname, a.status,
|
||||
m.cpu_percent, m.ram_percent, m.disk_percent, m.temperature_c,
|
||||
m.net_rx_bps, m.net_tx_bps,
|
||||
a.last_seen
|
||||
FROM agents a LEFT JOIN metrics m ON m.agent_id = a.id
|
||||
WHERE a.agent_type = 'metric'
|
||||
ORDER BY a.hostname",
|
||||
@@ -90,9 +104,17 @@ pub async fn metrics(State(db): State<SqlitePool>) -> Result<Json<MetricsWidget>
|
||||
|
||||
let agents = rows
|
||||
.into_iter()
|
||||
.map(|(id, hostname, status, cpu, ram, disk, temp, last_seen)| AgentMetricSummary {
|
||||
agent_id: id, hostname, status, cpu_percent: cpu, ram_percent: ram,
|
||||
disk_percent: disk, temperature_c: temp, last_seen,
|
||||
.map(|(id, hostname, status, cpu, ram, disk, temp, rx, tx, last_seen)| AgentMetricSummary {
|
||||
agent_id: id,
|
||||
hostname,
|
||||
status,
|
||||
cpu_percent: cpu.unwrap_or(0.0),
|
||||
ram_percent: ram.unwrap_or(0.0),
|
||||
disk_percent: disk.unwrap_or(0.0),
|
||||
temperature_c: temp.unwrap_or(0.0),
|
||||
net_rx_bps: rx.unwrap_or(0),
|
||||
net_tx_bps: tx.unwrap_or(0),
|
||||
last_seen,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Exemple de page Glance complète avec les deux widgets SentinelMesh
|
||||
# À intégrer dans votre glance.yml
|
||||
#
|
||||
# Installation :
|
||||
# 1. Copier sentinelmesh.css dans votre répertoire assets Glance
|
||||
# 2. Référencer le CSS dans server: custom-css-file: /assets/sentinelmesh.css
|
||||
# 3. Ajouter cette page dans votre glance.yml
|
||||
|
||||
pages:
|
||||
- name: Infrastructure
|
||||
slug: infra
|
||||
width: wide
|
||||
columns:
|
||||
- size: small
|
||||
widgets:
|
||||
# --- Métriques système ---
|
||||
- type: custom-api
|
||||
title: Métriques systèmes
|
||||
cache: 5s
|
||||
url: http://sentinelmesh:8080/api/v1/widgets/metrics
|
||||
template: |
|
||||
<ul class="list list-gap-15">
|
||||
{{ range .JSON.Array "agents" }}
|
||||
{{- $cpu := .Float "cpu_percent" -}}
|
||||
{{- $ram := .Float "ram_percent" -}}
|
||||
{{- $disk := .Float "disk_percent" -}}
|
||||
{{- $temp := .Float "temperature_c" -}}
|
||||
{{- $up := eq (.String "status") "online" -}}
|
||||
|
||||
<li class="sm-agent">
|
||||
<div class="flex justify-between items-center margin-bottom-6">
|
||||
<span class="size-h4 {{ if $up }}color-highlight{{ else }}color-negative{{ end }}">
|
||||
{{ .String "hostname" }}
|
||||
</span>
|
||||
<span class="size-h6 color-paragraph"
|
||||
{{ .String "last_seen" | parseTime "rfc3339" | toRelativeTime }}></span>
|
||||
</div>
|
||||
|
||||
{{ if $up }}
|
||||
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">CPU</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $cpu 85.0 }}sm-bar-crit{{ else if ge $cpu 60.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" $cpu }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $cpu }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">RAM</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $ram 90.0 }}sm-bar-crit{{ else if ge $ram 70.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" $ram }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $ram }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">Disque</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $disk 90.0 }}sm-bar-crit{{ else if ge $disk 75.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" $disk }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $disk }}%</span>
|
||||
</div>
|
||||
|
||||
{{ if gt $temp 0.0 }}
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">Temp</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $temp 85.0 }}sm-bar-crit{{ else if ge $temp 70.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" (mul (div $temp 100.0) 100.0) }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $temp }}°C</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ else }}
|
||||
<p class="size-h6 color-negative">Agent hors ligne</p>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
- size: full
|
||||
widgets:
|
||||
# --- Découverte réseau ---
|
||||
- type: custom-api
|
||||
title: Réseau local — 10.0.0.0/22
|
||||
cache: 30s
|
||||
url: http://sentinelmesh:8080/api/v1/widgets/network
|
||||
template: |
|
||||
{{- $online := .JSON.Int "online" -}}
|
||||
{{- $offline := .JSON.Int "offline" -}}
|
||||
{{- $total := .JSON.Int "total" -}}
|
||||
|
||||
<div class="sm-net-header flex justify-between margin-bottom-10">
|
||||
<span class="size-h5 color-paragraph">
|
||||
<span class="color-positive">{{ $online }} en ligne</span>
|
||||
{{ if gt $offline 0 }} · <span class="color-negative">{{ $offline }} hors ligne</span>{{ end }}
|
||||
· {{ $total }} total
|
||||
</span>
|
||||
{{ if .JSON.Exists "last_scan_at" }}
|
||||
<span class="size-h6 color-paragraph"
|
||||
{{ .JSON.String "last_scan_at" | parseTime "rfc3339" | toRelativeTime }}></span>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<ul class="list list-gap-10 collapsible-container" data-collapse-after="15">
|
||||
{{ range .JSON.Array "devices" }}
|
||||
{{- $state := .String "state" -}}
|
||||
{{- $host := .String "hostname" -}}
|
||||
{{- $ip := .String "ip" -}}
|
||||
{{- $vendor := .String "vendor" -}}
|
||||
<li class="sm-device flex gap-10 items-center">
|
||||
|
||||
<div class="sm-state-dot {{ if eq $state "online" }}sm-dot-online{{ else }}sm-dot-offline{{ end }}"></div>
|
||||
|
||||
<div class="grow min-width-0">
|
||||
<div class="flex justify-between">
|
||||
<span class="size-h4 {{ if eq $state "online" }}color-highlight{{ else }}color-paragraph{{ end }} text-truncate">
|
||||
{{- if ne $host "" }}{{ $host }}{{ else }}{{ $ip }}{{ end -}}
|
||||
</span>
|
||||
<span class="size-h6 color-paragraph shrink-0 margin-left-10">{{ $ip }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-5 flex-wrap margin-top-3">
|
||||
{{ if ne $vendor "" }}
|
||||
<span class="sm-badge sm-badge-vendor size-h6">{{ $vendor }}</span>
|
||||
{{ end }}
|
||||
{{ range .Array "services" }}
|
||||
<span class="sm-badge size-h6">{{ .String "" }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
@@ -0,0 +1,87 @@
|
||||
/* ============================================================
|
||||
SentinelMesh — CSS custom pour widgets Glance
|
||||
Copier dans le répertoire assets de Glance et référencer :
|
||||
custom-css-file: /assets/sentinelmesh.css
|
||||
============================================================ */
|
||||
|
||||
/* --- Widget réseau --- */
|
||||
|
||||
.sm-net-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sm-device {
|
||||
align-items: flex-start;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* Point de statut coloré */
|
||||
.sm-state-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-top: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sm-dot-online { background: var(--color-positive, #4caf50); }
|
||||
.sm-dot-offline { background: var(--color-negative, #f44336); opacity: 0.5; }
|
||||
|
||||
/* Badges services / vendor */
|
||||
.sm-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-widget-content-background, rgba(255,255,255,0.06));
|
||||
color: var(--color-text-subdue, #aaa);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.sm-badge-vendor {
|
||||
color: var(--color-primary, #7ca9d4);
|
||||
}
|
||||
|
||||
/* --- Widget métriques --- */
|
||||
|
||||
.sm-agent {
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--color-widget-content-background, rgba(255,255,255,0.06));
|
||||
}
|
||||
.sm-agent:last-child { border-bottom: none; }
|
||||
|
||||
.sm-metric-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sm-metric-label {
|
||||
width: 40px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sm-metric-val {
|
||||
width: 36px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Barre de progression */
|
||||
.sm-bar-track {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-widget-content-background, rgba(255,255,255,0.08));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sm-bar {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
min-width: 2px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.sm-bar-ok { background: var(--color-positive, #4caf50); }
|
||||
.sm-bar-warn { background: var(--color-base-500, #ff9800); }
|
||||
.sm-bar-crit { background: var(--color-negative, #f44336); }
|
||||
@@ -0,0 +1,76 @@
|
||||
# Widget SentinelMesh — Métriques système
|
||||
# À copier dans votre glance.yml
|
||||
#
|
||||
# Prérequis :
|
||||
# - Backend SentinelMesh démarré (port 8080)
|
||||
# - agent-metric en cours d'exécution sur chaque machine
|
||||
# - Fichier CSS custom : assets/sentinelmesh.css
|
||||
|
||||
- type: custom-api
|
||||
title: Métriques systèmes
|
||||
cache: 5s
|
||||
url: http://sentinelmesh:8080/api/v1/widgets/metrics
|
||||
template: |
|
||||
<ul class="list list-gap-15">
|
||||
{{ range .JSON.Array "agents" }}
|
||||
{{- $cpu := .Float "cpu_percent" -}}
|
||||
{{- $ram := .Float "ram_percent" -}}
|
||||
{{- $disk := .Float "disk_percent" -}}
|
||||
{{- $temp := .Float "temperature_c" -}}
|
||||
{{- $up := eq (.String "status") "online" -}}
|
||||
|
||||
<li class="sm-agent">
|
||||
<div class="flex justify-between items-center margin-bottom-6">
|
||||
<span class="size-h4 {{ if $up }}color-highlight{{ else }}color-negative{{ end }}">
|
||||
{{ .String "hostname" }}
|
||||
</span>
|
||||
<span class="size-h6 color-paragraph"
|
||||
{{ .String "last_seen" | parseTime "rfc3339" | toRelativeTime }}></span>
|
||||
</div>
|
||||
|
||||
{{ if $up }}
|
||||
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">CPU</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $cpu 85.0 }}sm-bar-crit{{ else if ge $cpu 60.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" $cpu }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $cpu }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">RAM</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $ram 90.0 }}sm-bar-crit{{ else if ge $ram 70.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" $ram }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $ram }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">Disque</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $disk 90.0 }}sm-bar-crit{{ else if ge $disk 75.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" $disk }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $disk }}%</span>
|
||||
</div>
|
||||
|
||||
{{ if gt $temp 0.0 }}
|
||||
<div class="sm-metric-row">
|
||||
<span class="sm-metric-label size-h6 color-paragraph">Temp</span>
|
||||
<div class="sm-bar-track">
|
||||
<div class="sm-bar {{ if ge $temp 85.0 }}sm-bar-crit{{ else if ge $temp 70.0 }}sm-bar-warn{{ else }}sm-bar-ok{{ end }}"
|
||||
style="width: {{ printf "%.0f" (mul (div $temp 100.0) 100.0) }}%"></div>
|
||||
</div>
|
||||
<span class="sm-metric-val size-h6">{{ printf "%.0f" $temp }}°C</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ else }}
|
||||
<p class="size-h6 color-negative">Agent hors ligne</p>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
@@ -0,0 +1,61 @@
|
||||
# Widget SentinelMesh — Découverte réseau
|
||||
# À copier dans votre glance.yml
|
||||
#
|
||||
# Prérequis :
|
||||
# - Backend SentinelMesh démarré (port 8080)
|
||||
# - agent-scan-network en cours d'exécution
|
||||
# - Fichier CSS custom copié dans votre répertoire assets Glance
|
||||
# et référencé dans glance.yml : custom-css-file: /assets/sentinelmesh.css
|
||||
|
||||
- type: custom-api
|
||||
title: Réseau local
|
||||
cache: 30s
|
||||
url: http://sentinelmesh:8080/api/v1/widgets/network
|
||||
template: |
|
||||
{{- $online := .JSON.Int "online" -}}
|
||||
{{- $offline := .JSON.Int "offline" -}}
|
||||
{{- $total := .JSON.Int "total" -}}
|
||||
|
||||
<div class="sm-net-header flex justify-between margin-bottom-10">
|
||||
<span class="size-h5 color-paragraph">
|
||||
{{ $online }} en ligne
|
||||
{{ if gt $offline 0 }} · <span class="color-negative">{{ $offline }} hors ligne</span>{{ end }}
|
||||
· {{ $total }} total
|
||||
</span>
|
||||
{{ if .JSON.Exists "last_scan_at" }}
|
||||
<span class="size-h6 color-paragraph"
|
||||
{{ .JSON.String "last_scan_at" | parseTime "rfc3339" | toRelativeTime }}></span>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<ul class="list list-gap-10 collapsible-container" data-collapse-after="8">
|
||||
{{ range .JSON.Array "devices" }}
|
||||
{{- $state := .String "state" -}}
|
||||
{{- $host := .String "hostname" -}}
|
||||
{{- $ip := .String "ip" -}}
|
||||
{{- $vendor := .String "vendor" -}}
|
||||
<li class="sm-device flex gap-10 items-center">
|
||||
|
||||
<div class="sm-state-dot {{ if eq $state "online" }}sm-dot-online{{ else }}sm-dot-offline{{ end }}"></div>
|
||||
|
||||
<div class="grow min-width-0">
|
||||
<div class="flex justify-between">
|
||||
<span class="size-h4 color-highlight text-truncate">
|
||||
{{- if ne $host "" }}{{ $host }}{{ else }}{{ $ip }}{{ end -}}
|
||||
</span>
|
||||
<span class="size-h6 color-paragraph shrink-0 margin-left-10">{{ $ip }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-5 flex-wrap margin-top-3">
|
||||
{{ if ne $vendor "" }}
|
||||
<span class="sm-badge sm-badge-vendor size-h6">{{ $vendor }}</span>
|
||||
{{ end }}
|
||||
{{ range .Array "services" }}
|
||||
<span class="sm-badge size-h6">{{ .String "" }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
Reference in New Issue
Block a user