Added some monitoring widgets and scripts
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
import { MessageWidget } from './MessageWidget.jsx'
|
import { DiskUsage } from './widgets/DiskUsage.jsx'
|
||||||
import { PieChartWidget } from './PieChartWidget.jsx'
|
import { MessageWidget } from './widgets/MessageWidget.jsx'
|
||||||
|
import { PieChartWidget } from './widgets/PieChartWidget.jsx'
|
||||||
|
import { ScriptStatus } from './widgets/ScriptStatus.jsx'
|
||||||
|
|
||||||
const WidgetTypes = {
|
const WidgetTypes = {
|
||||||
pie: PieChartWidget,
|
pie: PieChartWidget,
|
||||||
message: MessageWidget,
|
message: MessageWidget,
|
||||||
|
'script-status': ScriptStatus,
|
||||||
|
'disk-usage': DiskUsage,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Widget = ({ type, value }) => {
|
export const Widget = ({ type, value }) => {
|
||||||
|
|||||||
+8
-18
@@ -1,13 +1,9 @@
|
|||||||
import { useEffect, useRef } from 'preact/hooks'
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
import { hashCode } from '../util.jsx'
|
import { hashCode } from '../../util.jsx'
|
||||||
|
|
||||||
const CANVAS_SIZE = 350
|
|
||||||
|
|
||||||
export const PieChartWidget = ({ title, parts, labels, total }) => {
|
|
||||||
title = title || 'Grafico a torta'
|
|
||||||
|
|
||||||
|
export const PieChart = ({ parts, labels, total }) => {
|
||||||
parts = parts || [1]
|
parts = parts || [1]
|
||||||
labels = labels || []
|
labels = labels || parts
|
||||||
|
|
||||||
const canvasRef = useRef()
|
const canvasRef = useRef()
|
||||||
|
|
||||||
@@ -33,7 +29,7 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
|
|||||||
g.scale(2, 2)
|
g.scale(2, 2)
|
||||||
g.translate(width / 2, height / 2)
|
g.translate(width / 2, height / 2)
|
||||||
|
|
||||||
g.font = `18px 'Open Sans'`
|
g.font = `16px 'Open Sans'`
|
||||||
g.textAlign = 'center'
|
g.textAlign = 'center'
|
||||||
g.textBaseline = 'middle'
|
g.textBaseline = 'middle'
|
||||||
|
|
||||||
@@ -62,13 +58,14 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
|
|||||||
g.beginPath()
|
g.beginPath()
|
||||||
g.moveTo(0, 0)
|
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.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.stroke()
|
||||||
|
|
||||||
g.fillStyle = '#333'
|
g.fillStyle = '#333'
|
||||||
g.fillText(
|
g.fillText(
|
||||||
label,
|
label,
|
||||||
Math.cos(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.9
|
Math.sin(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9125
|
||||||
)
|
)
|
||||||
|
|
||||||
acc += angle
|
acc += angle
|
||||||
@@ -76,12 +73,5 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
|
|||||||
}
|
}
|
||||||
}, [canvasRef, parts])
|
}, [canvasRef, parts])
|
||||||
|
|
||||||
return (
|
return <canvas ref={canvasRef}></canvas>
|
||||||
<>
|
|
||||||
<div class="title">{title}</div>
|
|
||||||
<div class="content">
|
|
||||||
<canvas ref={canvasRef}></canvas>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -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) {
|
export function changeCase(from, to, s) {
|
||||||
return toCaseMap[to](fromCaseMap[from](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 {
|
code {
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
}
|
}
|
||||||
@@ -587,7 +591,8 @@ $heading-scale: 1.33;
|
|||||||
|
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
|
|
||||||
&.pie {
|
&.pie,
|
||||||
|
&.disk-usage {
|
||||||
grid-row: span 2;
|
grid-row: span 2;
|
||||||
|
|
||||||
& > .content {
|
& > .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/config"
|
||||||
"git.phc.dm.unipi.it/phc/storage/database"
|
"git.phc.dm.unipi.it/phc/storage/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/storage/monitor"
|
||||||
"git.phc.dm.unipi.it/phc/storage/routes"
|
"git.phc.dm.unipi.it/phc/storage/routes"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -16,18 +17,46 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
router := &routes.Router{
|
// Setup services
|
||||||
Database: database.NewJSON("database.local.json"),
|
|
||||||
|
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()
|
app := fiber.New()
|
||||||
|
|
||||||
// Main middlewares
|
// Middlewares
|
||||||
app.Use(logger.New())
|
app.Use(logger.New())
|
||||||
app.Use(recover.New())
|
app.Use(recover.New())
|
||||||
|
|
||||||
// Static files
|
// 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)
|
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()
|
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 {
|
api.Post("/login", func(c *fiber.Ctx) error {
|
||||||
var form struct {
|
var form struct {
|
||||||
Password string `form:"password"`
|
Password string `form:"password"`
|
||||||
@@ -67,22 +74,25 @@ func (r *Router) Api(api fiber.Router) {
|
|||||||
return c.JSON("ok")
|
return c.JSON("ok")
|
||||||
})
|
})
|
||||||
|
|
||||||
api.Get("/current-user", func(c *fiber.Ctx) error {
|
api.Get("/current-user",
|
||||||
if _, found := adminSessions[c.Cookies("sid")]; !found {
|
func(c *fiber.Ctx) error {
|
||||||
return c.JSON("anonymous")
|
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 {
|
api.Get("/dashboard-state",
|
||||||
state, err := r.Database.GetDashboardState()
|
isAdminMiddleware,
|
||||||
if err != nil {
|
func(c *fiber.Ctx) error {
|
||||||
return err
|
state, err := r.Database.GetDashboardState()
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(state)
|
return c.JSON(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.Post("/dashboard-state",
|
api.Post("/dashboard-state",
|
||||||
isAdminMiddleware,
|
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
|
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 {
|
type Router struct {
|
||||||
Database database.Database
|
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