implement server side rendering for account page,
remove chartjs module
This commit is contained in:
@@ -2,6 +2,7 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
|
|
||||||
@@ -17,6 +18,68 @@ type Account struct {
|
|||||||
Min int
|
Min int
|
||||||
Max int
|
Max int
|
||||||
}
|
}
|
||||||
|
Resources map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type Constraint struct {
|
||||||
|
Max int64
|
||||||
|
Used int64
|
||||||
|
Avail int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Match struct {
|
||||||
|
Name string
|
||||||
|
Match string
|
||||||
|
Max int64
|
||||||
|
Used int64
|
||||||
|
Avail int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type NumericResource struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
Multiplier int64
|
||||||
|
Base int64
|
||||||
|
Compact bool
|
||||||
|
Unit string
|
||||||
|
Display bool
|
||||||
|
Global Constraint
|
||||||
|
Nodes map[string]Constraint
|
||||||
|
Total Constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageResource struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
Multiplier int64
|
||||||
|
Base int64
|
||||||
|
Compact bool
|
||||||
|
Unit string
|
||||||
|
Display bool
|
||||||
|
Disks []string
|
||||||
|
Global Constraint
|
||||||
|
Nodes map[string]Constraint
|
||||||
|
Total Constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListResource struct {
|
||||||
|
Type string
|
||||||
|
Whitelist bool
|
||||||
|
Display bool
|
||||||
|
Global []Match
|
||||||
|
Nodes map[string][]Match
|
||||||
|
Total []Match
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceChart struct {
|
||||||
|
Type string
|
||||||
|
Display bool
|
||||||
|
Name string
|
||||||
|
Used int64
|
||||||
|
Max int64
|
||||||
|
Avail float64
|
||||||
|
Prefix string
|
||||||
|
Unit string
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETAccount(c *gin.Context) {
|
func HandleGETAccount(c *gin.Context) {
|
||||||
@@ -28,6 +91,58 @@ func HandleGETAccount(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range account.Resources {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case NumericResource:
|
||||||
|
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||||
|
account.Resources[k] = ResourceChart{
|
||||||
|
Type: t.Type,
|
||||||
|
Display: t.Display,
|
||||||
|
Name: t.Name,
|
||||||
|
Used: t.Total.Used,
|
||||||
|
Max: t.Total.Max,
|
||||||
|
Avail: avail,
|
||||||
|
Prefix: prefix,
|
||||||
|
Unit: t.Unit,
|
||||||
|
}
|
||||||
|
case StorageResource:
|
||||||
|
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||||
|
account.Resources[k] = ResourceChart{
|
||||||
|
Type: t.Type,
|
||||||
|
Display: t.Display,
|
||||||
|
Name: t.Name,
|
||||||
|
Used: t.Total.Used,
|
||||||
|
Max: t.Total.Max,
|
||||||
|
Avail: avail,
|
||||||
|
Prefix: prefix,
|
||||||
|
Unit: t.Unit,
|
||||||
|
}
|
||||||
|
case ListResource:
|
||||||
|
l := struct {
|
||||||
|
Type string
|
||||||
|
Display bool
|
||||||
|
Resources []ResourceChart
|
||||||
|
}{
|
||||||
|
Type: t.Type,
|
||||||
|
Display: t.Display,
|
||||||
|
Resources: []ResourceChart{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range t.Total {
|
||||||
|
l.Resources = append(l.Resources, ResourceChart{
|
||||||
|
Type: t.Type,
|
||||||
|
Display: t.Display,
|
||||||
|
Name: r.Name,
|
||||||
|
Used: r.Used,
|
||||||
|
Max: r.Max,
|
||||||
|
Avail: float64(r.Avail), // usually an int
|
||||||
|
Unit: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
account.Resources[k] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
||||||
"global": common.Global,
|
"global": common.Global,
|
||||||
"page": "account",
|
"page": "account",
|
||||||
@@ -39,7 +154,9 @@ func HandleGETAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetUserAccount(username string, token string, csrf string) (Account, error) {
|
func GetUserAccount(username string, token string, csrf string) (Account, error) {
|
||||||
account := Account{}
|
account := Account{
|
||||||
|
Resources: map[string]any{},
|
||||||
|
}
|
||||||
|
|
||||||
ctx := common.RequestContext{
|
ctx := common.RequestContext{
|
||||||
Cookies: map[string]string{
|
Cookies: map[string]string{
|
||||||
@@ -49,6 +166,8 @@ func GetUserAccount(username string, token string, csrf string) (Account, error)
|
|||||||
},
|
},
|
||||||
Body: map[string]any{},
|
Body: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get user account basic data
|
||||||
res, code, err := common.RequestGetAPI("/user/config/cluster", ctx)
|
res, code, err := common.RequestGetAPI("/user/config/cluster", ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return account, err
|
return account, err
|
||||||
@@ -56,12 +175,89 @@ func GetUserAccount(username string, token string, csrf string) (Account, error)
|
|||||||
if code != 200 {
|
if code != 200 {
|
||||||
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
|
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mapstructure.Decode(ctx.Body, &account)
|
err = mapstructure.Decode(ctx.Body, &account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return account, err
|
return account, err
|
||||||
} else {
|
} else {
|
||||||
account.Username = username
|
account.Username = username
|
||||||
return account, nil
|
}
|
||||||
|
|
||||||
|
ctx.Body = map[string]any{}
|
||||||
|
// get user resources
|
||||||
|
res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx)
|
||||||
|
if err != nil {
|
||||||
|
return account, err
|
||||||
|
}
|
||||||
|
if code != 200 {
|
||||||
|
return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res)
|
||||||
|
}
|
||||||
|
resources := ctx.Body
|
||||||
|
|
||||||
|
ctx.Body = map[string]any{}
|
||||||
|
// get resource meta data
|
||||||
|
res, code, err = common.RequestGetAPI("/global/config/resources", ctx)
|
||||||
|
if err != nil {
|
||||||
|
return account, err
|
||||||
|
}
|
||||||
|
if code != 200 {
|
||||||
|
return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
|
||||||
|
}
|
||||||
|
meta := ctx.Body["resources"].(map[string]any)
|
||||||
|
|
||||||
|
// build each resource by its meta type
|
||||||
|
for k, v := range meta {
|
||||||
|
m := v.(map[string]any)
|
||||||
|
t := m["type"].(string)
|
||||||
|
r := resources[k].(map[string]any)
|
||||||
|
if t == "numeric" {
|
||||||
|
n := NumericResource{}
|
||||||
|
n.Type = t
|
||||||
|
err_m := mapstructure.Decode(m, &n)
|
||||||
|
err_r := mapstructure.Decode(r, &n)
|
||||||
|
if err_m != nil || err_r != nil {
|
||||||
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
|
}
|
||||||
|
account.Resources[k] = n
|
||||||
|
} else if t == "storage" {
|
||||||
|
n := StorageResource{}
|
||||||
|
n.Type = t
|
||||||
|
err_m := mapstructure.Decode(m, &n)
|
||||||
|
err_r := mapstructure.Decode(r, &n)
|
||||||
|
if err_m != nil || err_r != nil {
|
||||||
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
|
}
|
||||||
|
account.Resources[k] = n
|
||||||
|
} else if t == "list" {
|
||||||
|
n := ListResource{}
|
||||||
|
n.Type = t
|
||||||
|
err_m := mapstructure.Decode(m, &n)
|
||||||
|
err_r := mapstructure.Decode(r, &n)
|
||||||
|
if err_m != nil || err_r != nil {
|
||||||
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
|
}
|
||||||
|
account.Resources[k] = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatNumber(val int64, base int64) (float64, string) {
|
||||||
|
valf := float64(val)
|
||||||
|
basef := float64(base)
|
||||||
|
steps := 0
|
||||||
|
for math.Abs(valf) > basef && steps < 4 {
|
||||||
|
valf /= basef
|
||||||
|
steps++
|
||||||
|
}
|
||||||
|
|
||||||
|
if base == 1000 {
|
||||||
|
prefixes := []string{"", "K", "M", "G", "T"}
|
||||||
|
return valf, prefixes[steps]
|
||||||
|
} else if base == 1024 {
|
||||||
|
prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"}
|
||||||
|
return valf, prefixes[steps]
|
||||||
|
} else {
|
||||||
|
return 0, ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
{{template "head" .}}
|
{{template "head" .}}
|
||||||
<script src="scripts/account.js" type="module"></script>
|
<script src="scripts/account.js" type="module"></script>
|
||||||
<script src="modules/chart.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<link rel="modulepreload" href="scripts/utils.js">
|
<link rel="modulepreload" href="scripts/utils.js">
|
||||||
<link rel="modulepreload" href="scripts/dialog.js">
|
<link rel="modulepreload" href="scripts/dialog.js">
|
||||||
<style>
|
<style>
|
||||||
@@ -56,7 +54,23 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="w3-card w3-padding">
|
<section class="w3-card w3-padding">
|
||||||
<h3>Cluster Resources</h3>
|
<h3>Cluster Resources</h3>
|
||||||
<div id="resource-container"></div>
|
<div id="resource-container">
|
||||||
|
{{range .account.Resources}}
|
||||||
|
{{if .Display}}
|
||||||
|
{{if eq .Type "numeric"}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Type "storage"}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Type "list"}}
|
||||||
|
{{range .Resources}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -1,230 +1,14 @@
|
|||||||
import { dialog } from "./dialog.js";
|
import { dialog } from "./dialog.js";
|
||||||
import { requestAPI, setAppearance } from "./utils.js";
|
import { requestAPI, setAppearance } from "./utils.js";
|
||||||
|
|
||||||
class ResourceChart extends HTMLElement {
|
|
||||||
constructor () {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: "open" });
|
|
||||||
this.shadowRoot.innerHTML = `
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
figure {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
max-width: 400px;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
}
|
|
||||||
figcaption {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style id="responsive-style" media="not all">
|
|
||||||
figure {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
max-height: 1lh;
|
|
||||||
}
|
|
||||||
figcaption {
|
|
||||||
margin: 0;
|
|
||||||
margin-left: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1ch;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<figure>
|
|
||||||
<div>
|
|
||||||
<canvas></canvas>
|
|
||||||
</div>
|
|
||||||
<figcaption></figcaption>
|
|
||||||
</figure>
|
|
||||||
`;
|
|
||||||
this.responsiveStyle = this.shadowRoot.querySelector("#responsive-style");
|
|
||||||
this.canvas = this.shadowRoot.querySelector("canvas");
|
|
||||||
this.caption = this.shadowRoot.querySelector("figcaption");
|
|
||||||
}
|
|
||||||
|
|
||||||
set data (data) {
|
|
||||||
for (const line of data.title) {
|
|
||||||
this.caption.innerHTML += `<span>${line}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvas.role = "img";
|
|
||||||
this.canvas.ariaLabel = data.ariaLabel;
|
|
||||||
|
|
||||||
const chartData = {
|
|
||||||
type: "pie",
|
|
||||||
data: data.data,
|
|
||||||
options: {
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
interaction: {
|
|
||||||
mode: "nearest"
|
|
||||||
},
|
|
||||||
onHover: function (e, activeElements) {
|
|
||||||
if (window.innerWidth <= data.breakpoint) {
|
|
||||||
updateTooltipShow(e.chart, false);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
updateTooltipShow(e.chart, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chart = new window.Chart(this.canvas, chartData);
|
|
||||||
|
|
||||||
if (data.breakpoint) {
|
|
||||||
this.responsiveStyle.media = `screen and (width <= ${data.breakpoint}px)`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.responsiveStyle.media = "not all";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get data () {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a really bad way to do this, but chartjs api does not expose many ways to dynamically set hover and tooltip options
|
|
||||||
function updateTooltipShow (chart, enabled) {
|
|
||||||
chart.options.plugins.tooltip.enabled = enabled;
|
|
||||||
chart.options.interaction.mode = enabled ? "nearest" : null;
|
|
||||||
chart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("resource-chart", ResourceChart);
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", init);
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
const prefixes = {
|
|
||||||
1024: [
|
|
||||||
"",
|
|
||||||
"Ki",
|
|
||||||
"Mi",
|
|
||||||
"Gi",
|
|
||||||
"Ti"
|
|
||||||
],
|
|
||||||
1000: [
|
|
||||||
"",
|
|
||||||
"K",
|
|
||||||
"M",
|
|
||||||
"G",
|
|
||||||
"T"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
setAppearance();
|
setAppearance();
|
||||||
|
|
||||||
let resources = requestAPI("/user/dynamic/resources");
|
|
||||||
let meta = requestAPI("/global/config/resources");
|
|
||||||
|
|
||||||
resources = await resources;
|
|
||||||
meta = (await meta).resources;
|
|
||||||
|
|
||||||
populateResources("#resource-container", meta, resources);
|
|
||||||
|
|
||||||
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeForm);
|
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateResources (containerID, meta, resources) {
|
|
||||||
if (resources instanceof Object) {
|
|
||||||
const container = document.querySelector(containerID);
|
|
||||||
Object.keys(meta).forEach((resourceType) => {
|
|
||||||
if (meta[resourceType].display) {
|
|
||||||
if (meta[resourceType].type === "list") {
|
|
||||||
resources[resourceType].total.forEach((listResource) => {
|
|
||||||
createResourceUsageChart(container, listResource.name, listResource.avail, listResource.used, listResource.max, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
createResourceUsageChart(container, meta[resourceType].name, resources[resourceType].total.avail, resources[resourceType].total.used, resources[resourceType].total.max, meta[resourceType]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createResourceUsageChart (container, resourceName, resourceAvail, resourceUsed, resourceMax, resourceUnitData) {
|
|
||||||
const chart = document.createElement("resource-chart");
|
|
||||||
container.append(chart);
|
|
||||||
const maxStr = parseNumber(resourceMax, resourceUnitData);
|
|
||||||
const usedStr = parseNumber(resourceUsed, resourceUnitData);
|
|
||||||
const usedRatio = resourceUsed / resourceMax;
|
|
||||||
const R = Math.min(usedRatio * 510, 255);
|
|
||||||
const G = Math.min((1 - usedRatio) * 510, 255);
|
|
||||||
const usedColor = `rgb(${R}, ${G}, 0)`;
|
|
||||||
chart.data = {
|
|
||||||
title: [resourceName, `Used ${usedStr} of ${maxStr}`],
|
|
||||||
ariaLabel: `${resourceName} used ${usedStr} of ${maxStr}`,
|
|
||||||
data: {
|
|
||||||
labels: [
|
|
||||||
"Used",
|
|
||||||
"Available"
|
|
||||||
],
|
|
||||||
datasets: [{
|
|
||||||
label: resourceName,
|
|
||||||
data: [resourceUsed, resourceAvail],
|
|
||||||
backgroundColor: [
|
|
||||||
usedColor,
|
|
||||||
"rgb(140, 140, 140)"
|
|
||||||
],
|
|
||||||
borderWidth: 0,
|
|
||||||
hoverOffset: 4
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
breakpoint: 680
|
|
||||||
};
|
|
||||||
chart.style = "margin: 10px;";
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseNumber (value, unitData) {
|
|
||||||
if (!unitData) {
|
|
||||||
return `${value}`;
|
|
||||||
}
|
|
||||||
const compact = unitData.compact;
|
|
||||||
const multiplier = unitData.multiplier;
|
|
||||||
const base = unitData.base;
|
|
||||||
const unit = unitData.unit;
|
|
||||||
value = multiplier * value;
|
|
||||||
if (value <= 0) {
|
|
||||||
return `0 ${unit}`;
|
|
||||||
}
|
|
||||||
else if (compact) {
|
|
||||||
const exponent = Math.floor(Math.log(value) / Math.log(base));
|
|
||||||
value = value / base ** exponent;
|
|
||||||
const unitPrefix = prefixes[base][exponent];
|
|
||||||
return `${value} ${unitPrefix}${unit}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return `${value} ${unit}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePasswordChangeForm () {
|
function handlePasswordChangeForm () {
|
||||||
const body = `
|
const body = `
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
47
web/templates/resource-chart.tmpl
Normal file
47
web/templates/resource-chart.tmpl
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{{define "resource-chart"}}
|
||||||
|
<resource-chart>
|
||||||
|
<template shadowrootmode="open">
|
||||||
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
|
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
#container{
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
padding: 10px 10px 10px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
progress {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
height: 1em;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
#caption {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="container">
|
||||||
|
<progress value="{{.Used}}" max="{{.Max}}"></progress>
|
||||||
|
<p id="caption">
|
||||||
|
<span>{{.Name}}</span>
|
||||||
|
{{if eq .Type "list"}}
|
||||||
|
<span>{{.Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
||||||
|
{{else}}
|
||||||
|
<span>{{printf "%.2f" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</resource-chart>
|
||||||
|
{{end}}-
|
Reference in New Issue
Block a user