Compare commits
2 Commits
43a19b723a
...
86620b034f
| Author | SHA1 | Date | |
|---|---|---|---|
| 86620b034f | |||
| d771925727 |
@@ -1,10 +1,20 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v2"
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var store = model.NewHostStore()
|
||||||
|
|
||||||
func Dashboard(c *fiber.Ctx) error {
|
func Dashboard(c *fiber.Ctx) error {
|
||||||
|
hosts := store.All()
|
||||||
return c.Render("index", fiber.Map{
|
return c.Render("index", fiber.Map{
|
||||||
"View": "hosts",
|
"View": "hosts",
|
||||||
|
"Title": "Hosts",
|
||||||
|
"Hosts": hosts,
|
||||||
|
"Active": countByStatus(hosts, "active"),
|
||||||
|
"Offline": countByStatus(hosts, "offline"),
|
||||||
"NavItems": []fiber.Map{
|
"NavItems": []fiber.Map{
|
||||||
{"ID": "hosts", "Label": "Hosts", "Icon": "server"},
|
{"ID": "hosts", "Label": "Hosts", "Icon": "server"},
|
||||||
{"ID": "terminal", "Label": "Terminal", "Icon": "terminal"},
|
{"ID": "terminal", "Label": "Terminal", "Icon": "terminal"},
|
||||||
@@ -13,6 +23,57 @@ func Dashboard(c *fiber.Ctx) error {
|
|||||||
{"ID": "keychain", "Label": "Keychain", "Icon": "key-round"},
|
{"ID": "keychain", "Label": "Keychain", "Icon": "key-round"},
|
||||||
{"ID": "settings", "Label": "Settings", "Icon": "settings2"},
|
{"ID": "settings", "Label": "Settings", "Icon": "settings2"},
|
||||||
},
|
},
|
||||||
"Title": "Hosts",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DashboardList(c *fiber.Ctx) error {
|
||||||
|
q := c.Query("q")
|
||||||
|
filter := c.Query("filter", "all")
|
||||||
|
hosts := store.Search(q, filter)
|
||||||
|
return c.Render("host_list", fiber.Map{
|
||||||
|
"Hosts": hosts,
|
||||||
|
"Active": countByStatus(hosts, "active"),
|
||||||
|
"Offline": countByStatus(hosts, "offline"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateHost(c *fiber.Ctx) error {
|
||||||
|
name := c.FormValue("name")
|
||||||
|
ip := c.FormValue("ip")
|
||||||
|
os := c.FormValue("os")
|
||||||
|
provider := c.FormValue("provider")
|
||||||
|
hostType := c.FormValue("type")
|
||||||
|
|
||||||
|
if name == "" || ip == "" {
|
||||||
|
return c.Status(400).SendString("name and ip required")
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Add(name, ip, os, provider, hostType)
|
||||||
|
hosts := store.All()
|
||||||
|
return c.Render("host_list", fiber.Map{
|
||||||
|
"Hosts": hosts,
|
||||||
|
"Active": countByStatus(hosts, "active"),
|
||||||
|
"Offline": countByStatus(hosts, "offline"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteHost(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
store.Delete(id)
|
||||||
|
hosts := store.All()
|
||||||
|
return c.Render("host_list", fiber.Map{
|
||||||
|
"Hosts": hosts,
|
||||||
|
"Active": countByStatus(hosts, "active"),
|
||||||
|
"Offline": countByStatus(hosts, "offline"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func countByStatus(hosts []model.Host, status string) int {
|
||||||
|
n := 0
|
||||||
|
for _, h := range hosts {
|
||||||
|
if h.Status == status {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Host struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Status string `json:"status"` // active | offline
|
||||||
|
LastSeen string `json:"lastSeen"`
|
||||||
|
Type string `json:"type"` // api | db | edge | web | desktop | server
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostStore struct {
|
||||||
|
Hosts []Host
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHostStore() *HostStore {
|
||||||
|
return &HostStore{
|
||||||
|
Hosts: defaultHosts(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HostStore) All() []Host {
|
||||||
|
return s.Hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HostStore) Search(q, filter string) []Host {
|
||||||
|
var result []Host
|
||||||
|
for _, h := range s.Hosts {
|
||||||
|
if filter != "" && filter != "all" && h.Status != filter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if q == "" || strings.Contains(strings.ToLower(h.Name), strings.ToLower(q)) ||
|
||||||
|
strings.Contains(strings.ToLower(h.IP), strings.ToLower(q)) ||
|
||||||
|
strings.Contains(strings.ToLower(h.OS), strings.ToLower(q)) {
|
||||||
|
result = append(result, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return []Host{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HostStore) Delete(id string) {
|
||||||
|
for i, h := range s.Hosts {
|
||||||
|
if h.ID == id {
|
||||||
|
s.Hosts = append(s.Hosts[:i], s.Hosts[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HostStore) Add(name, ip, os, provider, hostType string) Host {
|
||||||
|
h := Host{
|
||||||
|
ID: randString(8),
|
||||||
|
Name: name,
|
||||||
|
IP: ip,
|
||||||
|
OS: os,
|
||||||
|
Provider: provider,
|
||||||
|
Status: "active",
|
||||||
|
LastSeen: time.Now().Format("Jan 2, 2006"),
|
||||||
|
Type: hostType,
|
||||||
|
}
|
||||||
|
s.Hosts = append(s.Hosts, h)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func randString(n int) string {
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultHosts() []Host {
|
||||||
|
return []Host{
|
||||||
|
{ID: "h1", Name: "api-prod-01", IP: "10.0.1.15", OS: "Ubuntu 22.04", Provider: "AWS US-East", Status: "active", LastSeen: "Online", Type: "api"},
|
||||||
|
{ID: "h2", Name: "db-primary-01", IP: "10.0.2.5", OS: "Debian 12", Provider: "AWS US-East", Status: "active", LastSeen: "Online", Type: "db"},
|
||||||
|
{ID: "h3", Name: "web-edge-02", IP: "192.168.1.20", OS: "Alpine 3.19", Provider: "DigitalOcean", Status: "offline", LastSeen: "2 hours ago", Type: "edge"},
|
||||||
|
{ID: "h4", Name: "staging-api-01", IP: "10.0.3.10", OS: "Ubuntu 22.04", Provider: "AWS EU-West", Status: "active", LastSeen: "5 min ago", Type: "api"},
|
||||||
|
{ID: "h5", Name: "dev-db-02", IP: "192.168.1.45", OS: "Fedora 39", Provider: "Hetzner", Status: "offline", LastSeen: "1 day ago", Type: "db"},
|
||||||
|
{ID: "h6", Name: "monitor-01", IP: "10.0.0.5", OS: "Ubuntu 24.04", Provider: "AWS US-East", Status: "active", LastSeen: "Just now", Type: "server"},
|
||||||
|
{ID: "h7", Name: "worker-pool-01", IP: "10.0.4.50", OS: "Debian 12", Provider: "GCP US-Central", Status: "active", LastSeen: "1 min ago", Type: "server"},
|
||||||
|
{ID: "h8", Name: "bastion-host", IP: "54.85.12.7", OS: "Ubuntu 22.04", Provider: "AWS US-East", Status: "offline", LastSeen: "3 days ago", Type: "server"},
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -16,7 +16,7 @@ func main() {
|
|||||||
engine := html.New("./views", ".html")
|
engine := html.New("./views", ".html")
|
||||||
engine.Reload(true)
|
engine.Reload(true)
|
||||||
engine.AddFunc("svg", func(name string) string {
|
engine.AddFunc("svg", func(name string) string {
|
||||||
return "" // TODO: inline SVG icons
|
return ""
|
||||||
})
|
})
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
@@ -29,7 +29,9 @@ func main() {
|
|||||||
app.Static("/static", "./static")
|
app.Static("/static", "./static")
|
||||||
|
|
||||||
app.Get("/", handler.Dashboard)
|
app.Get("/", handler.Dashboard)
|
||||||
app.Get("/hosts", handler.Dashboard)
|
app.Get("/hosts", handler.DashboardList)
|
||||||
|
app.Post("/hosts", handler.CreateHost)
|
||||||
|
app.Delete("/hosts/:id", handler.DeleteHost)
|
||||||
app.Get("/api/health", handler.Health)
|
app.Get("/api/health", handler.Health)
|
||||||
|
|
||||||
log.Fatal(app.Listen(":1947"))
|
log.Fatal(app.Listen(":1947"))
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
/* Lumina System — Custom CSS */
|
/* Lumina System — Custom CSS */
|
||||||
|
|
||||||
|
/* Alpine cloak — hide until initialized */
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
|
||||||
/* Dot grid background */
|
/* Dot grid background */
|
||||||
.dot-grid {
|
.dot-grid {
|
||||||
background-image: radial-gradient(#e2e8f0 1.5px, transparent 1.5px);
|
background-image: radial-gradient(#e2e8f0 1.5px, transparent 1.5px);
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{{define "host_list"}}
|
||||||
|
<div id="host-list" class="flex flex-col gap-4">
|
||||||
|
<div x-show="view === 'grid'" x-cloak
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
|
{{range .Hosts}}
|
||||||
|
<div class="rounded-xl bg-white border border-outline-variant/30 p-4 hover:shadow-md transition-shadow animate-fade-in">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-on-surface">{{.Name}}</p>
|
||||||
|
<p class="text-xs text-on-surface-variant">{{.IP}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">
|
||||||
|
{{.Status}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-on-surface-variant mb-3">
|
||||||
|
<span>{{.OS}}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{{.Provider}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between pt-3 border-t border-outline-variant/20">
|
||||||
|
<span class="text-xs text-on-surface-variant">{{.LastSeen}}</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-primary/10 text-on-surface-variant hover:text-primary transition-colors"
|
||||||
|
onclick="alert('Connect to {{.Name}}')" title="Connect">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-red-50 text-on-surface-variant hover:text-red-500 transition-colors"
|
||||||
|
hx-delete="/hosts/{{.ID}}" hx-target="#host-list" hx-confirm="Delete {{.Name}}?" title="Delete">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="view === 'list'" x-cloak class="flex flex-col gap-2">
|
||||||
|
{{range .Hosts}}
|
||||||
|
<div class="rounded-xl bg-white border border-outline-variant/30 px-4 py-3 flex items-center gap-4 hover:shadow-sm transition-shadow animate-fade-in">
|
||||||
|
<div class="w-2 h-2 rounded-full {{if eq .Status "active"}}bg-secondary{{else}}bg-on-surface-variant{{end}} shrink-0"></div>
|
||||||
|
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center shrink-0">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p class="text-sm font-medium text-on-surface">{{.Name}}</p>
|
||||||
|
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">{{.Status}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-on-surface-variant truncate">{{.IP}} · {{.OS}} · {{.Provider}}</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-on-surface-variant shrink-0">{{.LastSeen}}</span>
|
||||||
|
<div class="flex gap-1 shrink-0">
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-primary/10 text-on-surface-variant hover:text-primary transition-colors"
|
||||||
|
onclick="alert('Connect to {{.Name}}')" title="Connect">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-red-50 text-on-surface-variant hover:text-red-500 transition-colors"
|
||||||
|
hx-delete="/hosts/{{.ID}}" hx-target="#host-list" hx-confirm="Delete {{.Name}}?" title="Delete">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if eq (len .Hosts) 0}}
|
||||||
|
<div class="text-center py-12 text-on-surface-variant">
|
||||||
|
<p class="text-lg font-medium">No hosts found</p>
|
||||||
|
<p class="text-sm mt-1">Try a different search or add a new host</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="rounded-xl border-2 border-dashed border-outline-variant/30 p-6 flex flex-col items-center justify-center text-on-surface-variant/50 hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer min-h-[160px]"
|
||||||
|
@click="$dispatch('open-modal')">
|
||||||
|
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
<span class="text-sm font-medium">Add New Host</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{{define "host_modal"}}
|
||||||
|
<div id="host-modal" class="fixed inset-0 z-50" x-data="{ open: false }" x-show="open" x-cloak
|
||||||
|
@open-modal.window="open = true" @keydown.escape.window="open = false">
|
||||||
|
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="open = false"></div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center p-4" x-show="open" x-transition>
|
||||||
|
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6" @click.outside="open = false">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-lg font-semibold text-on-surface">Add New Host</h2>
|
||||||
|
<button @click="open = false"
|
||||||
|
class="p-1 rounded-lg hover:bg-surface-container-low text-on-surface-variant">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form hx-post="/hosts" hx-target="#host-list" hx-swap="outerHTML"
|
||||||
|
@submit="open = false"
|
||||||
|
class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-on-surface mb-1">Host Name</label>
|
||||||
|
<input type="text" name="name" required placeholder="e.g. web-prod-01"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-on-surface mb-1">IP Address</label>
|
||||||
|
<input type="text" name="ip" required placeholder="e.g. 10.0.1.100"
|
||||||
|
class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50">
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-on-surface mb-1">OS</label>
|
||||||
|
<select name="os" class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||||
|
<option value="Ubuntu 22.04">Ubuntu 22.04</option>
|
||||||
|
<option value="Ubuntu 24.04">Ubuntu 24.04</option>
|
||||||
|
<option value="Debian 12">Debian 12</option>
|
||||||
|
<option value="Alpine 3.19">Alpine 3.19</option>
|
||||||
|
<option value="Fedora 39">Fedora 39</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-on-surface mb-1">Provider</label>
|
||||||
|
<select name="provider" class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||||
|
<option value="AWS US-East">AWS US-East</option>
|
||||||
|
<option value="AWS EU-West">AWS EU-West</option>
|
||||||
|
<option value="DigitalOcean">DigitalOcean</option>
|
||||||
|
<option value="Hetzner">Hetzner</option>
|
||||||
|
<option value="GCP US-Central">GCP US-Central</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-on-surface mb-1">Type</label>
|
||||||
|
<select name="type" class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||||
|
<option value="server">Server</option>
|
||||||
|
<option value="api">API</option>
|
||||||
|
<option value="db">Database</option>
|
||||||
|
<option value="web">Web</option>
|
||||||
|
<option value="edge">Edge</option>
|
||||||
|
<option value="desktop">Desktop</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button type="button" @click="open = false"
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium text-on-surface-variant hover:bg-surface-container-low transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary/90 transition-colors">
|
||||||
|
Add Host
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<span class="w-5 h-5 bg-primary/20 rounded flex items-center justify-center text-xs font-bold">⚡</span>
|
<span class="w-5 h-5 bg-primary/20 rounded flex items-center justify-center text-xs font-bold">⚡</span>
|
||||||
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Active Connections</span>
|
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Active Connections</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-2xl font-bold text-on-surface">0</span>
|
<span class="text-2xl font-bold text-on-surface">{{.Active}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
||||||
<div class="flex items-center gap-2 text-secondary mb-1">
|
<div class="flex items-center gap-2 text-secondary mb-1">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<span class="w-5 h-5 bg-tertiary/20 rounded flex items-center justify-center text-xs font-bold">#</span>
|
<span class="w-5 h-5 bg-tertiary/20 rounded flex items-center justify-center text-xs font-bold">#</span>
|
||||||
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Saved Hosts</span>
|
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Saved Hosts</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-2xl font-bold text-on-surface">0</span>
|
<span class="text-2xl font-bold text-on-surface">{{len .Hosts}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@
|
|||||||
<div class="relative flex-1 max-w-md">
|
<div class="relative flex-1 max-w-md">
|
||||||
<input type="search" name="q" placeholder="Search hosts..."
|
<input type="search" name="q" placeholder="Search hosts..."
|
||||||
hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms"
|
hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms"
|
||||||
hx-target="#host-list"
|
hx-target="#host-list" hx-include="[name='filter']"
|
||||||
class="w-full pl-9 pr-3 py-2 rounded-lg border border-outline-variant/50
|
class="w-full pl-9 pr-3 py-2 rounded-lg border border-outline-variant/50
|
||||||
bg-white text-sm text-on-surface placeholder-on-surface-variant
|
bg-white text-sm text-on-surface placeholder-on-surface-variant
|
||||||
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50">
|
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50">
|
||||||
</div>
|
</div>
|
||||||
<select name="filter"
|
<select name="filter"
|
||||||
hx-get="/hosts?filter={value}" hx-target="#host-list"
|
hx-get="/hosts?filter={value}" hx-trigger="change" hx-target="#host-list"
|
||||||
class="px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface
|
class="px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface
|
||||||
focus:outline-none focus:ring-2 focus:ring-primary/30">
|
focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
@@ -57,12 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="host-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
{{template "host_list" .}}
|
||||||
<div class="rounded-xl border-2 border-dashed border-outline-variant/30 p-8 flex flex-col items-center justify-center text-on-surface-variant/50 hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer"
|
|
||||||
@click="console.log('add host')">
|
|
||||||
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
|
||||||
<span class="text-sm font-medium">Add New Host</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{{template "host_modal" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -18,6 +18,13 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="px-3 py-3 border-t border-outline-variant/20">
|
||||||
|
<p class="text-xs font-medium text-on-surface-variant uppercase tracking-wide mb-2 px-3">Transfers</p>
|
||||||
|
<div class="px-3 py-2 rounded-lg bg-surface-container-low/50 text-xs text-on-surface-variant flex items-center gap-2">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-on-surface-variant/30"></span>
|
||||||
|
No active transfers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="p-4 border-t border-outline-variant/20">
|
<div class="p-4 border-t border-outline-variant/20">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-8 h-8 rounded-full bg-tertiary/20 flex items-center justify-center text-tertiary text-sm font-semibold">U</div>
|
<div class="w-8 h-8 rounded-full bg-tertiary/20 flex items-center justify-center text-tertiary text-sm font-semibold">U</div>
|
||||||
Reference in New Issue
Block a user