Added some monitoring widgets and scripts
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
+8
-18
@@ -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 (
|
||||
<>
|
||||
<div class="title">{title}</div>
|
||||
<div class="content">
|
||||
<canvas ref={canvasRef}></canvas>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return <canvas ref={canvasRef}></canvas>
|
||||
}
|
||||
@@ -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 • <code>{disk}</code>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="title">
|
||||
<div class="row">
|
||||
<div class="row-group">
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
<div class="row-group">
|
||||
<button
|
||||
class="icon"
|
||||
onClick={() => {
|
||||
refreshOutput()
|
||||
showToast(
|
||||
<ToastMessage icon="info">Aggiornato "{title}"</ToastMessage>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{err ? (
|
||||
<p>Errore "{err}"</p>
|
||||
) : (
|
||||
<PieChart parts={[used]} labels={[byteSizeToString(used)]} total={total} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PieChart } from '../charts/PieChart.jsx'
|
||||
|
||||
export const PieChartWidget = ({ title, ...chart }) => {
|
||||
title = title || 'Grafico a torta'
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="title">{title}</div>
|
||||
<div class="content">
|
||||
<PieChart {...chart} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<div class="title">
|
||||
<div class="row">
|
||||
<div class="row-group">{title}</div>
|
||||
<div class="row-group">
|
||||
<button
|
||||
class="icon"
|
||||
onClick={() => {
|
||||
refreshOutput()
|
||||
showToast(
|
||||
<ToastMessage icon="info">Aggiornato "{title}"</ToastMessage>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{err ? (
|
||||
<p>Errore "{err}"</p>
|
||||
) : (
|
||||
<pre>
|
||||
<code>{output}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
router := &routes.Router{
|
||||
Database: database.NewJSON("database.local.json"),
|
||||
// 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: 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+23
-13
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
+5
-1
@@ -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
|
||||
}
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
DEVICE="$1"
|
||||
|
||||
df | grep "$DEVICE" | tr -s ' ' | cut -d' ' -f 2-3 | tr ' ' '\n'
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo 'active'
|
||||
echo 'active'
|
||||
echo 'active'
|
||||
echo 'failing'
|
||||
echo 'missing'
|
||||
Reference in New Issue
Block a user