diff --git a/_frontend/src/components/Widget.jsx b/_frontend/src/components/Widget.jsx
index 663692c..1df9f8f 100644
--- a/_frontend/src/components/Widget.jsx
+++ b/_frontend/src/components/Widget.jsx
@@ -1,9 +1,13 @@
-import { MessageWidget } from './MessageWidget.jsx'
-import { PieChartWidget } from './PieChartWidget.jsx'
+import { DiskUsage } from './widgets/DiskUsage.jsx'
+import { MessageWidget } from './widgets/MessageWidget.jsx'
+import { PieChartWidget } from './widgets/PieChartWidget.jsx'
+import { ScriptStatus } from './widgets/ScriptStatus.jsx'
const WidgetTypes = {
pie: PieChartWidget,
message: MessageWidget,
+ 'script-status': ScriptStatus,
+ 'disk-usage': DiskUsage,
}
export const Widget = ({ type, value }) => {
diff --git a/_frontend/src/components/PieChartWidget.jsx b/_frontend/src/components/charts/PieChart.jsx
similarity index 82%
rename from _frontend/src/components/PieChartWidget.jsx
rename to _frontend/src/components/charts/PieChart.jsx
index a7a0fb2..5b2d4d9 100644
--- a/_frontend/src/components/PieChartWidget.jsx
+++ b/_frontend/src/components/charts/PieChart.jsx
@@ -1,13 +1,9 @@
import { useEffect, useRef } from 'preact/hooks'
-import { hashCode } from '../util.jsx'
-
-const CANVAS_SIZE = 350
-
-export const PieChartWidget = ({ title, parts, labels, total }) => {
- title = title || 'Grafico a torta'
+import { hashCode } from '../../util.jsx'
+export const PieChart = ({ parts, labels, total }) => {
parts = parts || [1]
- labels = labels || []
+ labels = labels || parts
const canvasRef = useRef()
@@ -33,7 +29,7 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
g.scale(2, 2)
g.translate(width / 2, height / 2)
- g.font = `18px 'Open Sans'`
+ g.font = `16px 'Open Sans'`
g.textAlign = 'center'
g.textBaseline = 'middle'
@@ -62,13 +58,14 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
g.beginPath()
g.moveTo(0, 0)
g.arc(0, 0, width * 0.5 * 0.8, acc - 0.5 * Math.PI, acc + angle - 0.5 * Math.PI)
+ g.lineTo(0, 0)
g.stroke()
g.fillStyle = '#333'
g.fillText(
label,
- Math.cos(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9,
- Math.sin(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9
+ Math.cos(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9125,
+ Math.sin(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9125
)
acc += angle
@@ -76,12 +73,5 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
}
}, [canvasRef, parts])
- return (
- <>
-
{title}
-
-
-
- >
- )
+ return
}
diff --git a/_frontend/src/components/widgets/DiskUsage.jsx b/_frontend/src/components/widgets/DiskUsage.jsx
new file mode 100644
index 0000000..1d3573f
--- /dev/null
+++ b/_frontend/src/components/widgets/DiskUsage.jsx
@@ -0,0 +1,52 @@
+import { byteSizeToString, useRemoteState } from '../../util.jsx'
+import { PieChart } from '../charts/PieChart.jsx'
+import { Icon } from '../Icon.jsx'
+import { ToastMessage, useToasts } from '../Toasts.jsx'
+
+export const DiskUsage = ({ disk }) => {
+ const [showToast] = useToasts()
+ const [output, err, refreshOutput] = useRemoteState(
+ `/api/monitor/status?script=${encodeURIComponent(`disk-usage ${disk}`)}`,
+ '100\n0'
+ )
+
+ const [total, used] = output.split('\n').map(s => parseInt(s))
+
+ const title = (
+ <>
+ Spazio Utilizzato • {disk}
+ >
+ )
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {err ? (
+
Errore "{err}"
+ ) : (
+
+ )}
+
+ >
+ )
+}
diff --git a/_frontend/src/components/MessageWidget.jsx b/_frontend/src/components/widgets/MessageWidget.jsx
similarity index 100%
rename from _frontend/src/components/MessageWidget.jsx
rename to _frontend/src/components/widgets/MessageWidget.jsx
diff --git a/_frontend/src/components/widgets/PieChartWidget.jsx b/_frontend/src/components/widgets/PieChartWidget.jsx
new file mode 100644
index 0000000..f4b9916
--- /dev/null
+++ b/_frontend/src/components/widgets/PieChartWidget.jsx
@@ -0,0 +1,14 @@
+import { PieChart } from '../charts/PieChart.jsx'
+
+export const PieChartWidget = ({ title, ...chart }) => {
+ title = title || 'Grafico a torta'
+
+ return (
+ <>
+ {title}
+
+ >
+ )
+}
diff --git a/_frontend/src/components/widgets/ScriptStatus.jsx b/_frontend/src/components/widgets/ScriptStatus.jsx
new file mode 100644
index 0000000..6bd00c5
--- /dev/null
+++ b/_frontend/src/components/widgets/ScriptStatus.jsx
@@ -0,0 +1,43 @@
+import { useRemoteState } from '../../util.jsx'
+import { Icon } from '../Icon.jsx'
+import { ToastMessage, useToasts } from '../Toasts.jsx'
+
+export const ScriptStatus = ({ title, script }) => {
+ const [showToast] = useToasts()
+ const [output, err, refreshOutput] = useRemoteState(
+ `/api/monitor/status?script=${encodeURIComponent(script)}`,
+ ''
+ )
+
+ return (
+ <>
+
+
+
{title}
+
+
+
+
+
+
+ {err ? (
+
Errore "{err}"
+ ) : (
+
+ {output}
+
+ )}
+
+ >
+ )
+}
diff --git a/_frontend/src/util.jsx b/_frontend/src/util.jsx
index 07f2034..99aa67d 100644
--- a/_frontend/src/util.jsx
+++ b/_frontend/src/util.jsx
@@ -137,3 +137,24 @@ const toCaseMap = {
export function changeCase(from, to, s) {
return toCaseMap[to](fromCaseMap[from](s))
}
+
+//
+// Disk space conversions
+//
+
+const byteUnits = {
+ K: 1024,
+ M: 1024 ** 2,
+ G: 1024 ** 3,
+ T: 1024 ** 4,
+}
+
+export function byteSizeToString(size) {
+ let i = 0
+ while (size >= 450) {
+ size /= 1024
+ i++
+ }
+
+ return `${size.toFixed(2)}${Object.keys(byteUnits)[i]}B`
+}
diff --git a/_frontend/styles/main.scss b/_frontend/styles/main.scss
index 4c7afef..4043935 100644
--- a/_frontend/styles/main.scss
+++ b/_frontend/styles/main.scss
@@ -528,6 +528,10 @@ p {
}
}
+pre {
+ margin: 0;
+}
+
code {
font-size: 95%;
}
@@ -587,7 +591,8 @@ $heading-scale: 1.33;
min-height: 10rem;
- &.pie {
+ &.pie,
+ &.disk-usage {
grid-row: span 2;
& > .content {
diff --git a/jobs/jobs.go b/jobs/jobs.go
deleted file mode 100644
index 976b05a..0000000
--- a/jobs/jobs.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package jobs
-
-import (
- "os"
- "path"
-)
-
-type Config struct {
- ScriptsDir string `json:"scriptsDir"`
-}
-
-type Service struct {
- Config Config
-
- scriptPaths []string
-}
-
-func New(config Config) *Service {
- return &Service{
- Config: config,
- }
-}
-
-func (s *Service) LoadScripts() error {
- entries, err := os.ReadDir(s.Config.ScriptsDir)
- if err != nil {
- return err
- }
-
- s.scriptPaths = []string{}
-
- for _, entry := range entries {
- s.scriptPaths = append(s.scriptPaths,
- path.Join(s.Config.ScriptsDir, entry.Name()),
- )
- }
-
- return nil
-}
diff --git a/main.go b/main.go
index cfc2b31..c92703a 100644
--- a/main.go
+++ b/main.go
@@ -8,6 +8,7 @@ import (
"git.phc.dm.unipi.it/phc/storage/config"
"git.phc.dm.unipi.it/phc/storage/database"
+ "git.phc.dm.unipi.it/phc/storage/monitor"
"git.phc.dm.unipi.it/phc/storage/routes"
"github.com/gofiber/fiber/v2"
@@ -16,18 +17,46 @@ import (
)
func main() {
+ // Setup services
+
+ db := database.NewJSON("database.local.json")
+
+ monitorService := monitor.NewService(&monitor.Config{
+ ScriptsDir: "./scripts",
+ })
+
+ if err := monitorService.LoadScripts(); err != nil {
+ panic(err)
+ }
+
+ // The router wraps all application dependencies and provides "routing" methods
router := &routes.Router{
- Database: database.NewJSON("database.local.json"),
+ Database: db,
+ Monitor: monitorService,
}
+ // We use go-fiber as the HTTP framework
app := fiber.New()
- // Main middlewares
+ // Middlewares
app.Use(logger.New())
app.Use(recover.New())
// Static files
- app.Static("/", "/_frontend/dist")
+ app.Static("/", "./_frontend/dist")
+
+ // Setting up dynamic "single page application" routes (all routes these to the same html file)
+ for _, route := range []string{
+ "/",
+ "/api-keys",
+ "/admin",
+ "/buckets",
+ "/buckets/:bucket",
+ } {
+ app.Get(route, func(c *fiber.Ctx) error {
+ return c.SendFile("./_frontend/dist/index.html")
+ })
+ }
app.Route("/api", router.Api)
diff --git a/monitor/service.go b/monitor/service.go
new file mode 100644
index 0000000..100f187
--- /dev/null
+++ b/monitor/service.go
@@ -0,0 +1,58 @@
+package monitor
+
+import (
+ "bytes"
+ "os"
+ "os/exec"
+ "path"
+ "strings"
+)
+
+type Config struct {
+ ScriptsDir string `json:"scriptsDir"`
+}
+
+type Service struct {
+ Config *Config
+
+ scriptPaths map[string]string
+}
+
+func NewService(config *Config) *Service {
+ return &Service{
+ Config: config,
+ }
+}
+
+func (s *Service) LoadScripts() error {
+ entries, err := os.ReadDir(s.Config.ScriptsDir)
+ if err != nil {
+ return err
+ }
+
+ s.scriptPaths = map[string]string{}
+
+ for _, entry := range entries {
+ newScriptPath := path.Join(s.Config.ScriptsDir, entry.Name())
+ s.scriptPaths[entry.Name()] = newScriptPath
+ }
+
+ return nil
+}
+
+// TODO: Add caching
+func (s *Service) GetLastOutput(command string) (string, error) {
+ args := strings.Fields(command)
+
+ scriptPath := s.scriptPaths[args[0]]
+
+ cmd := exec.Command(scriptPath, args[1:]...)
+ var b bytes.Buffer
+ cmd.Stdout = &b
+
+ if err := cmd.Run(); err != nil {
+ return "", err
+ }
+
+ return b.String(), nil
+}
diff --git a/routes/api.go b/routes/api.go
index f8ec9e6..3e791c9 100644
--- a/routes/api.go
+++ b/routes/api.go
@@ -37,6 +37,13 @@ func (r *Router) Api(api fiber.Router) {
return c.Next()
}
+ //
+ // Setup "/api/monitor" routes
+ //
+ monitorRoute := api.Group("/monitor")
+ monitorRoute.Use(isAdminMiddleware)
+ r.ApiMonitor(monitorRoute)
+
api.Post("/login", func(c *fiber.Ctx) error {
var form struct {
Password string `form:"password"`
@@ -67,22 +74,25 @@ func (r *Router) Api(api fiber.Router) {
return c.JSON("ok")
})
- api.Get("/current-user", func(c *fiber.Ctx) error {
- if _, found := adminSessions[c.Cookies("sid")]; !found {
- return c.JSON("anonymous")
- }
+ api.Get("/current-user",
+ func(c *fiber.Ctx) error {
+ if _, found := adminSessions[c.Cookies("sid")]; !found {
+ return c.JSON("anonymous")
+ }
- return c.JSON("admin")
- })
+ return c.JSON("admin")
+ })
- api.Get("/dashboard-state", func(c *fiber.Ctx) error {
- state, err := r.Database.GetDashboardState()
- if err != nil {
- return err
- }
+ api.Get("/dashboard-state",
+ isAdminMiddleware,
+ func(c *fiber.Ctx) error {
+ state, err := r.Database.GetDashboardState()
+ if err != nil {
+ return err
+ }
- return c.JSON(state)
- })
+ return c.JSON(state)
+ })
api.Post("/dashboard-state",
isAdminMiddleware,
diff --git a/routes/monitor.go b/routes/monitor.go
new file mode 100644
index 0000000..a34ea81
--- /dev/null
+++ b/routes/monitor.go
@@ -0,0 +1,27 @@
+package routes
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func (r *Router) ApiMonitor(api fiber.Router) {
+ // Respond to requests like
+ // - "/api/monitor/status?script=SCRIPT_NAME" where SCRIPT_NAME is the name of a file inside "./scripts"
+ api.Get("/status", func(c *fiber.Ctx) error {
+ if qScript := c.Query("script"); qScript != "" {
+ log.Printf("Script %q", qScript)
+
+ output, err := r.Monitor.GetLastOutput(qScript)
+ if err != nil {
+ return err
+ }
+
+ return c.JSON(output)
+ }
+
+ return fmt.Errorf("no script, device or entity provided")
+ })
+}
diff --git a/routes/router.go b/routes/router.go
index 1fc5443..898aec3 100644
--- a/routes/router.go
+++ b/routes/router.go
@@ -1,7 +1,11 @@
package routes
-import "git.phc.dm.unipi.it/phc/storage/database"
+import (
+ "git.phc.dm.unipi.it/phc/storage/database"
+ "git.phc.dm.unipi.it/phc/storage/monitor"
+)
type Router struct {
Database database.Database
+ Monitor *monitor.Service
}
diff --git a/scripts/disk-usage b/scripts/disk-usage
new file mode 100755
index 0000000..9cfd46b
--- /dev/null
+++ b/scripts/disk-usage
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+DEVICE="$1"
+
+df | grep "$DEVICE" | tr -s ' ' | cut -d' ' -f 2-3 | tr ' ' '\n'
diff --git a/scripts/example-raid-status b/scripts/example-raid-status
new file mode 100755
index 0000000..146c68c
--- /dev/null
+++ b/scripts/example-raid-status
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+echo 'active'
+echo 'active'
+echo 'active'
+echo 'failing'
+echo 'missing'
\ No newline at end of file