merge barnch switch_to_templ

- Moved to templ for templating
- Started adding cluster statistics
This commit is contained in:
Matthew Stobbs 2024-03-19 09:13:44 -06:00
parent 6e0198de35
commit 48bdc94351
73 changed files with 224164 additions and 554 deletions

View File

@ -5,23 +5,23 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
pre_cmd = [""]
cmd = "templ generate && go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["tmp", "vendor", "testdata", "assets/node_modules"]
exclude_dir = ["tmp", "vendor", "testdata", "node_modules", "public"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_regex = ["_test.go", "_templ.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css"]
include_ext = ["go", "css", "templ", "config.js"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false

View File

@ -72,3 +72,28 @@ Overall goals:
</clustvirt:data>
</metadata>
```
### Open Source Projects
The following projects are being used as part of ClustVirt.
Only those being used directly are listed, not the dependencies.
#### Go
| Library | Use | License |
| --- | --- | --- |
| [xdg](https://github.com/adrg/xdg) | XDG paths | MIT |
| [spf13/viper](https://github.com/spf13/viper) | Configuration | MIT |
| [spf13](https://github.com/spf13/cobra) | Command Pattern CLI | Apache 2.0 |
| [go-chart/v2](https://github.com/wcharczuk/go-chart) | Chart generation | MIT |
| [libvirt](https://libvirt.org/go/libvirt) | Communications with Libvirtd | MIT |
| [libvirtxml](https://libvirt.org/go/libvirtxml) | Libvirt XML parsing and generation | MIT |
| [templ](https://github.com/a-h/templ) | HTML Templating Engine | MIT |
| [chi/v5](https://github.com/go-chi/chi) | HTTP Routing | MIT |
#### Node NPM
| Library | Use | License |
| --- | --- | ------- |
| [tailwindcss](https://tailwindcss.com) | CSS UI library | MIT |
| [tailwindcss/forms](https://tailwindcss.com) | CSS UI library for forms | MIT |

View File

@ -1,10 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
@layer components {
.notimpl {
@apply italic font-light list-image-possible;
}
}

View File

@ -1,22 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["../view/**/*.gohtml"],
theme: {
extend: {
listStyleImage: {
never: 'url("/static/icons/list-never.svg")',
accepted: 'url("/static/icons/list-accepted.svg")',
possible: 'url("/static/icons/list-possible.svg")',
informational: 'url("/static/icons/list-informational.svg")',
},
content: {
link: 'url("/static/icons/link.svg")',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('tailwind-dracula')(),
],
}

1
cluster/api.go Normal file
View File

@ -0,0 +1 @@
package cluster

1
cluster/auth.go Normal file
View File

@ -0,0 +1 @@
package cluster

69
cluster/builder.go Normal file
View File

@ -0,0 +1,69 @@
package cluster
import (
"log"
"time"
"git.staur.ca/stobbsm/clustvirt/lib/host"
)
// ClusterBuilder is used to build a Cluster object, which can then be used
type ClusterBuilder struct {
cluster *Cluster
}
// New starts the builder pattern for the Cluster.
// Sets the default interval time to 30 seconds.
func New() *ClusterBuilder {
return &ClusterBuilder{
cluster: &Cluster{
interval: time.Duration(time.Second * 30),
hosts: map[string]*host.Host{},
defaultURI: host.URI_QEMU_SSH_SYSTEM,
},
}
}
// SetInterval sets the check interval of the Cluster being built.
func (c *ClusterBuilder) SetInterval(i time.Duration) *ClusterBuilder {
c.cluster.interval = i
return c
}
// Build retuns the built cluster
func (c *ClusterBuilder) Build() *Cluster {
return c.cluster
}
func (c *ClusterBuilder) DefaultHostURI(uri *host.URI) *ClusterBuilder {
c.cluster.defaultURI = uri
return c
}
func (c *ClusterBuilder) AddHost(h string) *ClusterBuilder {
if _, ok := c.cluster.hosts[h]; ok {
log.Println("warning: trying to add duplicate host")
return c
}
hc, err := host.ConnectHost(c.cluster.defaultURI, h)
if err != nil {
log.Printf("failed to connect to host: %s, %s", h, err)
return c
}
c.cluster.hosts[h] = hc
return c
}
func (c *ClusterBuilder) AddHostWithURI(h string, uri *host.URI) *ClusterBuilder {
if _, ok := c.cluster.hosts[h]; ok {
log.Println("warning: trying to add duplicate host")
return c
}
hc, err := host.ConnectHost(uri, h)
if err != nil {
log.Printf("failed to connect to host: %s, %s", h, err)
return c
}
c.cluster.hosts[h] = hc
return c
}

26
cluster/cluster.go Normal file
View File

@ -0,0 +1,26 @@
// Package cluster is the unique portion of this application that implements
// basic cluster controls overtop of Libvirtd hosts. The controller is agnostic
// about where it is running, and doesn't need to be running on a host that
// has Libvirtd installed on it.
//
// The cluster can be configured through the use of TOML configuration file,
// or with CLI flags. This is done via the itegration of the greate go libraries
// spf13/viper and spf13/cobra.
package cluster
import (
"time"
"git.staur.ca/stobbsm/clustvirt/lib/host"
)
// Cluster is the data structure and controller for cluster access.
// Using it's methods, you can access hosts, virtual machines, health
// data, and more.
// Cluster implements a time.Ticker that will be used to check the connection
// status of hosts, and reconnect if a connection was blocked or interrupted.
type Cluster struct {
interval time.Duration
hosts map[string]*host.Host
defaultURI *host.URI
}

1
cluster/config.go Normal file
View File

@ -0,0 +1 @@
package cluster

1
cluster/daemon.go Normal file
View File

@ -0,0 +1 @@
package cluster

1
cluster/format/format.go Normal file
View File

@ -0,0 +1 @@
package format

1
cluster/format/json.go Normal file
View File

@ -0,0 +1 @@
package format

1
cluster/format/xml.go Normal file
View File

@ -0,0 +1 @@
package format

1
cluster/host.go Normal file
View File

@ -0,0 +1 @@
package cluster

1
cluster/network.go Normal file
View File

@ -0,0 +1 @@
package cluster

296
cluster/stats.go Normal file
View File

@ -0,0 +1,296 @@
package cluster
// ClusterStats is used to gather stats for the entire cluster
type ClusterStats struct {
CPU struct {
Sockets uint32
Cores uint32
Threads uint32
Allocated uint32
}
Memory struct {
Total uint64
Free uint64
Buffers uint64
Cached uint64
Allocated uint64
}
Storage struct {
Total uint64
Used uint64
Free uint64
Active uint32
Inactive uint32
Pools uint32
Volumes struct {
Total uint32
Active uint32
Inactive uint32
}
}
VM struct {
Count uint32
Started uint32
Stopped uint32
}
Host struct {
Count uint32
Available uint32
}
Network struct {
Count uint32
Active uint32
Inactive uint32
}
old *ClusterStats
c *Cluster
}
// ClusterStats is used to gather stats for the entire cluster
type StatDiff struct {
CPU struct {
Sockets int
Cores int
Threads int
Allocated int
}
Memory struct {
Total int
Free int
Buffers int
Cached int
Allocated int
}
Storage struct {
Total int
Used int
Free int
Active int
Inactive int
Pools int
Volumes struct {
Total int
Active int
Inactive int
}
}
VM struct {
Count int
Started int
Stopped int
}
Host struct {
Count int
Available int
}
Network struct {
Count int
Active int
Inactive int
}
}
// InitStats is given a cluster, which it then uses to load the initial statistics
// Does not close connections, but uses the host connections available to the
// cluster to add statistics together.
func InitStats(c *Cluster) *ClusterStats {
cs := &ClusterStats{
c: c,
}
return cs
}
// Update triggers the stats collector to refresh it's statistics
func (cs *ClusterStats) Update() {
cs.old = cs.copy()
cs.reset()
// Start looping through each host in the cluster, adding to the total
for _, h := range cs.c.hosts {
cs.Host.Count++
cs.Host.Available++
cs.CPU.Sockets += h.HostInfo.Sockets
cs.CPU.Cores += h.HostInfo.Cores
cs.CPU.Threads += h.HostInfo.Threads
cs.Memory.Total += h.NodeMemory.Total
cs.Memory.Free += h.NodeMemory.Free
cs.Memory.Buffers += h.NodeMemory.Buffers
cs.Memory.Cached += h.NodeMemory.Cached
// Storage Pool counting
cs.Storage.Pools += uint32(len(h.StoragePoolList))
countedSharedPools := map[string]struct{}{}
// Loop through available storage pools
for _, sp := range h.StoragePoolList {
if _, ok := countedSharedPools[sp.Name]; ok {
// Already counted this shared pool, move on
continue
}
if sp.HAEnabled {
countedSharedPools[sp.Name] = struct{}{}
}
if !sp.Active {
cs.Storage.Inactive++
continue
}
cs.Storage.Active++
cs.Storage.Total += sp.Capacity
cs.Storage.Used += sp.Allocation
cs.Storage.Free += sp.Capacity - sp.Allocation
// Volumes in the pool
cs.Storage.Volumes.Total += uint32(len(sp.Volumes))
for range sp.Volumes {
cs.Storage.Volumes.Active++
}
}
// VM Count
cs.VM.Count += uint32(len(h.VMList))
for _, vm := range h.VMList {
cs.CPU.Allocated += uint32(vm.VCPUs)
cs.Memory.Allocated += uint64(vm.Memory)
if vm.Active {
cs.VM.Started++
continue
}
cs.VM.Stopped++
}
// Network count
cs.Network.Count += uint32(len(h.NetworkList))
for _, ni := range h.NetworkList {
if ni.Active {
cs.Network.Active++
continue
}
cs.Network.Inactive++
}
}
}
// Diff returns a map of all the field and how they changed
func (cs *ClusterStats) Diff() StatDiff {
return StatDiff{
CPU: struct {
Sockets int
Cores int
Threads int
Allocated int
}{
Sockets: int(cs.CPU.Sockets) - int(cs.old.CPU.Sockets),
Cores: int(cs.CPU.Cores) - int(cs.old.CPU.Cores),
Threads: int(cs.CPU.Threads) - int(cs.old.CPU.Threads),
Allocated: int(cs.CPU.Allocated) - int(cs.old.CPU.Allocated),
},
Memory: struct {
Total int
Free int
Buffers int
Cached int
Allocated int
}{
Total: int(cs.old.Memory.Total - cs.Memory.Total),
Free: int(cs.old.Memory.Free - cs.Memory.Free),
Buffers: int(cs.old.Memory.Buffers - cs.Memory.Buffers),
Cached: int(cs.old.Memory.Cached - cs.Memory.Cached),
Allocated: int(cs.old.Memory.Allocated - cs.Memory.Allocated),
},
Storage: struct {
Total int
Used int
Free int
Active int
Inactive int
Pools int
Volumes struct {
Total int
Active int
Inactive int
}
}{
Total: int(cs.old.Storage.Total - cs.Storage.Total),
Used: int(cs.old.Storage.Used - cs.Storage.Used),
Free: int(cs.old.Storage.Free - cs.Storage.Free),
Active: int(cs.old.Storage.Active - cs.Storage.Active),
Inactive: int(cs.old.Storage.Inactive - cs.Storage.Inactive),
Pools: int(cs.old.Storage.Pools - cs.Storage.Pools),
Volumes: struct {
Total int
Active int
Inactive int
}{
Total: int(cs.old.Storage.Volumes.Total - cs.Storage.Volumes.Total),
Active: int(cs.old.Storage.Volumes.Active - cs.Storage.Volumes.Active),
Inactive: int(cs.old.Storage.Volumes.Inactive - cs.Storage.Volumes.Inactive),
},
},
VM: struct {
Count int
Started int
Stopped int
}{
Count: int(cs.old.VM.Count - cs.VM.Count),
Started: int(cs.old.VM.Started - cs.VM.Started),
Stopped: int(cs.old.VM.Stopped - cs.VM.Stopped),
},
Host: struct {
Count int
Available int
}{
Count: int(cs.old.Host.Count - cs.Host.Count),
Available: int(cs.old.Host.Available - cs.Host.Available),
},
Network: struct {
Count int
Active int
Inactive int
}{
Count: int(cs.old.Network.Count - cs.Network.Count),
Active: int(cs.old.Network.Active - cs.Network.Active),
Inactive: int(cs.old.Network.Inactive - cs.Network.Inactive),
},
}
}
// copy the clusterstats into a new clusterstatus object for comparison purposes
func (cs *ClusterStats) copy() *ClusterStats {
ncs := *cs
return &ncs
}
// reset all values to zero value
func (cs *ClusterStats) reset() {
cs.CPU.Sockets = 0
cs.CPU.Cores = 0
cs.CPU.Threads = 0
cs.CPU.Allocated = 0
cs.Memory.Total = 0
cs.Memory.Free = 0
cs.Memory.Buffers = 0
cs.Memory.Cached = 0
cs.Storage.Total = 0
cs.Storage.Used = 0
cs.Storage.Free = 0
cs.Storage.Active = 0
cs.Storage.Inactive = 0
cs.Storage.Pools = 0
cs.VM.Count = 0
cs.VM.Started = 0
cs.VM.Stopped = 0
cs.Host.Count = 0
cs.Host.Available = 0
cs.Network.Count = 0
cs.Network.Active = 0
cs.Network.Inactive = 0
}

1
cluster/storagepool.go Normal file
View File

@ -0,0 +1 @@
package cluster

1
cluster/vm.go Normal file
View File

@ -0,0 +1 @@
package cluster

29
cmd/clustvirt.go Normal file
View File

@ -0,0 +1,29 @@
// Package cmd contains the main application file for ClustVirt.
// This command is used for everything, implementing the command pattern
// with the help of spf13/cobra.
// After building, the executable's first argument must be one of the valid
// commands, such as `daemon`, `api`, `query`, `host`, etc. This pattern allows
// for a simple way to access functionality, reducing the need to use the
// REST api directly.
package cmd
// Global Arguments:
// All commands can use the following arguments:
// --output none|json|xml: Set the output type, none being just plain log,
// json outputing as a JSON document, and XML outputing as an XML document
// Commands planned:
// config: actions associated with the configuration of clustvirt
// set: set a configuration value
// get: get the current value of a configuration options
// list: list available configuration options
// daemon: actions associated with running clustvirt in daemon mode
// start: start the daemon
// stop: safely stop the daemon
// status: query the status of the daemon
// api: query the REST api. There must a clustvirt daemon running for this to function
// host: actions associated with a specific host in the cluster
// vm: actions asociated with a specifc vm in the cluster
// storagepool: actions associated with storage pools
// auth: actions associated with authentication
// network: actions to define networks

9
config/config.go Normal file
View File

@ -0,0 +1,9 @@
// Package config used spf13/viper to handle configuration. Sane defaults are set
// as default values, and a single host is set to `qemu:///system`, which is only
// used if it is found. Not having a local instance will not panic or crash anything.
//
// Viper uses configuration set via defaults -> config file -> env variables ->
// cli flags -> calls to Set (while running), so clustvirt uses the same method.
package config

27
css/style.css Normal file
View File

@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
@layer components {
.notimpl {
@apply italic font-light list-image-possible;
}
div.infobox {
@apply m-2 px-4 pt-2 flex-col shadow-md rounded-2xl border-8 border-double;
@apply divide-solid divide-y-2;
}
span>a::after {
@apply content-link w-3 h-3 inline-block invert;
}
span>a {
@apply text-base text-uiblue-400;
}
span>a:visited {
@apply text-base text-uiorange-400;
}
}

14
go.mod
View File

@ -2,9 +2,17 @@ module git.staur.ca/stobbsm/clustvirt
go 1.22.1
require libvirt.org/go/libvirt v1.10001.0
require (
github.com/a-h/templ v0.2.598
github.com/go-chi/chi/v5 v5.0.12
github.com/wcharczuk/go-chart/v2 v2.1.1
gopkg.in/ffmt.v1 v1.5.6
libvirt.org/go/libvirt v1.10001.0
libvirt.org/go/libvirtxml v1.10001.0
)
require (
github.com/go-chi/chi/v5 v5.0.12 // indirect
gopkg.in/ffmt.v1 v1.5.6 // indirect
github.com/blend/go-sdk v1.20220411.3 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.11.0 // indirect
)

45
go.sum
View File

@ -1,6 +1,51 @@
github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo=
github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/ffmt.v1 v1.5.6 h1:4Bu3riZp5sAIXW2T/18JM9BkwJLodurXFR0f7PXp+cw=
gopkg.in/ffmt.v1 v1.5.6/go.mod h1:LssvGOZFiBGoBcobkTqnyh+uN1VzIRoibW+c0JI/Ha4=
libvirt.org/go/libvirt v1.10001.0 h1:lEVDNE7xfzmZXiDEGIS8NvJSuaz11OjRXw+ufbQEtPY=
libvirt.org/go/libvirt v1.10001.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ=
libvirt.org/go/libvirtxml v1.10001.0 h1:r9WBs24r3mxIG3/hAMRRwDMy4ZaPHmhHjw72o/ceXic=
libvirt.org/go/libvirtxml v1.10001.0/go.mod h1:7Oq2BLDstLr/XtoQD8Fr3mfDNrzlI3utYKySXF2xkng=

38
lib/host/charts.go Normal file
View File

@ -0,0 +1,38 @@
package host
import (
"fmt"
"log"
"strings"
"github.com/wcharczuk/go-chart/v2"
)
// This file contains utilities to create charts based on the different data
// When a chart is rendered, it can return either an SVG or PNG. SVG is preferrable.
func (h *Host) ChartMemory() string {
h.getNodeInfo()
log.Println("Generating Chart")
memFree := float64(h.NodeMemory.Free)
memCached := float64(h.NodeMemory.Cached)
memBuffer := float64(h.NodeMemory.Buffers)
memTotal := float64(h.NodeMemory.Total)
c := chart.PieChart{
Title: fmt.Sprintf("Memory Info %s", h.SystemHostName),
Width: 256,
Height: 256,
Values: []chart.Value{
{Value: memTotal - memFree, Label: fmt.Sprintf("%.2f%% Free", memFree/memTotal*100)},
{Value: memTotal - memCached, Label: fmt.Sprintf("%.2f%% Cached", memCached/memTotal*100)},
{Value: memTotal - memBuffer, Label: fmt.Sprintf("%.2f%% Buffers", memBuffer/memTotal*100)},
},
}
sb := new(strings.Builder)
log.Println("Rendering chart")
if err := c.Render(chart.SVG, sb); err != nil {
return err.Error()
}
return sb.String()
}

View File

@ -16,6 +16,7 @@ import (
"git.staur.ca/stobbsm/clustvirt/lib/storagevol"
"git.staur.ca/stobbsm/clustvirt/util"
"libvirt.org/go/libvirt"
"libvirt.org/go/libvirtxml"
)
// Host holds information and acts as a connection handle for a Host
@ -80,6 +81,9 @@ type VMInfo struct {
ID uint
UUID []byte
XML string
Active bool
VCPUs uint
Memory uint
// States are the current states active on the host
States []guest.VMState
}
@ -105,6 +109,7 @@ type StoragePoolInfo struct {
Capacity uint64
Allocation uint64
Available uint64
IsNet bool
// HAEnabled indicates if the storage pool has High Availability
HAEnabled bool
// Volumes defined in the storage pool
@ -113,13 +118,13 @@ type StoragePoolInfo struct {
// VolumeInfo holds basic information about Volumes available in storage pools
type VolumeInfo struct {
Name string
Key string
Path string
Type string
Capacity uint64
Name string
Key string
Path string
Type string
Capacity uint64
Allocation uint64
XML string
XML string
}
// NetIfInfo holds basic information about available network interfaces (not their connections, the devices themselves)
@ -134,6 +139,7 @@ type NetworkInfo struct {
Name string
UUID []byte
XML string
Active bool
// NetIf is the network interface this connection is applied to
NetIf NetIfInfo
}
@ -228,29 +234,31 @@ func (h *Host) Close() error {
// private methods that load the different informational parts
func (h *Host) getInfo() {
var wg sync.WaitGroup
var wg = new(sync.WaitGroup)
infoFuncs := []func(*sync.WaitGroup){
infoFuncs := []func(){
h.getDevicesInfo,
h.getDomainInfo,
// h.getDomainInfo,
h.getIfaceInfo,
h.getNetsInfo,
h.getNodeInfo,
h.getSEVInfo,
h.getSecretsInfo,
h.getStoragePools,
// h.getSEVInfo,
// h.getSecretsInfo,
// h.getStoragePools,
}
for _, f := range infoFuncs {
wg.Add(1)
f(&wg)
go func(wg *sync.WaitGroup) {
defer wg.Done()
f()
}(wg)
}
wg.Wait()
}
func (h *Host) getStoragePools(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getStoragePools() {
spools, err := h.conn.ListAllStoragePools(0)
if err != nil {
log.Println(err)
@ -285,6 +293,19 @@ func (h *Host) getStoragePools(wg *sync.WaitGroup) {
h.StoragePoolList[i].Allocation = spInfo.Allocation
h.StoragePoolList[i].Available = spInfo.Available
spoolXML := &libvirtxml.StoragePool{}
if err = spoolXML.Unmarshal(h.StoragePoolList[i].XML); err != nil {
log.Println(err)
}
h.StoragePoolList[i].Type = spoolXML.Type
for _, t := range storagepool.NetTypes {
if h.StoragePoolList[i].Type == t {
h.StoragePoolList[i].IsNet = true
h.StoragePoolList[i].HAEnabled = true
continue
}
}
svols, err := s.ListAllStorageVolumes(0)
if err != nil {
log.Println(err)
@ -295,7 +316,7 @@ func (h *Host) getStoragePools(wg *sync.WaitGroup) {
if h.StoragePoolList[i].Volumes[j].Name, err = sv.GetName(); err != nil {
log.Println(err)
}
if h.StoragePoolList[i].Volumes[j].Key, err = sv.GetKey(); err != nil {
if h.StoragePoolList[i].Volumes[j].Key, err = sv.GetKey(); err != nil {
log.Println(err)
}
if h.StoragePoolList[i].Volumes[j].Path, err = sv.GetPath(); err != nil {
@ -321,8 +342,7 @@ func (h *Host) getStoragePools(wg *sync.WaitGroup) {
}
}
func (h *Host) getSecretsInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getSecretsInfo() {
nsecrets, err := h.conn.ListAllSecrets(0)
if err != nil {
log.Println(err)
@ -344,8 +364,7 @@ func (h *Host) getSecretsInfo(wg *sync.WaitGroup) {
}
}
func (h *Host) getNodeInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getNodeInfo() {
var err error
h.AvailableCPUTypes, err = h.conn.GetCPUModelNames("x86_64", 0)
if err != nil {
@ -413,8 +432,7 @@ func (h *Host) getNodeInfo(wg *sync.WaitGroup) {
}
}
func (h *Host) getSEVInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getSEVInfo() {
// getSEVInfo
h.HostSEVInfo.SEVEnabled = true
ns, err := h.conn.GetSEVInfo(0)
@ -442,8 +460,7 @@ func (h *Host) getSEVInfo(wg *sync.WaitGroup) {
}
}
func (h *Host) getDomainInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getDomainInfo() {
// getDomainInfo
doms, err := h.conn.ListAllDomains(0)
if err != nil {
@ -465,14 +482,22 @@ func (h *Host) getDomainInfo(wg *sync.WaitGroup) {
if h.VMList[i].XML, err = d.GetXMLDesc(0); err != nil {
log.Println(err)
}
vmXML := &libvirtxml.Domain{}
if err = vmXML.Unmarshal(h.VMList[i].XML); err != nil {
log.Println(err)
}
h.VMList[i].VCPUs = vmXML.VCPU.Value
h.VMList[i].Memory = vmXML.CurrentMemory.Value
if h.VMList[i].Active, err = d.IsActive(); err != nil {
log.Println(err)
}
d.Free()
}
}
}
func (h *Host) getIfaceInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getIfaceInfo() {
// getIfaceInfo
ifaces, err := h.conn.ListInterfaces()
if err != nil {
@ -498,8 +523,7 @@ func (h *Host) getIfaceInfo(wg *sync.WaitGroup) {
}
}
func (h *Host) getNetsInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getNetsInfo() {
// getNetsInfo
nets, err := h.conn.ListNetworks()
if err != nil {
@ -521,14 +545,16 @@ func (h *Host) getNetsInfo(wg *sync.WaitGroup) {
if h.NetworkList[i].XML, err = net.GetXMLDesc(0); err != nil {
log.Println(err)
}
if h.NetworkList[i].Active, err = net.IsActive(); err != nil {
log.Println(err)
}
net.Free()
}
}
}
func (h *Host) getDevicesInfo(wg *sync.WaitGroup) {
defer wg.Done()
func (h *Host) getDevicesInfo() {
ndevs, err := h.conn.ListAllNodeDevices(0)
if err != nil {
log.Println(err)

View File

@ -1 +1,6 @@
// Package network defines varialbes and methods to control libvirtd
// networks. Networks Can be defined cluster wide, and mapped to
// host interfaces this way.
package network
type Network struct{}

View File

@ -9,3 +9,38 @@ var StoragePoolStateMap = map[libvirt.StoragePoolState]string{
libvirt.STORAGE_POOL_DEGRADED: "degraded",
libvirt.STORAGE_POOL_INACCESSIBLE: "inaccessible",
}
var Types = []string{
"dir",
"fs",
"netfs",
"disk",
"iscsi",
"logical",
"scsi",
"mpath",
"rbd",
"sheepdog",
"gluster",
"zfs",
"iscsi-direct",
}
var LocalTypes = []string{
"dir",
"fs",
"disk",
"logical",
"scsi",
"zfs",
}
var NetTypes = []string{
"netfs",
"iscsi",
"mpath",
"rbd",
"sheepdog",
"gluster",
"iscsi-direct",
}

78
main.go
View File

@ -1,10 +1,17 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"git.staur.ca/stobbsm/clustvirt/cluster"
"git.staur.ca/stobbsm/clustvirt/lib/host"
"git.staur.ca/stobbsm/clustvirt/view"
"git.staur.ca/stobbsm/clustvirt/view/components"
"git.staur.ca/stobbsm/clustvirt/view/static"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
@ -13,42 +20,63 @@ const DEBUG bool = true
func main() {
log.Println("Starting clustvirt, the libvirt cluster manager")
clst := cluster.New().
AddHost("earth.staur.ca").
AddHost("venus.staur.ca").
AddHost("mars.staur.ca").
Build()
//venus, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, "venus.staur.ca")
//if err != nil {
// log.Fatal(err)
//}
//defer venus.Close()
//lm, err := venus.GetGuestByName("logan-minecraft")
//if err != nil {
// log.Fatal(err)
//}
//defer lm.Close()
initStats := cluster.InitStats(clst)
// Start webserver and serve homepage
defaultNavBar := []components.NavItem{
{Name: "Cluster", Href: "/"},
{Name: "Configuration", Href: "/config"},
{Name: "About", Href: "/about"},
}
fs := http.StripPrefix("/static/", http.FileServer(http.Dir("public")))
r := chi.NewRouter()
r.Use(middleware.Logger)
if DEBUG {
r.Use(middleware.NoCache)
}
r.Get("/static/*", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if DEBUG {
w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Add("Pragma", "no-cache")
w.Header().Add("Expire", "0")
}
fs.ServeHTTP(w, r)
})
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if DEBUG {
w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Add("Pragma", "no-cache")
w.Header().Add("Expire", "0")
}
w.Header().Add("Content", "text/html")
log.Println(view.ViewHome.Render(w, nil))
r.Get("/", templ.Handler(view.HostMain(defaultNavBar)).ServeHTTP)
r.Get("/cluster", func(w http.ResponseWriter, r *http.Request) {
initStats.Update()
log.Println(
"Rendering clusterstats",
view.ClusterInfo(
initStats,
initStats.Diff(),
defaultNavBar,
).Render(context.Background(), w))
})
r.Get("/about", templ.Handler(static.Home()).ServeHTTP)
r.Route("/htmx", func(r chi.Router) {
r.Get("/host/{hostname}", func(w http.ResponseWriter, r *http.Request) {
rhost, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, chi.URLParam(r, "hostname"))
if err != nil {
http.Error(w, fmt.Sprintf("error while getting host: %s", err), http.StatusInternalServerError)
return
}
defer rhost.Close()
log.Println("Rendering HostInfo", view.HostInfo(rhost).Render(context.Background(), w))
})
r.Get("/host/{hostname}/stats", func(w http.ResponseWriter, r *http.Request) {
rhost, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, chi.URLParam(r, "hostname"))
if err != nil {
http.Error(w, fmt.Sprintf("error while getting host: %s", err), http.StatusInternalServerError)
return
}
defer rhost.Close()
log.Println("Rendering stats", view.SysInfo(rhost).Render(context.Background(), w))
})
})
log.Println(http.ListenAndServe(":3000", r))

View File

@ -1,12 +1,11 @@
{
"name": "assets",
"name": "clustvirt",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"tailwind-dracula": "^1.1.0",
"tailwindcss": "^3.4.1"
}
},
@ -200,12 +199,15 @@
"dev": true
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": {
@ -1195,15 +1197,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwind-dracula": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tailwind-dracula/-/tailwind-dracula-1.1.0.tgz",
"integrity": "sha512-LjU1df6WGS5eUwozdPtZ7jARojHYTIe6y1Gj5v2QNaNnMnLm7ipPtlIEGlNXXraWjVnWeRbLZl4vvr48mMFGxw==",
"dev": true,
"peerDependencies": {
"tailwindcss": "2.x || 3.x"
}
},
"node_modules/tailwindcss": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",

View File

@ -1,7 +1,6 @@
{
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"tailwind-dracula": "^1.1.0",
"tailwindcss": "^3.4.1"
}
}

1
public/chart.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256">\n<path d="M 0 0 L 256 0 L 256 256 L 0 256 L 0 0" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><path d="M 5 5 L 251 5 L 251 251 L 5 251 L 5 5" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><circle cx="128" cy="128" r="123" style="stroke-width:5;stroke:rgba(255,255,255,1.0);fill:rgba(106,195,203,1.0)"/><text x="33" y="134" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif">Free</text></svg>

After

Width:  |  Height:  |  Size: 634 B

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m 8.3076888,12.923076 2.7692302,2.769231 4.615384,-6.4615418 m 7.384614,2.7692328 a 11.076921,11.076928 0 1 1 -22.15384198,0 11.076921,11.076928 0 0 1 22.15384198,0 z"
d="m 6.9230763,14.767385 2.3076926,2.307694 3.8461551,-5.38462 m 6.153846,2.307695 a 9.2307704,9.2307764 0 1 1 -18.46154043,0 9.2307704,9.2307764 0 0 1 18.46154043,0 z"
id="path1"
style="stroke-width:1.84615;stroke:#d6d6d6;stroke-opacity:1" />
style="stroke:#d6d6d6;stroke-width:1.53846;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -29,16 +29,16 @@
inkscape:zoom="9.8333333"
inkscape:cx="9.7627119"
inkscape:cy="9.7627119"
inkscape:window-width="1312"
inkscape:window-height="449"
inkscape:window-width="1440"
inkscape:window-height="847"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m 11.076927,11.076927 0.05046,-0.02462 a 0.92307691,0.9230774 0 0 1 1.308308,1.048616 l -0.871384,3.490464 a 0.92307691,0.9230774 0 0 0 1.308308,1.049846 l 0.05046,-0.02585 m 10.153842,-4.615384 a 11.076923,11.076929 0 1 1 -22.15384598,0 11.076923,11.076929 0 0 1 22.15384598,0 z M 12.000004,7.3846176 h 0.0098 v 0.00985 h -0.0098 z"
d="m 9.2307739,13.229418 0.04205,-0.02052 a 0.76923083,0.76923136 0 0 1 1.0902571,0.873847 l -0.7261531,2.908721 a 0.76923083,0.76923136 0 0 0 1.0902561,0.874872 l 0.04205,-0.02154 m 8.461536,-3.846154 a 9.23077,9.2307764 0 1 1 -18.46153989,0 9.23077,9.2307764 0 0 1 18.46153989,0 z m -9.230765,-3.846151 h 0.0082 v 0.0082 h -0.0082 z"
id="path1"
style="stroke:#d6d6d6;stroke-width:1.84615;stroke-opacity:1" />
style="stroke:#d6d6d6;stroke-width:1.53846;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -38,7 +38,7 @@
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M 19.833912,19.783417 A 11.082911,11.052709 0 0 0 4.1603098,4.1525257 M 19.833912,19.783417 A 11.082911,11.052709 0 0 1 4.1603098,4.1525257 M 19.833912,19.783417 4.1603098,4.1525257"
d="M 16.532287,20.52382 A 9.23747,9.2323036 0 0 0 3.4685327,7.4673718 M 16.532287,20.52382 A 9.23747,9.2323036 0 0 1 3.4685327,7.4673718 M 16.532287,20.52382 3.4685327,7.4673718"
id="path1"
style="stroke:#d6d6d6;stroke-width:1.84462;stroke-opacity:1" />
style="stroke:#d6d6d6;stroke-width:1.53913;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -38,7 +38,7 @@
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M 15.692306,11.999999 H 8.3076904 m 14.7692306,0 a 11.076923,11.076929 0 1 1 -22.15384598,0 11.076923,11.076929 0 0 1 22.15384598,0 z"
d="M 13.076923,13.99792 H 6.9230762 m 12.3076928,0 a 9.23077,9.2307764 0 1 1 -18.46153955,0 9.23077,9.2307764 0 0 1 18.46153955,0 z"
id="path1"
style="stroke:#d6d6d6;stroke-width:1.84615;stroke-opacity:1" />
style="stroke:#d6d6d6;stroke-width:1.53846;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

52
public/images/bars.svg Normal file
View File

@ -0,0 +1,52 @@
<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#fff">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

56
public/images/grid.svg Normal file
View File

@ -0,0 +1,56 @@
<svg width="105" height="105" viewBox="0 0 105 105" xmlns="http://www.w3.org/2000/svg" fill="#fff">
<circle cx="12.5" cy="12.5" r="12.5">
<animate attributeName="fill-opacity"
begin="0s" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="12.5" cy="52.5" r="12.5" fill-opacity=".5">
<animate attributeName="fill-opacity"
begin="100ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="52.5" cy="12.5" r="12.5">
<animate attributeName="fill-opacity"
begin="300ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="52.5" cy="52.5" r="12.5">
<animate attributeName="fill-opacity"
begin="600ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="92.5" cy="12.5" r="12.5">
<animate attributeName="fill-opacity"
begin="800ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="92.5" cy="52.5" r="12.5">
<animate attributeName="fill-opacity"
begin="400ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="12.5" cy="92.5" r="12.5">
<animate attributeName="fill-opacity"
begin="700ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="52.5" cy="92.5" r="12.5">
<animate attributeName="fill-opacity"
begin="500ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="92.5" cy="92.5" r="12.5">
<animate attributeName="fill-opacity"
begin="200ms" dur="1s"
values="1;.2;1" calcMode="linear"
repeatCount="indefinite" />
</circle>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/images/home/logo-banner-dark-800.png (Stored with Git LFS) Normal file

Binary file not shown.

139
tailwind.config.js Normal file
View File

@ -0,0 +1,139 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["**/*.templ", "**/*.go"],
safelist: [
{
pattern: /list-image-(accepted|informational|never|possible)/,
},
{
pattern: /[a-z]+-ui(blue|cyan|green|grey|orange|pink|purple|red|yellow)-[0-9]+/
},
],
theme: {
extend: {
listStyleImage: {
never: 'url("/static/icons/list-never.svg")',
accepted: 'url("/static/icons/list-accepted.svg")',
possible: 'url("/static/icons/list-possible.svg")',
informational: 'url("/static/icons/list-informational.svg")',
},
content: {
link: 'url("/static/icons/link.svg")',
},
colors: {
'uiblue': {
50: '#eef3fb',
100: '#dde7f8',
200: '#bbcff1',
300: '#99b6e9',
400: '#779ee2',
500: '#5586db',
600: '#446baf',
700: '#335083',
800: '#223658',
900: '#111b2c',
},
'uicyan': {
50: '#f2fefd',
100: '#e5fdfc',
200: '#cbfaf9',
300: '#b2f8f5',
400: '#98f5f2',
500: '#7ef3ef',
600: '#65c2bf',
700: '#4c928f',
800: '#326160',
900: '#193130',
},
'uigreen': {
50: '#f5f9f5',
100: '#ebf2ec',
200: '#d8e5d8',
300: '#c4d9c5',
400: '#b1ccb1',
500: '#9dbf9e',
600: '#7e997e',
700: '#5e735f',
800: '#3f4c3f',
900: '#1f2620',
},
'uigrey': {
50: '#f5f5f5',
100: '#eaeaea',
200: '#d6d6d6',
300: '#c1c1c1',
400: '#adadad',
500: '#989898',
600: '#7a7a7a',
700: '#5b5b5b',
800: '#3d3d3d',
900: '#1e1e1e',
},
'uiorange': {
50: '#f9f3ef',
100: '#f2e8df',
200: '#e5d0bf',
300: '#d8b9a0',
400: '#cba180',
500: '#be8a60',
600: '#986e4d',
700: '#72533a',
800: '#4c3726',
900: '#261c13',
},
'uipink': {
50: '#faf0f9',
100: '#f5e1f3',
200: '#ebc3e7',
300: '#e0a5d8',
400: '#d687cf',
500: '#cc69c3',
600: '#a3549c',
700: '#7a3f75',
800: '#522a4e',
900: '#291527',
},
'uipurple': {
50: '#fceefc',
100: '#f8dcf8',
200: '#f1b9f2',
300: '#ea97eb',
400: '#e374e5',
500: '#dc51de',
600: '#b041b2',
700: '#843185',
800: '#582059',
900: '#2c102c',
},
'uired': {
50: '#fbe9ef',
100: '#f7d2de',
200: '#efa5bd',
300: '#e8789d',
400: '#e04b7c',
500: '#d81e5b',
600: '#ad1849',
700: '#821237',
800: '#560c24',
900: '#2b0612',
},
'uiyellow': {
50: '#fffff5',
100: '#ffffea',
200: '#fffed6',
300: '#fffec1',
400: '#fffdad',
500: '#fffd98',
600: '#ccca7a',
700: '#99985b',
800: '#66653d',
900: '#33331e',
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
],
}

View File

@ -1,7 +0,0 @@
{{ define "footer" }}
<div class="flex gap-4 md:gap-6 sm:gap-8 divide divide-solid">
<div id="footer_left" class="flex-auto basis-1/4 md:basis-1/3 p-2">Left</div>
<div id="footer_middle" class="flex-auto basis-1/2 md:basis-1/3 p-2">Middle</div>
<div id="footer_right" class="flex-auto basis-1/4 md:basis-1/3 p-2">Right</div>
</div>
{{ end }}

View File

@ -1,10 +0,0 @@
{{ define "header" }}
<h1 class="text-2xl font-bold">Clustvirt</h1>
<h2 class="text-sm font-thin italic">Libvirtd simplified and clustered</h2>
<nav>
<ul>
<li>Server 1</li>
<li>Server 2</li>
</ul>
</nav>
{{ end }}

View File

@ -1,22 +0,0 @@
{{ define "_index" }}
<!DOCTYPE html>
<html class="text-nosferatu-100 bg-nosferatu-900">
<head>
<title>Clustvirt</title>
<link href="/static/css/style.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<header>
{{ template "header" }}
</header>
<div id="content" name="content">
{{ template "content" . }}
</div>
<footer>
{{ template "footer" }}
</footer>
</body>
</html>
{{ end }}

57
view/cluster.templ Normal file
View File

@ -0,0 +1,57 @@
package view
import (
"fmt"
"git.staur.ca/stobbsm/clustvirt/cluster"
"git.staur.ca/stobbsm/clustvirt/view/components"
"git.staur.ca/stobbsm/clustvirt/view/layouts"
)
templ ClusterInfo(cs *cluster.ClusterStats, diff cluster.StatDiff, navbar []components.NavItem) {
@layouts.Manager("ClustVirt", "Cluster Manager", navbar) {
<h3>Cluster Stats</h3>
<table class={ "table-auto", "w-full" }>
<caption class={ "caption-top" }>
CPU stats
</caption>
<thead>
<tr>
<th>Sockets</th>
<th>Cores</th>
<th>Threads</th>
<th>Allocated</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{ fmt.Sprint(cs.CPU.Sockets) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Cores) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Threads) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Allocated) }
</td>
</tr>
<tr>
<td>
{ fmt.Sprint(diff.CPU.Sockets) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Cores) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Threads) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Allocated) }
</td>
</tr>
</tbody>
</table>
}
}

195
view/cluster_templ.go Normal file
View File

@ -0,0 +1,195 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package view
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import (
"fmt"
"git.staur.ca/stobbsm/clustvirt/cluster"
"git.staur.ca/stobbsm/clustvirt/view/components"
"git.staur.ca/stobbsm/clustvirt/view/layouts"
)
func ClusterInfo(cs *cluster.ClusterStats, diff cluster.StatDiff, navbar []components.NavItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h3>Cluster Stats</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{"table-auto", "w-full"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<table class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var3).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 = []any{"caption-top"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<caption class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var4).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">CPU stats</caption> <thead><tr><th>Sockets</th><th>Cores</th><th>Threads</th><th>Allocated</th></tr></thead> <tbody><tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cs.CPU.Sockets))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 27, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cs.CPU.Cores))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 30, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cs.CPU.Threads))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 33, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cs.CPU.Allocated))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 36, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr><tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(diff.CPU.Sockets))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 41, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(diff.CPU.Cores))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 44, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(diff.CPU.Threads))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 47, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(diff.CPU.Allocated))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/cluster.templ`, Line: 50, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr></tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layouts.Manager("ClustVirt", "Cluster Manager", navbar).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,13 @@
package components
templ A(href string, text string) {
<a href={ templ.URL(href) }>
{ text }
</a>
}
templ ANewTab(href string, text string) {
<a href={ templ.URL(href) } target="_blank">
{ text }
</a>
}

View File

@ -0,0 +1,103 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func A(href string, text string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL = templ.URL(href)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/components/anchor.templ`, Line: 4, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func ANewTab(href string, text string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL = templ.URL(href)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" target=\"_blank\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/components/anchor.templ`, Line: 10, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,15 @@
package components
import "fmt"
templ List(listImage string) {
<ul class={
"list-inside",
fmt.Sprintf("list-image-%s", listImage) }>
{ children... }
</ul>
}
templ ListItem() {
<li>{ children... }</li>
}

View File

@ -0,0 +1,92 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "fmt"
func List(listImage string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{
"list-inside",
fmt.Sprintf("list-image-%s", listImage)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func ListItem() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var3.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,5 @@
package components
templ ContentP() {
<p>{ children... }</p>
}

View File

@ -0,0 +1,43 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func ContentP() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,12 @@
package components
templ FlexRow(wrap bool) {
<div class={
"flex",
templ.KV("flex-nowrap", !wrap),
templ.KV("flex-wrap", wrap),
templ.KV("md:flex-nowrap", wrap)
}>
{ children... }
</div>
}

View File

@ -0,0 +1,60 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func FlexRow(wrap bool) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{
"flex",
templ.KV("flex-nowrap", !wrap),
templ.KV("flex-wrap", wrap),
templ.KV("md:flex-nowrap", wrap)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,5 @@
package components
templ Hero() {
{ children... }
}

View File

@ -0,0 +1,35 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Hero() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,24 @@
package components
import "fmt"
type InfoBoxConfig struct {
ColourClass string
InFlex bool
FlexBasis string
}
templ InfoBox(title string, colour string) {
<div class={
"infobox",
"h-full",
fmt.Sprintf("bg-%s-900", colour),
fmt.Sprintf("text-%s-100", colour),
fmt.Sprintf("shadow-%s-900", colour),
fmt.Sprintf("divide-%s-800", colour),
fmt.Sprintf("border-%s-800", colour),
}>
<h3 class="text-xl font-semibold">{ title }</h3>
{ children... }
</div>
}

View File

@ -0,0 +1,85 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "fmt"
type InfoBoxConfig struct {
ColourClass string
InFlex bool
FlexBasis string
}
func InfoBox(title string, colour string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{
"infobox",
"h-full",
fmt.Sprintf("bg-%s-900", colour),
fmt.Sprintf("text-%s-100", colour),
fmt.Sprintf("shadow-%s-900", colour),
fmt.Sprintf("divide-%s-800", colour),
fmt.Sprintf("border-%s-800", colour),
}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><h3 class=\"text-xl font-semibold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/components/infobox.templ`, Line: 20, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,40 @@
package components
templ NavBar(hero templ.Component, navItems []NavItem) {
<nav
class={ "w-100",
"bg-uigrey-600",
"shadow",
"shadow-uigrey-700",
"rounded",
"rounded-full",
"md:px-auto" }
>
<div
class={ "md:h-16",
"h-28",
"mx-auto",
"md:px-4",
"container",
"flex",
"items-center",
"justify-between",
"flex-wrap",
"md:flex-nowrap" }
>
<a href="/">
@hero
</a>
<div class={ "text-uigrey-200", "font-semibold", "md:w-auto", "md:order-2", "order-3" }>
<ul class={ "flex", "justify-between", "sm:w-full", "md:w-auto" }>
for _, ni := range navItems {
<li class={ "hover:text-uiblue-200", "md:px-4", "md:py-2" }>
<a href={ templ.URL(ni.Href) }>{ ni.Name }</a>
</li>
}
</ul>
</div>
<div class={ "order-2", "md:order-3" }>Login</div>
</div>
</nav>
}

View File

@ -0,0 +1,184 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func NavBar(hero templ.Component, navItems []NavItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"w-100",
"bg-uigrey-600",
"shadow",
"shadow-uigrey-700",
"rounded",
"rounded-full",
"md:px-auto"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{"md:h-16",
"h-28",
"mx-auto",
"md:px-4",
"container",
"flex",
"items-center",
"justify-between",
"flex-wrap",
"md:flex-nowrap"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var3).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"/\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = hero.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 = []any{"text-uigrey-200", "font-semibold", "md:w-auto", "md:order-2", "order-3"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var4).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 = []any{"flex", "justify-between", "sm:w-full", "md:w-auto"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var5).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, ni := range navItems {
var templ_7745c5c3_Var6 = []any{"hover:text-uiblue-200", "md:px-4", "md:py-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var6).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL = templ.URL(ni.Href)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(ni.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/components/navbar.templ`, Line: 31, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 = []any{"order-2", "md:order-3"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var9).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">Login</div></div></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

14
view/components/util.go Normal file
View File

@ -0,0 +1,14 @@
package components
import (
"fmt"
)
type NavItem struct {
Name string
Href string
}
func NoWrapSize(size string) string {
return fmt.Sprintf("%s:flex-nowrap", size)
}

View File

@ -1,4 +0,0 @@
{{ define "content" }}
<h1>{{.HostName}}</h1>
<p>This will contain all the graphs, storage pool lists, vm lists, etc for the host selected.</p>
{{ end }}

133
view/hostinfo.templ Normal file
View File

@ -0,0 +1,133 @@
package view
import (
"fmt"
"git.staur.ca/stobbsm/clustvirt/lib/host"
"git.staur.ca/stobbsm/clustvirt/view/components"
"git.staur.ca/stobbsm/clustvirt/view/layouts"
)
script memchart(used uint64, free uint64, buf uint64, cache uint64) {
ctx = document.getElementById('memchart');
new Chart(ctx, {
type: 'pie',
data: {
labels: [
'Used',
'Free',
'Cached',
'Buffered'
],
datasets: [{
label: 'Memory Usage',
data: [
used,
free,
cache,
buf,
],
backgroundColor: [
'rgb(255,0,0)',
'rgb(0,255,0)',
'rgb(128,128,0)',
'rgb(0,0,255)'
],
hoverOffset: 4
}],
}
});
}
// MemChart get's the memory pi chart from the host
templ MemChart(h *host.Host) {
<div class={ "flex" , "flex-row" }>
<div class={ "size-40" , "md:size-80" }>
<canvas id="memchart" class={ "size-40" , "md:size-80" }></canvas>
</div>
<ul>
<li>Free: <span id="memfree">{ fmt.Sprintf("%d Gi", h.NodeMemory.Free/1024/1024) }</span></li>
<li>Cached: <span id="memcach">{ fmt.Sprintf("%d Gi", h.NodeMemory.Cached/1024/1024) }</span></li>
<li>Buffers: <span id="membuf">{ fmt.Sprintf("%d Gi", h.NodeMemory.Buffers/1024/1024) }</span></li>
<li>Total: <span id="memtot">{ fmt.Sprintf("%d Gi", h.NodeMemory.Total/1024/1024) }</span></li>
</ul>
</div>
@memchart(h.NodeMemory.Total-h.NodeMemory.Free-h.NodeMemory.Buffers-h.NodeMemory.Cached, h.NodeMemory.Free,
h.NodeMemory.Buffers, h.NodeMemory.Cached)
}
templ hostButton(hostname string) {
<button
hx-target="#sysContent"
hx-get={ fmt.Sprintf("/htmx/host/%s", hostname) }
class={ "rounded",
"border",
"border-solid",
"border-uiblue-700",
"text-uigrey-200",
"bg-uiblue-800",
"hover:border-uipurple-400",
"hover:text-ui-grey-800",
"hover:bg-uipurple-600",
"gap-2",
"p-1",
"my-1",
"w-full",
"flex",
"flex-row",
"justify-between" }
>
<span class={ "px-2" }>{ hostname }</span>
<img class={ "htmx-indicator" , "inline-block" , "h-6" , "px-2" } src="/static/images/bars.svg"/>
</button>
}
// HostConnect is the page that allows us to select a host to get information from
templ HostMain(navBarItems []components.NavItem) {
@layouts.Manager("ClustVirt", "Cluster Manager", navBarItems) {
<div class={ "flex" , "flex-row" , "h-full" , "mt-2" }>
<div id="sysNavBar" class={ "w-1/6" , "border-uigrey-600" , "border" , "rounded" , "border-dotted" , "p-2" }>
<ul>
<li>
@hostButton("venus.staur.ca")
</li>
<li>
@hostButton("earth.staur.ca")
</li>
<li>
@hostButton("mars.staur.ca")
</li>
</ul>
</div>
<div id="sysContent" class={ "w-3/4" , "px-2" }>
<p>This is where you can see a system overview of all available hosts</p>
<p>
For now, there is just this simple box to choose a host to connect to
and push the button to load the system information via HTMX
</p>
</div>
</div>
}
}
// HostInfo is meant to be an HTMX response
templ HostInfo(h *host.Host) {
<div class={ "flex" , "flex-col" }>
<div
class={ "flex" , "flex-row" , "justify-start" , "px-2" }
hx-trigger="every 30s"
hx-get={ fmt.Sprintf("/htmx/host/%s/stats", h.HostName) }
hx-target="#sysInfo"
>
<h3>{ h.HostName }</h3>
<img class={ "h-6" , "px-2" , "htmx-indicator" , "inline-block" } src="/static/images/grid.svg"/>
</div>
<div class={ "flex" , "flex-row" , "flex-wrap" } id="sysInfo">
@SysInfo(h)
</div>
</div>
}
templ SysInfo(h *host.Host) {
@MemChart(h)
}

545
view/hostinfo_templ.go Normal file
View File

@ -0,0 +1,545 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package view
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import (
"fmt"
"git.staur.ca/stobbsm/clustvirt/lib/host"
"git.staur.ca/stobbsm/clustvirt/view/components"
"git.staur.ca/stobbsm/clustvirt/view/layouts"
)
func memchart(used uint64, free uint64, buf uint64, cache uint64) templ.ComponentScript {
return templ.ComponentScript{
Name: `__templ_memchart_9d4c`,
Function: `function __templ_memchart_9d4c(used, free, buf, cache){ctx = document.getElementById('memchart');
new Chart(ctx, {
type: 'pie',
data: {
labels: [
'Used',
'Free',
'Cached',
'Buffered'
],
datasets: [{
label: 'Memory Usage',
data: [
used,
free,
cache,
buf,
],
backgroundColor: [
'rgb(255,0,0)',
'rgb(0,255,0)',
'rgb(128,128,0)',
'rgb(0,0,255)'
],
hoverOffset: 4
}],
}
});
}`,
Call: templ.SafeScript(`__templ_memchart_9d4c`, used, free, buf, cache),
CallInline: templ.SafeScriptInline(`__templ_memchart_9d4c`, used, free, buf, cache),
}
}
// MemChart get's the memory pi chart from the host
func MemChart(h *host.Host) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"flex", "flex-row"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{"size-40", "md:size-80"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var3).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 = []any{"size-40", "md:size-80"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<canvas id=\"memchart\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var4).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"></canvas></div><ul><li>Free: <span id=\"memfree\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d Gi", h.NodeMemory.Free/1024/1024))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/hostinfo.templ`, Line: 48, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></li><li>Cached: <span id=\"memcach\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d Gi", h.NodeMemory.Cached/1024/1024))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/hostinfo.templ`, Line: 49, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></li><li>Buffers: <span id=\"membuf\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d Gi", h.NodeMemory.Buffers/1024/1024))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/hostinfo.templ`, Line: 50, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></li><li>Total: <span id=\"memtot\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d Gi", h.NodeMemory.Total/1024/1024))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/hostinfo.templ`, Line: 51, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></li></ul></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = memchart(h.NodeMemory.Total-h.NodeMemory.Free-h.NodeMemory.Buffers-h.NodeMemory.Cached, h.NodeMemory.Free,
h.NodeMemory.Buffers, h.NodeMemory.Cached).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func hostButton(hostname string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var10 = []any{"rounded",
"border",
"border-solid",
"border-uiblue-700",
"text-uigrey-200",
"bg-uiblue-800",
"hover:border-uipurple-400",
"hover:text-ui-grey-800",
"hover:bg-uipurple-600",
"gap-2",
"p-1",
"my-1",
"w-full",
"flex",
"flex-row",
"justify-between"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button hx-target=\"#sysContent\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/htmx/host/%s", hostname)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var10).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 = []any{"px-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var11).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(hostname)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/hostinfo.templ`, Line: 79, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 = []any{"htmx-indicator", "inline-block", "h-6", "px-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var13).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" src=\"/static/images/bars.svg\"></button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
// HostConnect is the page that allows us to select a host to get information from
func HostMain(navBarItems []components.NavItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil {
templ_7745c5c3_Var14 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var15 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
var templ_7745c5c3_Var16 = []any{"flex", "flex-row", "h-full", "mt-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var16).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 = []any{"w-1/6", "border-uigrey-600", "border", "rounded", "border-dotted", "p-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"sysNavBar\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var17).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><ul><li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = hostButton("venus.staur.ca").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li><li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = hostButton("earth.staur.ca").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li><li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = hostButton("mars.staur.ca").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li></ul></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 = []any{"w-3/4", "px-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"sysContent\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var18).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><p>This is where you can see a system overview of all available hosts</p><p>For now, there is just this simple box to choose a host to connect to and push the button to load the system information via HTMX</p></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layouts.Manager("ClustVirt", "Cluster Manager", navBarItems).Render(templ.WithChildren(ctx, templ_7745c5c3_Var15), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
// HostInfo is meant to be an HTMX response
func HostInfo(h *host.Host) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
if templ_7745c5c3_Var19 == nil {
templ_7745c5c3_Var19 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var20 = []any{"flex", "flex-col"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var20).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 = []any{"flex", "flex-row", "justify-start", "px-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var21).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-trigger=\"every 30s\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/htmx/host/%s/stats", h.HostName)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"#sysInfo\"><h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(h.HostName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/hostinfo.templ`, Line: 121, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 = []any{"h-6", "px-2", "htmx-indicator", "inline-block"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var23).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" src=\"/static/images/grid.svg\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 = []any{"flex", "flex-row", "flex-wrap"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var24).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" id=\"sysInfo\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = SysInfo(h).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func SysInfo(h *host.Host) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
if templ_7745c5c3_Var25 == nil {
templ_7745c5c3_Var25 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = MemChart(h).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

7
view/layouts/common.go Normal file
View File

@ -0,0 +1,7 @@
package layouts
// LayoutConfig specifics common elements of layouts
type LayoutConfig struct {
MainTitle string
SubTitle string
}

View File

@ -0,0 +1,30 @@
package layouts
import "git.staur.ca/stobbsm/clustvirt/view/components"
templ Manager(title string, subtitle string, navBarItem []components.NavItem) {
<!DOCTYPE html>
<html class={ "text-slate-50" , "bg-slate-900" }>
<head>
<title>ClustVirt</title>
<link href="/static/css/style.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<div class={ "flex" , "flex-col" , "h-screen" }>
<header class={ "px-4" }>
@header(title, subtitle, navBarItem)
</header>
<main class={ "px-4" , "h-full" }>
{ children... }
</main>
<footer class={ "px-4" , "bottom-0", "w-full" , "justify-self-end" }>
@footer()
</footer>
</div>
<!-- Load HTMX -->
<script src=" https://unpkg.com/htmx.org@1.9.11" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
</body>
</html>
}

View File

@ -0,0 +1,146 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package layouts
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "git.staur.ca/stobbsm/clustvirt/view/components"
func Manager(title string, subtitle string, navBarItem []components.NavItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 = []any{"text-slate-50", "bg-slate-900"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<html class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><head><title>ClustVirt</title><link href=\"/static/css/style.css\" type=\"text/css\" rel=\"stylesheet\"></head><body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{"flex", "flex-col", "h-screen"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var3).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 = []any{"px-4"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<header class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var4).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = header(title, subtitle, navBarItem).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 = []any{"px-4", "h-full"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var5).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 = []any{"px-4", "bottom-0", "w-full", "justify-self-end"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var6).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</footer></div><!-- Load HTMX --><script src=\" https://unpkg.com/htmx.org@1.9.11\" integrity=\"sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0\" crossorigin=\"anonymous\">\n </script><script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,41 @@
package layouts
import "git.staur.ca/stobbsm/clustvirt/view/components"
templ StaticPage(title string, subtitle string, navBarItems []components.NavItem) {
<!DOCTYPE html>
<html class={ "text-uigrey-100" , "bg-uigrey-900" }>
<head>
<title>Clustvirt</title>
<link href="/static/css/style.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<header>
@header(title, subtitle, navBarItems)
</header>
<div id="content" name="content" class={ "flex" , "flex-col" , "gap-2" }>
{ children... }
</div>
<footer>
@footer()
</footer>
</body>
</html>
}
templ hero(title string) {
<h1 class={ "text-2xl", "font-bold", "text-uiblue-200", "md:order-1" }>{ title }</h1>
}
templ header(title string, subtitle string, navBarItems []components.NavItem) {
<h2 class={ "text-lg" , "font-semibold" , "italic" , "h-6" }>{ subtitle }</h2>
@components.NavBar(hero(title), navBarItems)
}
templ footer() {
<div class="flex gap-4 md:gap-6 sm:gap-8 divide divide-solid">
<div id="footer_left" class="flex-auto basis-1/4 md:basis-1/3 p-2">Left</div>
<div id="footer_middle" class="flex-auto basis-1/2 md:basis-1/3 p-2">Middle</div>
<div id="footer_right" class="flex-auto basis-1/4 md:basis-1/3 p-2">Right</div>
</div>
}

View File

@ -0,0 +1,223 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package layouts
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "git.staur.ca/stobbsm/clustvirt/view/components"
func StaticPage(title string, subtitle string, navBarItems []components.NavItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 = []any{"text-uigrey-100", "bg-uigrey-900"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<html class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var2).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><head><title>Clustvirt</title><link href=\"/static/css/style.css\" type=\"text/css\" rel=\"stylesheet\"></head><body><header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = header(title, subtitle, navBarItems).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{"flex", "flex-col", "gap-2"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"content\" name=\"content\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var3).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</footer></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func hero(title string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var5 = []any{"text-2xl", "font-bold", "text-uiblue-200", "md:order-1"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var5).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layouts/staticpage.templ`, Line: 26, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func header(title string, subtitle string, navBarItems []components.NavItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var8 = []any{"text-lg", "font-semibold", "italic", "h-6"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var8).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layouts/staticpage.templ`, Line: 30, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.NavBar(hero(title), navBarItems).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func footer() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex gap-4 md:gap-6 sm:gap-8 divide divide-solid\"><div id=\"footer_left\" class=\"flex-auto basis-1/4 md:basis-1/3 p-2\">Left</div><div id=\"footer_middle\" class=\"flex-auto basis-1/2 md:basis-1/3 p-2\">Middle</div><div id=\"footer_right\" class=\"flex-auto basis-1/4 md:basis-1/3 p-2\">Right</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -1,43 +0,0 @@
package view
import (
"log"
"os"
)
// Regular pages are defined as views here, like the homepage
// Major components of each page.
const (
index = `view/_index.gohtml`
header = `view/_header.gohtml`
footer = `view/_footer.gohtml`
)
// These constitute the static parts of the site that don't need to change, loaded as a template for rendering
const (
home = `view/static/home.gohtml`
)
func init() {
if fi, err := os.Stat(index); err != nil {
log.Fatal(fi.Name(), fi.IsDir(), err)
}
if fi, err := os.Stat(header); err != nil {
log.Fatal(fi.Name(), fi.IsDir(), err)
}
if fi, err := os.Stat(footer); err != nil {
log.Fatal(fi.Name(), fi.IsDir(), err)
}
if fi, err := os.Stat(home); err != nil {
log.Fatal(fi.Name(), fi.IsDir(), err)
}}
var ViewHome *View
func init() {
log.Println("Initializing homepage")
var err error
ViewHome, err = NewFromFile(home)
log.Println(err)
}

View File

@ -1,166 +0,0 @@
{{ define "content" }}
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double divide-solid divide-y-2
bg-dracula-900 text-dracula-100 shadow-dracula-900
divide-dracula-800 border-dracula-800">
<h3 class="text-xl font-semibold">What is this?</h3>
<p>Clustvirt (work in progress name) aims to be the agnostic
cluster controller for libvirtd. The server component is
used to display both the WebUI and run the REST API used to
control one to many libvirtd hosts to manage virual machines,
LXC containers (through libvirtd), gather information about
each host, and monitor each host.</p>
</div>
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double divide-solid divide-y-2
bg-blade-900 text-blade-100 shadow-blade-900
divide-blade-800 border-blade-800">
<h3 class="text-xl font-semibold">Project Goals</h4>
<ul class="leading-relaxed list-image-accepted list-inside">
<li class="accepted">Open source, currently on the MIT license</li>
<li class="notimpl">Base OS Agnostic. If it can run libvirtd, this should be
able to control it on some level</li>
<li class="notimpl">Control the Virtual Machine life cycle
on one or more libvirtd hosts</li>
<li class="notimpl">Add clusting capabilities to libvirtd host,
including;</li>
<li class="notimpl">Migration of VMs</li>
<li class="notimpl">Syncronizing secrets</li>
<li class="notimpl">Syncronizing VLANs, bridges, host only
networking</li>
<li class="notimpl">Sharing HA storage availability</li>
<li class="notimpl">Locking shared resources like disks</li>
<li class="notimpl">Starting VMs marked for HA on another host
when one goes down</li>
<li class="notimpl">Manage a library of Cloud-init resources
and templates to build new VMs quickly</li>
<li class="notimpl">Local Storage management, including local
directory, lvm, zfs (if installed)</li>
<li class="notimpl">Advanced Storage management, such as Ceph,
glusterfs, drbd, iscsi, nfs</li>
<li class="notimpl">Storage syncronization of local disks
between hosts (zfs snapshots, lvm snapshots, rsync)</li>
<li class="notimpl">Backup scheduling, creation, restoration</li>
</ul>
</div>
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double divide-solid divide-y-2
bg-cullen-900 text-cullen-100 shadow-cullen-900
divide-cullen-800 border-cullen-800">
<h3 class="text-xl font-semibold">Stretch Goals</h3>
<ul class="leading-relaxed list-image-possible list-inside">
<li>Install the OS which libvirtd is running on</li>
<li>Install/provision libvirtd on a host that does not have
it installed</li>
<li>Tools to move from one vendor to clustvirt/libvirtd</li>
<li>VM templates for common aspects of VM creation and management,
like appliances</li>
<li>External tool access that can be used to manage things that
are not managed here (cephadm dashboard, for instance)</li>
</ul>
</div>
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double divide-solid divide-y-2
bg-morbius-900 text-morbius-100 shadow-morbius-900
divide-morbius-800 border-morbius-800">
<h3 class="text-xl font-semibold">Reddit Requested Features</h3>
<ul class="list-image-possible list-inside leading-relaxed">
<li>Search/Filter on hosts/vms - @Lopsided_Speaker_553</li>
<li>Balance on resource usage per host/Automattically migrate to
least used host - @Lopsided_Speaker_553</li>
<li>Support inter-vm only commmunication (VxLAN style)
- @Lopsided_Speaker_553</li>
<li>Deploy VMs using only API - @Lopsided_Speaker_553</li>
<li>Well documented, first class API - @kasperlitheater</li>
<li>Bootstrap service to configure a new server - @phatpappa_</li>
<li>For the love of kitten, don't use XML as configuration files
- @pascalbrax</li>
<li>Expose the Cluster Manager functionalities as API - @raven2611</li>
<li>CPU architecture awareness for migrations - @raven2611</li>
<li>Inter VM Communications via VXLAN/EVPN - @raven2611</li>
</ul>
</div>
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double divide-solid divide-y-2
bg-marcelin-900 text-marcelin-100 shadow-marcelin-900
divide-marcelin-800 border-marcelin-800">
<h3 class="text-xl font-semibold">Never Going to Happen</h3>
<ul class="list-image-never list-inside leading-relaxed">
<li>Kubernetes</li>
<li>Application container management (docker, podman, etc)</li>
<li>Become an OS</li>
<li>Have a paywall</li>
<li>Vendor lock-in</li>
<li>Become a commercial entity (even indirectly)</li>
<li>Anything that does not have an Open Source standard behind it</li>
<li>Directly control a guest Operating System</li>
</ul>
</div>
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double divide-solid divide-y-2
bg-dracula-900 text-dracula-100 shadow-dracula-900
divide-dracula-800 border-dracula-800">
<h3 class="text-xl font-semibold">Why?</h3>
<ul class="list-image-informational list-inside leading-relaxed">
<li>Broadcom buying VMWare, and VMWare losing a free teir for
homelabbers pissed me off</li>
<li>Vendor lock-in pisses me off</li>
<li>Even good open source Hyperconverged systems (Proxmox, as
an example) exhibit a form of vendor lock-in</li>
<li>Libvirt is terrific, has the functionality for everything
those other providers do, but there really is not a
great option for those dipping their toes into Open Source</li>
<li>Its fun to build things that solve a need</li>
<li>I really want to do it</li>
</ul>
</div>
<div class="m-2 px-4 pt-2 flex-col shadow-md
rounded-2xl border-8 border-double
bg-voncount-900 text-voncount-100 shadow-voncount-900
divide-voncount-800 border-voncount-800">
<h3 class="text-xl font-semibold border-b-2 border-voncount-800">Other notes</h3>
<p>I recently created a <a href="http://redd.it/1bct15z"
class="after:content-link after:w-3 after:h-3 after:inline-block
after:invert text-base text-morbius-100">post</a>
on reddit announcing that I was building this, and while the majority
of responses were supportive, even offering features that may enhance
what I originally set out to do, many responded with
"Why do we need another one??"</p>
<p>Besides the list above about why this exists, I wanted to clarify
a few things those individuals did not seeem to get: This is not
a rebuild of Proxmox, Cloudstack, VMWare, Harvester or any of
the other "Hyper-converged/Single-solution/turnkey/Operating System"
offerings out there. This will not take over your base operating system
on your machine, just act as a cluster manager and interface to access
the existing libvirtd instances on those machines, nor will it prescribe
a set of requirements that make it hard to move your own infrastructure
around.</p>
<p>At the heart of this project is that I hate the enshitifiation of
Open Source that has been going on, where its just another way to make
money and control the eco system. RedHat tried to do it by locking
down their source code, Proxmox does it by making sure anything you
do on Proxmox is tied to Proxmox (no offense to Proxmox), and even
Hashicorp, who I loved so dearly, changed from a pure Open Source
licensing model to one that protects the business over the community.</p>
<p>I will not let that happen here.</p>
<p>This project will seek to use the Unix philosophy, of building off
of existing standards, combining tools, and having one tool do one
job well. This does not mean there will be one application for each
aspect of the job, but that this application stack will manage Libvirtd
well, and have individual and configurable paths to manage each sub-aspect
of the libvirt stack. This stack will not create a Ceph cluster for you,
it leaves you to do that. It will not even talk to a ceph cluster.
It will, however, let you add that cluster via configuration options to
define it as a storage pool that libvirt can use.</p>
<p>If you want something that will allow you to use a single interface
to create all sub aspects that can be used by libvirt
(managing all firewall rules, creating a ceph cluster, etc.),
use something like Proxmox which includes that builtin
functionality. This isn't the stack for you.<p>
</div>
{{ end }}

257
view/static/home.templ Normal file
View File

@ -0,0 +1,257 @@
package static
import (
"git.staur.ca/stobbsm/clustvirt/view/layouts"
"git.staur.ca/stobbsm/clustvirt/view/components"
)
templ Home() {
@layouts.StaticPage("ClustVirt", "Libvirt, clustered and managed", []components.NavItem{
{Name: "What", Href: "#"},
{Name: "Why", Href: "#"},
{Name: "Goals", Href: "#"},
{Name: "Stretch", Href: "#"},
{Name: "Requests", Href: "#"},
{Name: "Never", Href: "#"},
{Name: "Notes", Href: "#"},
}) {
<div>
@components.InfoBox("What is This?", "uiorange") {
<div class={"mx-4"}>
@components.ContentP() {
Clustvirt (work in progress name) aims to be the agnostic
cluster controller for libvirtd. The server component is
used to display both the WebUI and run the REST API used to
control one to many libvirtd hosts to manage virual machines,
LXC containers (through libvirtd), gather information about
each host, and monitor each host.
}
</div>
}
</div>
<div>
@components.InfoBox("Why?", "uiblue") {
@components.List("informational") {
@components.ListItem() {
Broadcom buying VMWare, and VMWare losing a free
teir for homelabbers pissed me off
}
@components.ListItem() {
Vendor lock-in pisses me off
}
@components.ListItem() {
Even good open source Hyperconverged systems
(Proxmox, as an example) exhibit a form of vendor lock-in
}
@components.ListItem() {
Libvirt is terrific, has the functionality for everything
those other providers do, but there really is not a great
option for those dipping their toes into Open Source
}
@components.ListItem() {
Its fun to build things that solve a need
}
@components.ListItem() {
I really want to do it
}
}
}
</div>
<div>
@components.InfoBox("Project Goals", "uigreen") {
@components.List("accepted") {
@components.ListItem() {
Open source, currently on the MIT license
}
@components.ListItem() {
Base OS Agnostic. If it can run libvirtd, this should be able to control it on some level
}
@components.ListItem() {
Control the Virtual Machine life cycle on one or more libvirtd hosts
}
@components.ListItem() {
Add clusting capabilities to libvirtd host, including;
}
@components.ListItem() {
Migration of VMs
}
@components.ListItem() {
Syncronizing secrets
}
@components.ListItem() {
Syncronizing VLANs, bridges, host only networking
}
@components.ListItem() {
Sharing HA storage availability
}
@components.ListItem() {
Locking shared resources like disks
}
@components.ListItem() {
Starting VMs marked for HA on another host when one goes down
}
@components.ListItem() {
Manage a library of Cloud-init resources and templates to build new VMs quickly
}
@components.ListItem() {
Local Storage management, including local directory, lvm, zfs (if installed)
}
@components.ListItem() {
Advanced Storage management, such as Ceph, glusterfs, drbd, iscsi, nfs
}
@components.ListItem() {
Storage syncronization of local disks between hosts (zfs snapshots, lvm snapshots, rsync)
}
@components.ListItem() {
Backup scheduling, creation, restoration
}
}
}
</div>
<div>
@components.InfoBox("Stretch Goals", "uipurple") {
@components.List("possible") {
@components.ListItem() {
Install the OS which libvirtd is running on
}
@components.ListItem() {
Install/provision libvirtd on a host that does not have it installed
}
@components.ListItem() {
Tools to move from one vendor to clustvirt/libvirtd
}
@components.ListItem() {
VM templates for common aspects of VM creation and management,
like appliances
}
@components.ListItem() {
External tool access that can be used to manage things that are not
managed here (cephadm dashboard, for instance)
}
}
}
</div>
<div>
@components.InfoBox("Reddit Requested Feature", "uiyellow") {
@components.List("possible") {
@components.ListItem() {
Search/Filter on hosts/vms - @Lopsided_Speaker_553
}
@components.ListItem() {
Balance on resource usage per host/Automattically migrate to least
used host - @Lopsided_Speaker_553
}
@components.ListItem() {
Support inter-vm only commmunication (VxLAN style) - @Lopsided_Speaker_553
}
@components.ListItem() {
Deploy VMs using only API - @Lopsided_Speaker_553
}
@components.ListItem() {
Well documented, first class API - @kasperlitheater
}
@components.ListItem() {
Bootstrap service to configure a new server - @phatpappa_
}
@components.ListItem() {
For the love of kitten, don't use XML as configuration files - @pascalbrax
}
@components.ListItem() {
Expose the Cluster Manager functionalities as API - @raven2611
}
@components.ListItem() {
CPU architecture awareness for migrations - @raven2611
}
@components.ListItem() {
Inter VM Communications via VXLAN/EVPN - @raven2611
}
}
}
</div>
<div class={ }>
@components.InfoBox("Never Going to Happen", "uired") {
@components.List("never") {
@components.ListItem() {
Kubernetes
}
@components.ListItem() {
Application container management (docker, podman, etc)
}
@components.ListItem() {
Become an OS
}
@components.ListItem() {
Have a paywall
}
@components.ListItem() {
Vendor lock-in
}
@components.ListItem() {
Become a commercial entity (even indirectly)
}
@components.ListItem() {
Anything that does not have an Open Source standard behind it
}
@components.ListItem() {
Directly control a guest Operating System
}
}
}
</div>
@components.InfoBox("Other things to note", "uipink") {
<div class={"flex", "flex-col", "gap-4", "mx-4"}>
@components.ContentP() {
I recently created a
<span>
@components.ANewTab("http://redd.it/1bct15z", "post")
</span>
on reddit announcing that I was building this, and while the majority
of responses were supportive, even offering features that may enhance
what I originally set out to do, many responded with
"Why do we need another one??"
}
@components.ContentP() {
Besides the list above about why this exists, I wanted to clarify
a few things those individuals did not seeem to get: This is not
a rebuild of Proxmox, Cloudstack, VMWare, Harvester or any of
the other "Hyper-converged/Single-solution/turnkey/Operating System"
offerings out there. This will not take over your base operating system
on your machine, just act as a cluster manager and interface to access
the existing libvirtd instances on those machines, nor will it prescribe
a set of requirements that make it hard to move your own infrastructure
around.
}
@components.ContentP() {
At the heart of this project is that I hate the enshitifiation of
Open Source that has been going on, where its just another way to make
money and control the eco system. RedHat tried to do it by locking
down their source code, Proxmox does it by making sure anything you
do on Proxmox is tied to Proxmox (no offense to Proxmox), and even
Hashicorp, who I loved so dearly, changed from a pure Open Source
licensing model to one that protects the business over the community
}
@components.ContentP() {
I will not let that happen here.
}
@components.ContentP() {
This project will seek to use the Unix philosophy, of building off
of existing standards, combining tools, and having one tool do one
job well. This does not mean there will be one application for each
aspect of the job, but that this application stack will manage Libvirtd
well, and have individual and configurable paths to manage each sub-aspect
of the libvirt stack. This stack will not create a Ceph cluster for you,
it leaves you to do that. It will not even talk to a ceph cluster.
It will, however, let you add that cluster via configuration options to
define it as a storage pool that libvirt can use.
}
@components.ContentP() {
If you want something that will allow you to use a single interface
to create all sub aspects that can be used by libvirt
(managing all firewall rules, creating a ceph cluster, etc.),
use something like Proxmox which includes that builtin
functionality. This isn't the stack for you.
}
</div>
}
}
}

1459
view/static/home_templ.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
// Package view handles WebUI generation for clustvirt. The methods and utilties in this module control what is viewed,
// templates that are loaded, and building those templates. Caching is not considered beyond what is done
// automattically by go (if anything).
package view
import (
"html/template"
"io"
"log"
"os"
)
// View is responsible for assembling a group of templates, providing
// methods to add data and compose pages in a common way.
type View struct {
content string
template *template.Template
}
var basetemplate *template.Template
// New returns a new instance of the View, expecting the content to be the actual
// content as a template defining "content", in string format.
func New(content string) *View {
if basetemplate == nil {
log.Println("Initializing base template")
basetemplate = template.Must(template.New("").ParseFiles(index, header, footer))
}
log.Println("Cloning base template")
v := &View{template: template.Must(basetemplate.Clone())}
v.parse(content)
return v
}
// NewFromFile loads a template from a file
func NewFromFile(file string) (*View, error) {
b, err := os.ReadFile(file)
return New(string(b)), err
}
func (v *View) parse(tmpl string) error {
log.Println("Parsing template contents")
if _, err := v.template.Parse(tmpl); err != nil {
return err
}
log.Println("Template parsed")
return nil
}
// Render returns the executed template with data
func (v *View) Render(w io.Writer, data any) error {
log.Println("Excuting template")
return v.template.ExecuteTemplate(w, "_index", data)
}