Skip to content

Commit 555b923

Browse files
committed
Use templ as template enine for dashboard component templates
Signed-off-by: Artiom Diomin <artiom@kubermatic.com>
1 parent b6ff4b0 commit 555b923

6 files changed

Lines changed: 847 additions & 187 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/MakeNowJust/heredoc/v2 v2.0.1
1010
github.com/Masterminds/semver/v3 v3.5.0
1111
github.com/Masterminds/sprig/v3 v3.3.0
12+
github.com/a-h/templ v0.3.1020
1213
github.com/aws/aws-sdk-go-v2/config v1.32.27
1314
github.com/bombsimon/logrusr/v4 v4.1.0
1415
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67
8888
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
8989
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
9090
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
91+
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
92+
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
9193
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
9294
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 h1:r2uwBUQhLhcPzaWz9tRJqc8MjYwHb+oF2+Q6467BF14=
9395
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=

pkg/dashboard/components.templ

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package dashboard
2+
3+
import "fmt"
4+
5+
templ Layout(data *dashboardData) {
6+
<!DOCTYPE html>
7+
<html lang="en" class="h-100">
8+
<head>
9+
<meta charset="UTF-8"/>
10+
<link rel="icon" type="image/svg+xml" href="/assets/favicon.png"/>
11+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
12+
<meta http-equiv="refresh" content="10"/>
13+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"/>
14+
<title>KubeOne Dashboard</title>
15+
</head>
16+
<body class="d-flex flex-column h-100">
17+
<header class="navbar navbar-expand-lg sticky-top bd-navbar border-bottom py-3 bg-body px-4 shadow-sm">
18+
<nav class="container-l bd-gutter d-flex flex-wrap flex-lg-nowrap align-items-center">
19+
<img src="/assets/kubeone-logo.png" alt="logo" style="height: 30px;"/>
20+
<h4 class="mx-3 my-0">Dashboard</h4>
21+
</nav>
22+
</header>
23+
<main class="flex-shrink-0 p-4 bg-body-tertiary flex-grow-1">
24+
<div class="container-l d-flex flex-column h-100 gap-4">
25+
<div class="card shadow-sm">
26+
<div class="card-body">
27+
<h4 class="card-title pb-2">Control-Plane Nodes</h4>
28+
@NodesTable(data.ControlPlaneNodes)
29+
</div>
30+
if len(data.WorkerNodes) > 0 {
31+
<div class="card-body">
32+
<h4 class="card-title pb-2">Worker Nodes</h4>
33+
@NodesTable(data.WorkerNodes)
34+
</div>
35+
}
36+
</div>
37+
<div class="card shadow-sm">
38+
<div class="card-body">
39+
<h4 class="card-title pb-2">Machine Deployments</h4>
40+
@MachineDeploymentsTable(data.MachineDeployments)
41+
</div>
42+
</div>
43+
</div>
44+
</main>
45+
<footer class="footer mt-auto py-3 border-top">
46+
<div class="container-l px-4">
47+
<span class="text-body-secondary">Powered by Kubermatic</span>
48+
</div>
49+
</footer>
50+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
51+
<script>
52+
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
53+
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
54+
</script>
55+
</body>
56+
</html>
57+
}
58+
59+
templ StatusPill(status string) {
60+
<span
61+
role="button"
62+
class={ fmt.Sprintf("text-bg-%s rounded-circle", pillClass(status)) }
63+
style="width:20px; height: 20px"
64+
data-bs-toggle="tooltip"
65+
title={ pillTitle(status) }
66+
></span>
67+
}
68+
69+
templ StatusBadge(status string) {
70+
<span class={ fmt.Sprintf("badge text-bg-%s rounded-pill", badgeClass(status)) }>{ status }</span>
71+
}
72+
73+
templ NodesTable(nodes []node) {
74+
<div class="row row-cols-1 row-cols-md-4 g-3">
75+
for _, n := range nodes {
76+
<div class="col">
77+
<div class="card">
78+
<div class="card-header d-flex align-items-center gap-2">
79+
@StatusPill(n.Status)
80+
{ n.Name }
81+
</div>
82+
<div class="card-body">
83+
<div class="row row-cols-2 g-2">
84+
<div class="col-4"><strong>Version</strong></div>
85+
<div class="col">{ n.Version }</div>
86+
<div class="col-4"><strong>Seen</strong></div>
87+
<div class="col">{ n.LastHeartbeatTime.String() }</div>
88+
if n.IsControlPlane {
89+
<div class="col-4"><strong>etcd</strong></div>
90+
<div class="col">
91+
<div class="d-flex">
92+
if n.EtcdOK {
93+
@StatusBadge("Ready")
94+
} else {
95+
@StatusBadge("Not Ready")
96+
}
97+
</div>
98+
</div>
99+
<div class="col-4"><strong>apiserver</strong></div>
100+
<div class="col">
101+
<div class="d-flex">
102+
if n.APIServerOK {
103+
@StatusBadge("Ready")
104+
} else {
105+
@StatusBadge("Not Ready")
106+
}
107+
</div>
108+
</div>
109+
}
110+
</div>
111+
</div>
112+
</div>
113+
</div>
114+
}
115+
</div>
116+
}
117+
118+
templ MachineDeploymentsTable(machineDeployments []machineDeployment) {
119+
<div class="mt-2">
120+
<table class="table">
121+
<thead>
122+
<tr>
123+
<th scope="col">Status</th>
124+
<th scope="col">Name</th>
125+
<th scope="col">Namespace</th>
126+
<th scope="col">Replicas</th>
127+
<th scope="col">Kubelet</th>
128+
<th scope="col">Age</th>
129+
</tr>
130+
</thead>
131+
<tbody>
132+
for _, md := range machineDeployments {
133+
<tr class="table-light shadow-sm">
134+
<td>
135+
<div class="d-flex">
136+
@StatusPill("Ready")
137+
</div>
138+
</td>
139+
<td><strong>{ md.Name }</strong></td>
140+
<td>{ md.Namespace }</td>
141+
<td>{ fmt.Sprintf("%d / %d", md.AvailableReplicas, md.Replicas) }</td>
142+
<td>{ md.Kubelet }</td>
143+
<td>{ md.Age.String() }</td>
144+
</tr>
145+
<tr>
146+
<td colspan="7" class="p-0">
147+
<h5 class="p-2 m-0">Machines</h5>
148+
@MachinesTable(md.Machines)
149+
</td>
150+
</tr>
151+
}
152+
</tbody>
153+
</table>
154+
</div>
155+
}
156+
157+
templ MachinesTable(machines *[]machine) {
158+
<table class="table">
159+
<thead>
160+
<tr>
161+
<th scope="col">Status</th>
162+
<th scope="col">Name</th>
163+
<th scope="col">Namespace</th>
164+
<th scope="col">Node</th>
165+
<th scope="col">Kubelet</th>
166+
<th scope="col">Address</th>
167+
<th scope="col">Age</th>
168+
</tr>
169+
</thead>
170+
<tbody>
171+
if machines != nil {
172+
for _, m := range *machines {
173+
<tr class={ templ.KV("table-danger", m.Deleted) }>
174+
<td>
175+
<div class="d-flex">
176+
if !m.Deleted {
177+
@StatusPill("Ready")
178+
} else {
179+
@StatusPill("Deleting")
180+
}
181+
</div>
182+
</td>
183+
<td>{ m.Namespace }</td>
184+
<td>{ m.Name }</td>
185+
<td>{ m.Node }</td>
186+
<td>{ m.Kubelet }</td>
187+
<td>{ m.Address }</td>
188+
<td>{ m.Age.String() }</td>
189+
</tr>
190+
}
191+
}
192+
</tbody>
193+
</table>
194+
}
195+
196+
func pillClass(status string) string {
197+
if status == "" {
198+
return "secondary"
199+
}
200+
if status == "Ready" {
201+
return "success"
202+
}
203+
return "danger"
204+
}
205+
206+
func pillTitle(status string) string {
207+
if status == "" {
208+
return "Unknown"
209+
}
210+
return status
211+
}
212+
213+
func badgeClass(status string) string {
214+
if status == "" {
215+
return "secondary"
216+
}
217+
if status == "Ready" {
218+
return "success"
219+
}
220+
return "danger"
221+
}

0 commit comments

Comments
 (0)