Compare commits
8 commits
d6d0fe9d15
...
bc2df3accf
Author | SHA1 | Date | |
---|---|---|---|
bc2df3accf | |||
51eb9c0cac | |||
191d74076e | |||
2f893e458c | |||
75a3d67402 | |||
92fc0b057e | |||
14a4d7fef4 | |||
1f8546cb37 |
49 changed files with 2631 additions and 154 deletions
32
.air.toml
Normal file
32
.air.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
root = "./src"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./src/."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor"]
|
||||
exclude_file = []
|
||||
exclude_regex = []
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
18
.drone.yml
Normal file
18
.drone.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
kind: pipeline
|
||||
name: default
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: Frontend build
|
||||
image: node:16-alpine
|
||||
commands:
|
||||
- cd ui
|
||||
- npm install -g pnpm
|
||||
- pnpm install
|
||||
- pnpm run build
|
||||
- name: Backend test
|
||||
image: golangci/golangci-lint:latest
|
||||
commands:
|
||||
- go mod download
|
||||
- golangci-lint run --timeout 5m
|
||||
- go test -v ./src/...
|
|
@ -1,17 +1,15 @@
|
|||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 88
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
[*.py]
|
||||
indent_style = space
|
||||
|
||||
[*.{json,md,rst,ini,yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
tmp
|
|
@ -5,4 +5,18 @@ repos:
|
|||
- id: golangci-lint-repo-mod
|
||||
name: GolangCI Lint
|
||||
- id: go-test-repo-mod
|
||||
name: Test
|
||||
name: Backend tests
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: tsc
|
||||
name: tsc
|
||||
entry: tsc
|
||||
language: node
|
||||
files: \.tsx?$
|
||||
args: ["-p", "./ui/tsconfig.json"]
|
||||
additional_dependencies: ["typescript@4.5.2"]
|
||||
pass_filenames: false
|
||||
|
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
15
.prettierrc.json
Normal file
15
.prettierrc.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"bracketSpacing": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.json", "*.md", "*.rst", "*.ini", "*.yml", "*.yaml"],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
|
@ -2,11 +2,20 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch SEBRAUC server",
|
||||
"name": "SEBRAUC server",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/src"
|
||||
},
|
||||
{
|
||||
"name": "UI dev server",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/ui",
|
||||
"runtimeArgs": ["run-script", "dev"],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
|||
![SEBRAUC](ui/src/assets/logo_border.svg)
|
2
go.mod
2
go.mod
|
@ -3,9 +3,7 @@ module code.thetadev.de/TSGRain/SEBRAUC
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
code.thetadev.de/ThetaDev/gotry v0.3.2
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/gofiber/fiber/v2 v2.21.0
|
||||
github.com/gofiber/websocket/v2 v2.0.12
|
||||
github.com/stretchr/testify v1.7.0
|
||||
|
|
7
go.sum
7
go.sum
|
@ -1,15 +1,10 @@
|
|||
code.thetadev.de/ThetaDev/gotry v0.3.2 h1:x5JOBszLbCo4FDe9V8ynHsV6EfvALV7wUqnJ/5vtjbw=
|
||||
code.thetadev.de/ThetaDev/gotry v0.3.2/go.mod h1:lKo6abOTMy5uO25ifG7JsGG3DYZd0XZd0xqa6y41BoU=
|
||||
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
|
||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fasthttp/websocket v1.4.3-rc.9 h1:CWJH0vONrOatdKXZgkgbFKWllijD9aY50C5KfbSDcWk=
|
||||
github.com/fasthttp/websocket v1.4.3-rc.9/go.mod h1:eXL2zqDbexYJxaCw8/PQlm7VcMK6uoGvwbYbTdt4dFo=
|
||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI=
|
||||
github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q=
|
||||
github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ=
|
||||
|
@ -45,8 +40,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
16
openapi.yml
16
openapi.yml
|
@ -45,6 +45,22 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/StatusMessage"
|
||||
|
||||
/reboot:
|
||||
post:
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: "#/components/schemas/StatusMessage"
|
||||
default:
|
||||
description: "Server error"
|
||||
content:
|
||||
"application/json":
|
||||
schema:
|
||||
$ref: "#/components/schemas/StatusMessage"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
RaucStatus:
|
||||
|
|
|
@ -5,40 +5,40 @@
|
|||
<title>Chat Example</title>
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
var conn;
|
||||
var msg = document.getElementById("msg");
|
||||
var log = document.getElementById("log");
|
||||
var conn
|
||||
var msg = document.getElementById("msg")
|
||||
var log = document.getElementById("log")
|
||||
|
||||
function appendLog(item) {
|
||||
var doScroll =
|
||||
log.scrollTop > log.scrollHeight - log.clientHeight - 1;
|
||||
log.appendChild(item);
|
||||
log.scrollTop > log.scrollHeight - log.clientHeight - 1
|
||||
log.appendChild(item)
|
||||
if (doScroll) {
|
||||
log.scrollTop = log.scrollHeight - log.clientHeight;
|
||||
log.scrollTop = log.scrollHeight - log.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
if (window["WebSocket"]) {
|
||||
conn = new WebSocket("ws://" + document.location.host + "/api/ws");
|
||||
conn = new WebSocket("ws://" + document.location.host + "/api/ws")
|
||||
conn.onclose = function (evt) {
|
||||
var item = document.createElement("div");
|
||||
item.innerHTML = "<b>Connection closed.</b>";
|
||||
appendLog(item);
|
||||
};
|
||||
var item = document.createElement("div")
|
||||
item.innerHTML = "<b>Connection closed.</b>"
|
||||
appendLog(item)
|
||||
}
|
||||
conn.onmessage = function (evt) {
|
||||
var messages = evt.data.split("\n");
|
||||
var messages = evt.data.split("\n")
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
var item = document.createElement("div");
|
||||
item.innerText = messages[i];
|
||||
appendLog(item);
|
||||
var item = document.createElement("div")
|
||||
item.innerText = messages[i]
|
||||
appendLog(item)
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
var item = document.createElement("div");
|
||||
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
|
||||
appendLog(item);
|
||||
var item = document.createElement("div")
|
||||
item.innerHTML = "<b>Your browser does not support WebSockets.</b>"
|
||||
appendLog(item)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html {
|
||||
|
|
|
@ -3,8 +3,6 @@ package fixtures
|
|||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.thetadev.de/ThetaDev/gotry/try"
|
||||
)
|
||||
|
||||
func doesFileExist(filepath string) bool {
|
||||
|
@ -13,7 +11,10 @@ func doesFileExist(filepath string) bool {
|
|||
}
|
||||
|
||||
func getProjectRoot() string {
|
||||
p := try.String(os.Getwd())
|
||||
p, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
if doesFileExist(filepath.Join(p, "go.mod")) {
|
||||
|
@ -27,7 +28,10 @@ func getProjectRoot() string {
|
|||
|
||||
func CdProjectRoot() {
|
||||
root := getProjectRoot()
|
||||
try.Check(os.Chdir(root))
|
||||
err := os.Chdir(root)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetTestfilesDir() string {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.thetadev.de/ThetaDev/gotry/try"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -17,7 +16,10 @@ func TestGetProjectRoot(t *testing.T) {
|
|||
|
||||
t.Run("subdir", func(t *testing.T) {
|
||||
root1 := getProjectRoot()
|
||||
try.Check(os.Chdir(filepath.Join(root1, "src/rauc")))
|
||||
err := os.Chdir(filepath.Join(root1, "src/rauc"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
root := getProjectRoot()
|
||||
assert.True(t, doesFileExist(filepath.Join(root, "go.sum")))
|
||||
|
@ -26,7 +28,10 @@ func TestGetProjectRoot(t *testing.T) {
|
|||
|
||||
func TestCdProjectRoot(t *testing.T) {
|
||||
CdProjectRoot()
|
||||
try.Check(os.Chdir("src/rauc"))
|
||||
err := os.Chdir("src/rauc")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
CdProjectRoot()
|
||||
assert.True(t, doesFileExist("go.sum"))
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
@ -18,9 +19,9 @@ var (
|
|||
)
|
||||
|
||||
type Rauc struct {
|
||||
Command string
|
||||
Args []string
|
||||
Broadcast chan string
|
||||
command string
|
||||
args []string
|
||||
broadcast chan string
|
||||
status RaucStatus
|
||||
runningMtx sync.Mutex
|
||||
}
|
||||
|
@ -33,9 +34,23 @@ type RaucStatus struct {
|
|||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
func (r *Rauc) completed() {
|
||||
func NewRauc(command string, args []string, broadcast chan string) *Rauc {
|
||||
r := &Rauc{
|
||||
command: command,
|
||||
args: args,
|
||||
broadcast: broadcast,
|
||||
}
|
||||
|
||||
r.broadcast <- r.GetStatusJson()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Rauc) completed(updateFile string) {
|
||||
r.status.Installing = false
|
||||
r.Broadcast <- r.GetStatusJson()
|
||||
r.broadcast <- r.GetStatusJson()
|
||||
|
||||
_ = os.Remove(updateFile)
|
||||
}
|
||||
|
||||
func (r *Rauc) RunRauc(updateFile string) error {
|
||||
|
@ -58,10 +73,10 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
|||
r.status = RaucStatus{
|
||||
Installing: true,
|
||||
}
|
||||
r.Broadcast <- r.GetStatusJson()
|
||||
r.broadcast <- r.GetStatusJson()
|
||||
|
||||
//nolint:gosec
|
||||
cmd := exec.Command(r.Command, append(r.Args, updateFile)...)
|
||||
cmd := exec.Command(r.command, append(r.args, updateFile)...)
|
||||
|
||||
readPipe, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
@ -91,14 +106,14 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
|||
}
|
||||
|
||||
if hasUpdate {
|
||||
r.Broadcast <- r.GetStatusJson()
|
||||
r.broadcast <- r.GetStatusJson()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
r.completed()
|
||||
r.completed(updateFile)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -107,7 +122,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
|||
if err != nil {
|
||||
fmt.Printf("RAUC failed with %s\n", err)
|
||||
}
|
||||
r.completed()
|
||||
r.completed(updateFile)
|
||||
}()
|
||||
|
||||
return nil
|
||||
|
|
|
@ -25,7 +25,7 @@ func NewHub() *MessageHub {
|
|||
return &MessageHub{
|
||||
clients: make(map[*websocket.Conn]hubClient),
|
||||
register: make(chan *websocket.Conn),
|
||||
Broadcast: make(chan string),
|
||||
Broadcast: make(chan string, 5),
|
||||
unregister: make(chan *websocket.Conn),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,17 @@ package server
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/assets"
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/ui"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
|
@ -18,6 +22,8 @@ type SEBRAUCServer struct {
|
|||
address string
|
||||
raucUpdater *rauc.Rauc
|
||||
hub *MessageHub
|
||||
tmpdir string
|
||||
currentId int
|
||||
}
|
||||
|
||||
type statusMessage struct {
|
||||
|
@ -28,19 +34,21 @@ type statusMessage struct {
|
|||
func NewServer(address string) *SEBRAUCServer {
|
||||
hub := NewHub()
|
||||
|
||||
raucUpdater := &rauc.Rauc{
|
||||
Command: "go",
|
||||
Args: []string{
|
||||
raucUpdater := rauc.NewRauc("go", []string{
|
||||
"run",
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock",
|
||||
},
|
||||
Broadcast: hub.Broadcast,
|
||||
}, hub.Broadcast)
|
||||
|
||||
tmpdir, err := util.NewTmpdir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &SEBRAUCServer{
|
||||
address: address,
|
||||
raucUpdater: raucUpdater,
|
||||
hub: hub,
|
||||
tmpdir: tmpdir,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,10 +57,20 @@ func (srv *SEBRAUCServer) Run() error {
|
|||
AppName: "SEBRAUC",
|
||||
BodyLimit: 1024 * 1024 * 1024,
|
||||
ErrorHandler: errorHandler,
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
|
||||
app.Use(logger.New())
|
||||
|
||||
app.Use(compress.New(compress.Config{
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
return strings.HasPrefix(c.Path(), "/api")
|
||||
},
|
||||
}))
|
||||
|
||||
// just for testing
|
||||
app.Use("/api", cors.New())
|
||||
|
||||
app.Use("/api/ws", func(c *fiber.Ctx) error {
|
||||
// IsWebSocketUpgrade returns true if the client
|
||||
// requested upgrade to the WebSocket protocol.
|
||||
|
@ -64,15 +82,16 @@ func (srv *SEBRAUCServer) Run() error {
|
|||
})
|
||||
|
||||
app.Use("/", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(assets.Assets),
|
||||
PathPrefix: "files",
|
||||
Root: http.FS(ui.Assets),
|
||||
PathPrefix: ui.AssetsDir,
|
||||
MaxAge: 7200,
|
||||
}))
|
||||
|
||||
// ROUTES
|
||||
app.Get("/api/ws", websocket.New(srv.hub.Handler))
|
||||
app.Get("/api/test", srv.controllerTest)
|
||||
app.Post("/api/update", srv.controllerUpdate)
|
||||
app.Get("/api/status", srv.controllerStatus)
|
||||
app.Post("/api/reboot", srv.controllerReboot)
|
||||
|
||||
// Start messaging hub
|
||||
go srv.hub.Run()
|
||||
|
@ -85,12 +104,16 @@ func (srv *SEBRAUCServer) controllerUpdate(c *fiber.Ctx) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.SaveFile(file, "./update.raucb")
|
||||
|
||||
srv.currentId++
|
||||
updateFile := fmt.Sprintf("%s/update_%d.raucb", srv.tmpdir, srv.currentId)
|
||||
|
||||
err = c.SaveFile(file, updateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = srv.raucUpdater.RunRauc("./update.raucb")
|
||||
err = srv.raucUpdater.RunRauc(updateFile)
|
||||
if err == nil {
|
||||
writeStatus(c, true, "Update started")
|
||||
} else if errors.Is(err, util.ErrAlreadyRunning) {
|
||||
|
@ -107,15 +130,10 @@ func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (srv *SEBRAUCServer) controllerTest(c *fiber.Ctx) error {
|
||||
err := srv.raucUpdater.RunRauc("./update.raucb")
|
||||
if err == nil {
|
||||
writeStatus(c, true, "Update started")
|
||||
} else if errors.Is(err, util.ErrAlreadyRunning) {
|
||||
return fiber.NewError(fiber.StatusConflict, "already running")
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error {
|
||||
go util.Reboot(5 * time.Second)
|
||||
|
||||
writeStatus(c, true, "System is rebooting")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -123,7 +141,6 @@ func errorHandler(c *fiber.Ctx, err error) error {
|
|||
// API error handling
|
||||
if strings.HasPrefix(c.Path(), "/api") {
|
||||
writeStatus(c, false, err.Error())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,8 +1,72 @@
|
|||
package util
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tmpdirPrefix = "sebrauc"
|
||||
|
||||
func DoesFileExist(filepath string) bool {
|
||||
_, err := os.Stat(filepath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func CreateDirIfNotExists(dirpath string) error {
|
||||
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
|
||||
createErr := os.MkdirAll(dirpath, 0o777)
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTmpdir() (tmpdir string, err error) {
|
||||
for {
|
||||
bts := make([]byte, 16)
|
||||
_, err = rand.Read(bts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpdir = filepath.Join(os.TempDir(), fmt.Sprintf("%s_%x", tmpdirPrefix, bts))
|
||||
|
||||
if !DoesFileExist(tmpdir) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = CreateDirIfNotExists(tmpdir)
|
||||
return
|
||||
}
|
||||
|
||||
func PurgeTmpdirs() (count int) {
|
||||
dirs, _ := os.ReadDir(os.TempDir())
|
||||
|
||||
for _, de := range dirs {
|
||||
if !de.IsDir() {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(de.Name(), tmpdirPrefix+"_") {
|
||||
err := os.RemoveAll(filepath.Join(os.TempDir(), de.Name()))
|
||||
if err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Reboot(t time.Duration) {
|
||||
time.Sleep(t)
|
||||
/*
|
||||
cmd := exec.Command("shutdown", "-r", "0")
|
||||
_ = cmd.Run()
|
||||
*/
|
||||
fmt.Println("The system would reboot")
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDoesFileExist(t *testing.T) {
|
||||
|
@ -36,3 +39,32 @@ func TestDoesFileExist(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTmpdir(t *testing.T) {
|
||||
PurgeTmpdirs()
|
||||
|
||||
td, err := NewTmpdir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tfile := filepath.Join(td, "test.txt")
|
||||
f, err := os.Create(tfile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString("Hello")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.FileExists(t, tfile)
|
||||
|
||||
assert.Equal(t, 1, PurgeTmpdirs())
|
||||
assert.NoFileExists(t, tfile)
|
||||
}
|
||||
|
|
1
ui/.env.development
Normal file
1
ui/.env.development
Normal file
|
@ -0,0 +1 @@
|
|||
VITE_API_HOST=127.0.0.1:8080
|
5
ui/.gitignore
vendored
Normal file
5
ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
13
ui/index.html
Normal file
13
ui/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SEBRAUC</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
23
ui/package.json
Normal file
23
ui/package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/js": "^6.5.95",
|
||||
"axios": "^0.24.0",
|
||||
"byte-size": "^8.1.0",
|
||||
"preact": "^10.5.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.1.5",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"prettier": "^2.4.1",
|
||||
"sass": "^1.43.4",
|
||||
"typescript": "^4.5.2",
|
||||
"vite": "^2.6.14"
|
||||
}
|
||||
}
|
1222
ui/pnpm-lock.yaml
Normal file
1222
ui/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
3
ui/src/assets/favicon.svg
Normal file
3
ui/src/assets/favicon.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="13mm" height="13mm" version="1.1" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-17.612 -12.829)"><text transform="scale(.26458)" fill="#000000" font-family="sans-serif" font-size="40px" style="line-height:1.25;shape-inside:url(#rect23180);white-space:pre" xml:space="preserve"/><g transform="translate(-.003465 .81595)"><path transform="matrix(.27632 0 0 .27632 9.3797 5.4838)" d="m44.365 23.762 33.173 57.457-66.346-2e-6z" fill="#ff0039"/><g transform="translate(.2726 .27517)" fill="#fff" aria-label="SR"><path d="m19.954 22.779q-0.5715 0-1.0477-0.127-0.46567-0.127-0.79375-0.33866v-1.4182q0.46566 0.26458 0.92075 0.41275 0.45508 0.14817 0.93133 0.14817 0.52916 0 0.87841-0.20108 0.34925-0.20108 0.34925-0.62442 0-0.43392-0.254-0.67733-0.254-0.254-0.65616-0.55033-0.3175-0.23283-0.61383-0.51858-0.29633-0.28575-0.48683-0.66675-0.1905-0.39158-0.1905-0.91016 0-0.67733 0.3175-1.1853 0.3175-0.51858 0.89958-0.81491 0.59266-0.29633 1.3758-0.29633 0.59266 0 1.0901 0.127 0.49742 0.127 0.96308 0.381l-0.56092 1.2065q-0.80433-0.381-1.4922-0.381-0.42333 0-0.6985 0.23283-0.27517 0.22225-0.27517 0.61383 0 0.40216 0.24342 0.64558 0.24342 0.23283 0.68791 0.52916 0.62442 0.41275 0.93133 0.91016 0.3175 0.49742 0.3175 1.1642 0 0.79375-0.39158 1.3123-0.381 0.51858-1.0266 0.77258-0.635 0.254-1.4182 0.254z"/><path d="m23.902 22.755 1.5981-7.5565h1.7145q1.1536 0 1.7568 0.48683t0.60325 1.4711q0 0.84666-0.4445 1.4393-0.43392 0.59266-1.3652 0.86783l1.27 3.2914h-1.2382l-1.0689-3.0586h-1.0583l-0.64558 3.0586zm2.667-4.0111q0.94191 0 1.397-0.41275 0.45508-0.41275 0.45508-1.1007 0-0.58208-0.32808-0.81491-0.32808-0.24342-0.98425-0.24342h-0.68791l-0.55033 2.5717z"/></g></g></g></svg>
|
After Width: | Height: | Size: 1.8 KiB |
3
ui/src/assets/logo.svg
Normal file
3
ui/src/assets/logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="100mm" height="25mm" version="1.1" viewBox="0 0 100 25" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-16.98 -12.577)"><text transform="scale(.26458)" fill="#000000" font-family="sans-serif" font-size="40px" style="line-height:1.25;shape-inside:url(#rect23180);white-space:pre" xml:space="preserve"/><path transform="matrix(.34507 0 0 .34507 17.422 6.9892)" d="m44.365 23.762 33.173 57.457-66.346-2e-6z" fill="#ff0039"/><g transform="matrix(2.2124 0 0 2.2124 -21.795 -15.418)" fill="#fff" aria-label="SEBRAUC"><path d="m19.954 22.897q-0.5715 0-1.0477-0.127-0.46567-0.127-0.79375-0.33866v-1.4182q0.46566 0.26458 0.92075 0.41275 0.45508 0.14817 0.93133 0.14817 0.52916 0 0.87841-0.20108 0.34925-0.20108 0.34925-0.62442 0-0.43392-0.254-0.67733-0.254-0.254-0.65616-0.55033-0.3175-0.23283-0.61383-0.51858-0.29633-0.28575-0.48683-0.66675-0.1905-0.39158-0.1905-0.91016 0-0.67733 0.3175-1.1853 0.3175-0.51858 0.89958-0.81491 0.59266-0.29633 1.3758-0.29633 0.59266 0 1.0901 0.127 0.49742 0.127 0.96308 0.381l-0.56092 1.2065q-0.80433-0.381-1.4922-0.381-0.42333 0-0.6985 0.23283-0.27517 0.22225-0.27517 0.61383 0 0.40216 0.24342 0.64558 0.24342 0.23283 0.68791 0.52916 0.62442 0.41275 0.93133 0.91016 0.3175 0.49742 0.3175 1.1642 0 0.79375-0.39158 1.3123-0.381 0.51858-1.0266 0.77258-0.635 0.254-1.4182 0.254z"/><path d="m23.785 22.792 1.5981-7.5565h4.2227l-0.27517 1.3123h-2.6458l-0.34925 1.6616h2.4659l-0.28575 1.3123h-2.4659l-0.41275 1.9473h2.6458l-0.27516 1.3229z"/><path d="m29.521 22.792 1.5981-7.5565h2.2013q1.0795 0 1.7357 0.39158 0.65616 0.381 0.65616 1.2912 0 0.77258-0.43392 1.27-0.43392 0.49742-1.2171 0.66675v0.04233q0.51858 0.13758 0.8255 0.508 0.30692 0.35983 0.30692 0.92075 0 0.85725-0.39158 1.4076-0.39158 0.53975-1.0689 0.80433-0.66675 0.254-1.5134 0.254zm3.3126-4.5614q0.56092 0 0.89958-0.22225 0.34925-0.23283 0.34925-0.75142 0-0.70908-0.87841-0.70908h-0.78316l-0.35983 1.6827zm-0.52916 3.2385q0.58208 0 0.93133-0.28575 0.35983-0.29633 0.35983-0.84666 0-0.40217-0.23283-0.61383-0.23283-0.22225-0.70908-0.22225h-0.85725l-0.42333 1.9685z"/><g><path d="m36.199 22.792 1.5981-7.5565h1.7145q1.1536 0 1.7568 0.48683t0.60325 1.4711q0 0.84666-0.4445 1.4393-0.43392 0.59266-1.3652 0.86783l1.27 3.2914h-1.2382l-1.0689-3.0586h-1.0583l-0.64558 3.0586zm2.667-4.0111q0.94191 0 1.397-0.41275t0.45508-1.1007q0-0.58208-0.32808-0.81491-0.32808-0.24342-0.98425-0.24342h-0.68792l-0.55033 2.5717z"/><path d="m41.853 22.792 4.064-7.5565h1.27l0.87841 7.5565h-1.1112l-0.23283-2.1907h-2.5188l-1.1324 2.1907zm2.8575-3.1856h1.9156l-0.15875-1.6933q-0.03175-0.37042-0.0635-0.80433t-0.04233-0.79375h-0.02117q-0.15875 0.37042-0.34925 0.77258-0.1905 0.40216-0.41275 0.83608z"/><path d="m51.302 22.897q-1.1853 0-1.778-0.55033-0.58208-0.56092-0.58208-1.5557 0-0.17992 0.02117-0.42333 0.03175-0.24342 0.09525-0.508l0.98425-4.6249h1.1218l-0.98425 4.6567q-0.05292 0.22225-0.08467 0.46566-0.02117 0.23283-0.02117 0.39158 0 0.55033 0.32808 0.85725 0.32808 0.30692 0.97366 0.30692 0.86783 0 1.2912-0.45508 0.43392-0.46566 0.62441-1.3652l1.0372-4.8577h1.1324l-1.0583 4.953q-0.16933 0.8255-0.53975 1.4393-0.37042 0.60325-0.99483 0.94191-0.62441 0.32808-1.5663 0.32808z"/><path d="m58.784 22.897q-1.3017 0-2.0108-0.78316t-0.70908-2.1272q0-0.67733 0.15875-1.3652 0.15875-0.6985 0.47625-1.3229 0.32808-0.62442 0.80433-1.1112 0.48683-0.49742 1.143-0.77258 0.66675-0.28575 1.4922-0.28575 0.59266 0 1.0583 0.11642 0.47625 0.10583 0.89958 0.33867l-0.45508 0.93133q-0.30692-0.16933-0.66675-0.28575-0.35983-0.11642-0.83608-0.11642-0.74083 0-1.2912 0.33867-0.53975 0.33866-0.89958 0.91016-0.34925 0.56092-0.52916 1.2382-0.16933 0.67733-0.16933 1.3441 0 0.93133 0.4445 1.4499 0.45508 0.51858 1.3017 0.51858 0.43392 0 0.85725-0.09525t0.83608-0.24342v0.98425q-0.381 0.13758-0.83608 0.23283-0.45508 0.10583-1.0689 0.10583z"/></g></g></g></svg>
|
After Width: | Height: | Size: 3.8 KiB |
3
ui/src/assets/logo_border.svg
Normal file
3
ui/src/assets/logo_border.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg width="100mm" height="25mm" version="1.1" viewBox="0 0 100 25" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-16.98 -12.577)"><text transform="scale(.26458)" fill="#000000" font-family="sans-serif" font-size="40px" style="line-height:1.25;shape-inside:url(#rect23180);white-space:pre" xml:space="preserve"/><path transform="matrix(.34507 0 0 .34507 17.422 6.9892)" d="m44.365 23.762 33.173 57.457-66.346-2e-6z" fill="#ff0039" stroke="#000" stroke-width="2.3184"/><g transform="matrix(2.2124 0 0 2.2124 -21.795 -15.418)" fill="#fff" stroke="#000" stroke-width=".36159" aria-label="SEBRAUC"><g stroke-width=".36159"><path d="m19.954 22.897q-0.5715 0-1.0477-0.127-0.46567-0.127-0.79375-0.33866v-1.4182q0.46566 0.26458 0.92075 0.41275 0.45508 0.14817 0.93133 0.14817 0.52916 0 0.87841-0.20108 0.34925-0.20108 0.34925-0.62442 0-0.43392-0.254-0.67733-0.254-0.254-0.65616-0.55033-0.3175-0.23283-0.61383-0.51858-0.29633-0.28575-0.48683-0.66675-0.1905-0.39158-0.1905-0.91016 0-0.67733 0.3175-1.1853 0.3175-0.51858 0.89958-0.81491 0.59266-0.29633 1.3758-0.29633 0.59266 0 1.0901 0.127 0.49742 0.127 0.96308 0.381l-0.56092 1.2065q-0.80433-0.381-1.4922-0.381-0.42333 0-0.6985 0.23283-0.27517 0.22225-0.27517 0.61383 0 0.40216 0.24342 0.64558 0.24342 0.23283 0.68791 0.52916 0.62442 0.41275 0.93133 0.91016 0.3175 0.49742 0.3175 1.1642 0 0.79375-0.39158 1.3123-0.381 0.51858-1.0266 0.77258-0.635 0.254-1.4182 0.254z"/><path d="m23.785 22.792 1.5981-7.5565h4.2227l-0.27517 1.3123h-2.6458l-0.34925 1.6616h2.4659l-0.28575 1.3123h-2.4659l-0.41275 1.9473h2.6458l-0.27516 1.3229z"/><path d="m29.521 22.792 1.5981-7.5565h2.2013q1.0795 0 1.7357 0.39158 0.65616 0.381 0.65616 1.2912 0 0.77258-0.43392 1.27-0.43392 0.49742-1.2171 0.66675v0.04233q0.51858 0.13758 0.8255 0.508 0.30692 0.35983 0.30692 0.92075 0 0.85725-0.39158 1.4076-0.39158 0.53975-1.0689 0.80433-0.66675 0.254-1.5134 0.254zm3.3126-4.5614q0.56092 0 0.89958-0.22225 0.34925-0.23283 0.34925-0.75142 0-0.70908-0.87841-0.70908h-0.78316l-0.35983 1.6827zm-0.52916 3.2385q0.58208 0 0.93133-0.28575 0.35983-0.29633 0.35983-0.84666 0-0.40217-0.23283-0.61383-0.23283-0.22225-0.70908-0.22225h-0.85725l-0.42333 1.9685z"/></g><g><path d="m36.199 22.792 1.5981-7.5565h1.7145q1.1536 0 1.7568 0.48683t0.60325 1.4711q0 0.84666-0.4445 1.4393-0.43392 0.59266-1.3652 0.86783l1.27 3.2914h-1.2382l-1.0689-3.0586h-1.0583l-0.64558 3.0586zm2.667-4.0111q0.94191 0 1.397-0.41275t0.45508-1.1007q0-0.58208-0.32808-0.81491-0.32808-0.24342-0.98425-0.24342h-0.68792l-0.55033 2.5717z" stroke-width=".27119"/><path d="m41.853 22.792 4.064-7.5565h1.27l0.87841 7.5565h-1.1112l-0.23283-2.1907h-2.5188l-1.1324 2.1907zm2.8575-3.1856h1.9156l-0.15875-1.6933q-0.03175-0.37042-0.0635-0.80433t-0.04233-0.79375h-0.02117q-0.15875 0.37042-0.34925 0.77258-0.1905 0.40216-0.41275 0.83608z" stroke-width=".27119"/><path d="m51.302 22.897q-1.1853 0-1.778-0.55033-0.58208-0.56092-0.58208-1.5557 0-0.17992 0.02117-0.42333 0.03175-0.24342 0.09525-0.508l0.98425-4.6249h1.1218l-0.98425 4.6567q-0.05292 0.22225-0.08467 0.46566-0.02117 0.23283-0.02117 0.39158 0 0.55033 0.32808 0.85725 0.32808 0.30692 0.97366 0.30692 0.86783 0 1.2912-0.45508 0.43392-0.46566 0.62441-1.3652l1.0372-4.8577h1.1324l-1.0583 4.953q-0.16933 0.8255-0.53975 1.4393-0.37042 0.60325-0.99483 0.94191-0.62441 0.32808-1.5663 0.32808z" stroke-width=".27119"/><path d="m58.784 22.897q-1.3017 0-2.0108-0.78316t-0.70908-2.1272q0-0.67733 0.15875-1.3652 0.15875-0.6985 0.47625-1.3229 0.32808-0.62442 0.80433-1.1112 0.48683-0.49742 1.143-0.77258 0.66675-0.28575 1.4922-0.28575 0.59266 0 1.0583 0.11642 0.47625 0.10583 0.89958 0.33867l-0.45508 0.93133q-0.30692-0.16933-0.66675-0.28575-0.35983-0.11642-0.83608-0.11642-0.74083 0-1.2912 0.33867-0.53975 0.33866-0.89958 0.91016-0.34925 0.56092-0.52916 1.2382-0.16933 0.67733-0.16933 1.3441 0 0.93133 0.4445 1.4499 0.45508 0.51858 1.3017 0.51858 0.43392 0 0.85725-0.09525t0.83608-0.24342v0.98425q-0.381 0.13758-0.83608 0.23283-0.45508 0.10583-1.0689 0.10583z" stroke-width=".27119"/></g></g></g></svg>
|
After Width: | Height: | Size: 4 KiB |
95
ui/src/assets/logo_square_src.svg
Normal file
95
ui/src/assets/logo_square_src.svg
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="13mm"
|
||||
height="13mm"
|
||||
viewBox="0 0 13 13"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="logo_square_src.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
fit-margin-top="1"
|
||||
fit-margin-left="0.5"
|
||||
fit-margin-right="0.5"
|
||||
fit-margin-bottom="1"
|
||||
lock-margins="false"
|
||||
inkscape:zoom="7.6196617"
|
||||
inkscape:cx="-0.45933798"
|
||||
inkscape:cy="16.208069"
|
||||
inkscape:window-width="2516"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g24264" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<rect
|
||||
x="62.302925"
|
||||
y="21.613937"
|
||||
width="198.05888"
|
||||
height="77.345375"
|
||||
id="rect23180" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-17.612457,-12.829341)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text23178"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect23180);fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<g
|
||||
id="g24264"
|
||||
transform="translate(-0.00346504,0.81595258)">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ff0039;fill-opacity:1;stroke:none;stroke-width:3.20575;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30063"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="44.36451"
|
||||
sodipodi:cy="62.066853"
|
||||
sodipodi:r1="38.304989"
|
||||
sodipodi:r2="19.152494"
|
||||
sodipodi:arg1="-1.5707963"
|
||||
sodipodi:arg2="-0.52359878"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 44.364511,23.761864 33.173092,57.457484 -66.346187,-2e-6 z"
|
||||
transform="matrix(0.27469225,0,0,0.27469225,9.4905394,5.4921201)"
|
||||
inkscape:transform-center-y="-2.6305206" />
|
||||
<g
|
||||
aria-label="SR"
|
||||
id="text4148"
|
||||
style="font-size:10.5833px;line-height:1.25;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="translate(0.27260454,0.27516687)">
|
||||
<path
|
||||
d="m 19.953951,22.778534 q -0.571498,0 -1.047746,-0.127 -0.465666,-0.126999 -0.793748,-0.338665 v -1.418162 q 0.465665,0.264582 0.920747,0.412748 0.455082,0.148167 0.931331,0.148167 0.529165,0 0.878413,-0.201083 0.349249,-0.201083 0.349249,-0.624415 0,-0.433915 -0.253999,-0.677331 -0.253999,-0.253999 -0.656164,-0.550331 -0.317499,-0.232833 -0.613832,-0.518582 -0.296332,-0.285749 -0.486832,-0.666748 -0.190499,-0.391582 -0.190499,-0.910164 0,-0.677331 0.317499,-1.185329 0.317499,-0.518582 0.89958,-0.814914 0.592665,-0.296333 1.375829,-0.296333 0.592665,0 1.09008,0.127 0.497415,0.126999 0.963081,0.380999 l -0.560915,1.206496 q -0.804331,-0.380999 -1.492246,-0.380999 -0.423332,0 -0.698497,0.232832 -0.275166,0.22225 -0.275166,0.613832 0,0.402165 0.243416,0.645581 0.243416,0.232833 0.687914,0.529165 0.624415,0.412749 0.931331,0.910164 0.317499,0.497415 0.317499,1.164163 0,0.793747 -0.391582,1.312329 -0.380999,0.518582 -1.026581,0.772581 -0.634998,0.253999 -1.418162,0.253999 z"
|
||||
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path55900" />
|
||||
<path
|
||||
d="m 23.901529,22.755374 1.598079,-7.556476 h 1.714494 q 1.15358,0 1.756828,0.486832 0.603248,0.486832 0.603248,1.471079 0,0.846664 -0.444499,1.439328 -0.433915,0.592665 -1.365245,0.867831 l 1.269996,3.291406 H 27.796184 L 26.72727,19.6968 h -1.05833 l -0.645581,3.058574 z m 2.666992,-4.01107 q 0.941914,0 1.396995,-0.412749 0.455082,-0.412749 0.455082,-1.100663 0,-0.582082 -0.328082,-0.814914 -0.328082,-0.243416 -0.984247,-0.243416 h -0.687914 l -0.550332,2.571742 z"
|
||||
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path55902" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
111
ui/src/assets/logo_src.svg
Normal file
111
ui/src/assets/logo_src.svg
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100mm"
|
||||
height="25mm"
|
||||
viewBox="0 0 100 25.000001"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="logo_src.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
fit-margin-top="1"
|
||||
fit-margin-left="1"
|
||||
fit-margin-right="1"
|
||||
fit-margin-bottom="1"
|
||||
lock-margins="true"
|
||||
inkscape:zoom="2.9238518"
|
||||
inkscape:cx="147.23729"
|
||||
inkscape:cy="11.115475"
|
||||
inkscape:window-width="2516"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="text4148" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<rect
|
||||
x="62.302925"
|
||||
y="21.613937"
|
||||
width="198.05888"
|
||||
height="77.345375"
|
||||
id="rect23180" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-16.979957,-12.577341)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text23178"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect23180);fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ff0039;fill-opacity:1;stroke:#000000;stroke-width:2.31835097;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30063"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="44.36451"
|
||||
sodipodi:cy="62.066853"
|
||||
sodipodi:r1="38.304989"
|
||||
sodipodi:r2="19.152494"
|
||||
sodipodi:arg1="-1.5707963"
|
||||
sodipodi:arg2="-0.52359878"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 44.364511,23.761864 33.173092,57.457484 -66.346187,-2e-6 z"
|
||||
transform="matrix(0.34507286,0,0,0.34507286,17.421609,6.9891521)"
|
||||
inkscape:transform-center-y="-3.3045026" />
|
||||
<g
|
||||
aria-label="SEBRAUC"
|
||||
id="text4148"
|
||||
style="font-size:10.5833px;line-height:1.25;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(2.212436,0,0,2.212436,-21.795233,-15.418287)">
|
||||
<path
|
||||
d="m 19.953951,22.897436 q -0.571498,0 -1.047746,-0.127 -0.465666,-0.126999 -0.793748,-0.338665 v -1.418162 q 0.465665,0.264582 0.920747,0.412748 0.455082,0.148167 0.931331,0.148167 0.529165,0 0.878413,-0.201083 0.349249,-0.201083 0.349249,-0.624415 0,-0.433915 -0.253999,-0.677331 -0.253999,-0.253999 -0.656164,-0.550331 -0.317499,-0.232833 -0.613832,-0.518582 -0.296332,-0.285749 -0.486832,-0.666748 -0.190499,-0.391582 -0.190499,-0.910164 0,-0.677331 0.317499,-1.185329 0.317499,-0.518582 0.89958,-0.814914 0.592665,-0.296333 1.375829,-0.296333 0.592665,0 1.09008,0.127 0.497415,0.126999 0.963081,0.380999 l -0.560915,1.206496 q -0.804331,-0.380999 -1.492246,-0.380999 -0.423332,0 -0.698497,0.232832 -0.275166,0.22225 -0.275166,0.613832 0,0.402165 0.243416,0.645581 0.243416,0.232833 0.687914,0.529165 0.624415,0.412749 0.931331,0.910164 0.317499,0.497415 0.317499,1.164163 0,0.793747 -0.391582,1.312329 -0.380999,0.518582 -1.026581,0.772581 -0.634998,0.253999 -1.418162,0.253999 z"
|
||||
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;stroke:#000000;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path52706" />
|
||||
<path
|
||||
d="m 23.785113,22.791603 1.598078,-7.556476 h 4.222737 l -0.275166,1.312329 h -2.645825 l -0.349249,1.661578 h 2.465909 l -0.285749,1.31233 h -2.465909 l -0.412748,1.947327 h 2.645824 l -0.275165,1.322912 z"
|
||||
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;stroke:#000000;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path52708" />
|
||||
<path
|
||||
d="m 29.52126,22.791603 1.598078,-7.556476 h 2.201326 q 1.079497,0 1.735661,0.391582 0.656165,0.380999 0.656165,1.291163 0,0.772581 -0.433915,1.269996 -0.433916,0.497415 -1.21708,0.666748 v 0.04233 q 0.518582,0.137583 0.825497,0.507998 0.306916,0.359832 0.306916,0.920747 0,0.857248 -0.391582,1.407579 -0.391582,0.539748 -1.068913,0.804331 -0.666748,0.253999 -1.513412,0.253999 z m 3.312572,-4.561402 q 0.560915,0 0.899581,-0.222249 0.349249,-0.232833 0.349249,-0.751415 0,-0.709081 -0.878414,-0.709081 h -0.783164 l -0.359833,1.682745 z m -0.529165,3.23849 q 0.582082,0 0.931331,-0.285749 0.359832,-0.296333 0.359832,-0.846664 0,-0.402166 -0.232833,-0.613832 -0.232832,-0.222249 -0.709081,-0.222249 h -0.857247 l -0.423332,1.968494 z"
|
||||
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;stroke:#000000;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path52710" />
|
||||
<path
|
||||
d="m 36.199331,22.791603 1.598078,-7.556476 h 1.714495 q 1.153579,0 1.756827,0.486832 0.603248,0.486832 0.603248,1.471079 0,0.846664 -0.444498,1.439328 -0.433915,0.592665 -1.365246,0.867831 l 1.269996,3.291406 h -1.238246 l -1.068913,-3.058574 h -1.05833 l -0.645581,3.058574 z m 2.666991,-4.01107 q 0.941914,0 1.396996,-0.412749 0.455082,-0.412749 0.455082,-1.100663 0,-0.582082 -0.328082,-0.814914 -0.328083,-0.243416 -0.984247,-0.243416 h -0.687915 l -0.550331,2.571742 z"
|
||||
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:0.27119429;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path52712" />
|
||||
<path
|
||||
d="m 41.852916,22.791603 4.063988,-7.556476 H 47.1869 l 0.878413,7.556476 H 46.954067 L 46.721234,20.60086 h -2.518825 l -1.132413,2.190743 z m 2.857491,-3.185573 h 1.915578 l -0.15875,-1.693328 q -0.03175,-0.370416 -0.0635,-0.804331 -0.03175,-0.433915 -0.04233,-0.793747 h -0.02117 q -0.15875,0.370415 -0.349249,0.772581 -0.1905,0.402165 -0.412749,0.83608 z"
|
||||
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.271194;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path52714" />
|
||||
<path
|
||||
d="m 51.301688,22.897436 q -1.18533,0 -1.777994,-0.550331 -0.582082,-0.560915 -0.582082,-1.555746 0,-0.179916 0.02117,-0.423332 0.03175,-0.243415 0.09525,-0.507998 l 0.984247,-4.624902 h 1.12183 l -0.984247,4.656652 q -0.05292,0.222249 -0.08467,0.465665 -0.02117,0.232833 -0.02117,0.391582 0,0.550332 0.328083,0.857248 0.328082,0.306915 0.973663,0.306915 0.867831,0 1.291163,-0.455082 0.433915,-0.465665 0.624414,-1.365245 l 1.037164,-4.857735 h 1.132413 l -1.05833,4.952984 q -0.169333,0.825498 -0.539748,1.439329 -0.370416,0.603248 -0.994831,0.941914 -0.624414,0.328082 -1.566328,0.328082 z"
|
||||
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:0.27119429;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path52716" />
|
||||
<path
|
||||
d="m 58.784078,22.897436 q -1.301746,0 -2.010827,-0.783164 -0.709081,-0.783164 -0.709081,-2.127243 0,-0.677331 0.158749,-1.365246 0.15875,-0.698498 0.476249,-1.322912 0.328082,-0.624415 0.80433,-1.111247 0.486832,-0.497415 1.142997,-0.772581 0.666748,-0.285749 1.492245,-0.285749 0.592665,0 1.05833,0.116416 0.476248,0.105833 0.89958,0.338666 l -0.455081,0.93133 q -0.306916,-0.169332 -0.666748,-0.285749 -0.359832,-0.116416 -0.836081,-0.116416 -0.740831,0 -1.291163,0.338666 -0.539748,0.338665 -0.89958,0.910163 -0.349249,0.560915 -0.529165,1.238246 -0.169333,0.677332 -0.169333,1.344079 0,0.931331 0.444499,1.449913 0.455082,0.518581 1.301746,0.518581 0.433915,0 0.857247,-0.09525 0.423332,-0.09525 0.836081,-0.243416 v 0.984246 q -0.380999,0.137583 -0.836081,0.232833 -0.455082,0.105833 -1.068913,0.105834 z"
|
||||
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:0.27119429;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="path52718" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.6 KiB |
9
ui/src/components/Dropzone/Dropzone.scss
Normal file
9
ui/src/components/Dropzone/Dropzone.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.dropzone {
|
||||
&.highlight {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
.fileholder {
|
||||
display: none;
|
||||
}
|
||||
}
|
119
ui/src/components/Dropzone/Dropzone.tsx
Normal file
119
ui/src/components/Dropzone/Dropzone.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import {Component, createRef, JSX, ComponentChild, ComponentChildren} from "preact"
|
||||
import "./Dropzone.scss"
|
||||
|
||||
type Props = {
|
||||
disabled: boolean
|
||||
clickable: boolean
|
||||
multiple: boolean
|
||||
accept?: string
|
||||
onFilesAdded?: (files: File[]) => void
|
||||
|
||||
children?: ComponentChild | ComponentChildren
|
||||
}
|
||||
|
||||
type State = {
|
||||
highlight: boolean
|
||||
}
|
||||
|
||||
export default class Dropzone extends Component<Props, State> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
clickable: false,
|
||||
multiple: false,
|
||||
}
|
||||
|
||||
private fileInputRef = createRef<HTMLInputElement>()
|
||||
|
||||
openFileDialog() {
|
||||
this.fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
reset() {
|
||||
const input = this.fileInputRef.current
|
||||
if (input === null) return
|
||||
input.value = ""
|
||||
}
|
||||
|
||||
private onClick = () => {
|
||||
if (this.props.disabled || !this.props.clickable) return
|
||||
this.openFileDialog()
|
||||
}
|
||||
|
||||
private onFilesAdded = (evt: JSX.TargetedEvent) => {
|
||||
if (this.props.disabled || evt.target === null) return
|
||||
|
||||
const input = evt.target as HTMLInputElement
|
||||
const files = input.files
|
||||
|
||||
if (this.props.onFilesAdded) {
|
||||
const array = this.fileListToArray(files)
|
||||
this.props.onFilesAdded(array)
|
||||
}
|
||||
}
|
||||
|
||||
private onDragOver = (evt: DragEvent) => {
|
||||
evt.preventDefault()
|
||||
|
||||
if (this.props.disabled) return
|
||||
|
||||
this.setState({highlight: true})
|
||||
}
|
||||
|
||||
private onDragLeave = () => {
|
||||
this.setState({highlight: false})
|
||||
}
|
||||
|
||||
private onDrop = (evt: DragEvent) => {
|
||||
evt.preventDefault()
|
||||
this.setState({highlight: false})
|
||||
|
||||
if (this.props.disabled || evt.dataTransfer === null) return
|
||||
|
||||
const files = evt.dataTransfer.files
|
||||
|
||||
if (!this.props.multiple && files.length > 1) return
|
||||
|
||||
if (this.props.onFilesAdded) {
|
||||
const array = this.fileListToArray(files)
|
||||
this.props.onFilesAdded(array)
|
||||
}
|
||||
}
|
||||
|
||||
private fileListToArray(list: FileList | null): File[] {
|
||||
const array: File[] = []
|
||||
if (list === null) return array
|
||||
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
array.push(list.item(i)!)
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
class={`dropzone ${this.state.highlight ? "highlight" : ""}`}
|
||||
onDragOver={this.onDragOver}
|
||||
onDragLeave={this.onDragLeave}
|
||||
onDrop={this.onDrop}
|
||||
onClick={this.onClick}
|
||||
style={{
|
||||
cursor:
|
||||
!this.props.disabled && this.props.clickable
|
||||
? "pointer"
|
||||
: "default",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={this.fileInputRef}
|
||||
class="fileholder"
|
||||
type="file"
|
||||
multiple={this.props.multiple}
|
||||
accept={this.props.accept}
|
||||
onInput={this.onFilesAdded}
|
||||
/>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
30
ui/src/components/Icon/Icon.scss
Normal file
30
ui/src/components/Icon/Icon.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
.icon {
|
||||
vertical-align: sub;
|
||||
|
||||
> svg {
|
||||
color: inherit;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
|
||||
background: #1f85de;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #555;
|
||||
}
|
||||
}
|
29
ui/src/components/Icon/Icon.tsx
Normal file
29
ui/src/components/Icon/Icon.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {Component} from "preact"
|
||||
import "./Icon.scss"
|
||||
|
||||
type Props = {
|
||||
icon: string
|
||||
size: number
|
||||
color?: string
|
||||
}
|
||||
|
||||
export default class Icon extends Component<Props> {
|
||||
static defaultProps = {
|
||||
size: 24,
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span class="icon" style={{color: this.props.color}}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={`height: ${this.props.size}px; width: ${this.props.size}px;`}
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d={this.props.icon} />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
46
ui/src/components/ProgressCircle/ProgressCircle.scss
Normal file
46
ui/src/components/ProgressCircle/ProgressCircle.scss
Normal file
|
@ -0,0 +1,46 @@
|
|||
.progress-box {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
.progress-path {
|
||||
transition: stroke-dasharray 0.5s;
|
||||
}
|
||||
|
||||
circle {
|
||||
transition: fill 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
|
||||
color: white;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #555;
|
||||
}
|
||||
}
|
||||
}
|
60
ui/src/components/ProgressCircle/ProgressCircle.tsx
Normal file
60
ui/src/components/ProgressCircle/ProgressCircle.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import {Component, ComponentChild, ComponentChildren} from "preact"
|
||||
import "./ProgressCircle.scss"
|
||||
|
||||
type Props = {
|
||||
ready: boolean
|
||||
progress: number
|
||||
color: string
|
||||
|
||||
children?: ComponentChild | ComponentChildren
|
||||
}
|
||||
|
||||
export default class ProgressCircle extends Component<Props> {
|
||||
static defaultProps = {
|
||||
ready: false,
|
||||
progress: 0,
|
||||
color: "#FDB900",
|
||||
}
|
||||
|
||||
render() {
|
||||
const percentage = this.props.ready ? 0 : this.props.progress
|
||||
const visible = !this.props.ready && this.props.progress > 0
|
||||
|
||||
return (
|
||||
<div class="progress-box">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill={this.props.color} />
|
||||
|
||||
{visible ? (
|
||||
<path
|
||||
class="progress-path"
|
||||
stroke-linecap="round"
|
||||
stroke-width="5"
|
||||
stroke="#fff"
|
||||
fill="none"
|
||||
d="M50 10
|
||||
a 40 40 0 0 1 0 80
|
||||
a 40 40 0 0 1 0 -80"
|
||||
stroke-dasharray={`${percentage * 2.512}, 251.2`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{visible ? (
|
||||
<text
|
||||
id="count"
|
||||
x="50"
|
||||
y="50"
|
||||
text-anchor="middle"
|
||||
dy="7"
|
||||
font-size="20"
|
||||
fill="#fff"
|
||||
>
|
||||
{percentage}%
|
||||
</text>
|
||||
) : null}
|
||||
</svg>
|
||||
{visible ? null : this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
29
ui/src/components/Upload/Alert.tsx
Normal file
29
ui/src/components/Upload/Alert.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {Component} from "preact"
|
||||
import {mdiTriangleOutline} from "@mdi/js"
|
||||
import Icon from "../Icon/Icon"
|
||||
|
||||
type Props = {
|
||||
source?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class Alert extends Component<Props> {
|
||||
private stripMessage(message: string): string {
|
||||
return message.replace(/^error:/i, "").trim()
|
||||
}
|
||||
|
||||
render() {
|
||||
let msg = ""
|
||||
if (this.props.source !== undefined) msg += `${this.props.source} error: `
|
||||
msg += this.stripMessage(this.props.message)
|
||||
|
||||
return (
|
||||
<div class="alert">
|
||||
<span>
|
||||
<Icon icon={mdiTriangleOutline} color="#FF0039" />
|
||||
{msg}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
51
ui/src/components/Upload/Reboot.tsx
Normal file
51
ui/src/components/Upload/Reboot.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js"
|
||||
import axios, {AxiosError, AxiosResponse} from "axios"
|
||||
import {Component} from "preact"
|
||||
import {apiUrl} from "../../util/apiUrls"
|
||||
import Icon from "../Icon/Icon"
|
||||
|
||||
export default class Reboot extends Component {
|
||||
private triggerReboot = () => {
|
||||
const res = confirm("Reboot the system?")
|
||||
if (!res) return
|
||||
|
||||
axios
|
||||
.post(apiUrl + "/reboot")
|
||||
.then((response: AxiosResponse) => {
|
||||
const msg = response.data.msg
|
||||
|
||||
if (msg !== undefined) {
|
||||
alert(msg)
|
||||
} else {
|
||||
alert("No response")
|
||||
}
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
if (error.response) {
|
||||
const msg = error.response.data.msg
|
||||
|
||||
if (msg !== undefined) {
|
||||
alert("Error: " + msg)
|
||||
} else {
|
||||
alert("Error: no response")
|
||||
}
|
||||
} else {
|
||||
alert(String(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="alert">
|
||||
<span>
|
||||
<Icon icon={mdiCheckCircleOutline} color="#148420" />
|
||||
Reboot the system to apply the update
|
||||
</span>
|
||||
<button class="iconButton" onClick={this.triggerReboot}>
|
||||
<Icon icon={mdiRestore} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
44
ui/src/components/Upload/Updater.scss
Normal file
44
ui/src/components/Upload/Updater.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
.uploader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 0 auto;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding: 15px 8px;
|
||||
margin: 8px 0;
|
||||
|
||||
text-align: center;
|
||||
|
||||
border: 3px solid #fff;
|
||||
border-radius: 16px;
|
||||
|
||||
.top {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0.6em 0;
|
||||
border-radius: 16px;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
}
|
||||
}
|
248
ui/src/components/Upload/Updater.tsx
Normal file
248
ui/src/components/Upload/Updater.tsx
Normal file
|
@ -0,0 +1,248 @@
|
|||
import {Component, createRef} from "preact"
|
||||
import {mdiUpload} from "@mdi/js"
|
||||
import byteSize from "byte-size"
|
||||
import Dropzone from "../Dropzone/Dropzone"
|
||||
import ProgressCircle from "../ProgressCircle/ProgressCircle"
|
||||
import Icon from "../Icon/Icon"
|
||||
import "./Updater.scss"
|
||||
import axios from "axios"
|
||||
import Alert from "./Alert"
|
||||
import Reboot from "./Reboot"
|
||||
import {apiUrl, wsUrl} from "../../util/apiUrls"
|
||||
|
||||
class UploadStatus {
|
||||
uploading = false
|
||||
total = 0
|
||||
loaded = 0
|
||||
lastError = ""
|
||||
|
||||
constructor(uploading: boolean, total = 0, loaded = 0, lastError = "") {
|
||||
this.uploading = uploading
|
||||
this.total = total
|
||||
this.loaded = loaded
|
||||
this.lastError = lastError
|
||||
}
|
||||
|
||||
static fromProgressEvent(progressEvent: {
|
||||
loaded: number
|
||||
total: number
|
||||
}): UploadStatus {
|
||||
return new UploadStatus(true, progressEvent.total, progressEvent.loaded)
|
||||
}
|
||||
}
|
||||
|
||||
class RaucStatus {
|
||||
installing = false
|
||||
percent = 0
|
||||
message = ""
|
||||
last_error = ""
|
||||
log = ""
|
||||
}
|
||||
|
||||
const byteSizeOptions = {units: "metric"}
|
||||
|
||||
type Props = {}
|
||||
|
||||
type State = {
|
||||
uploadStatus: UploadStatus
|
||||
uploadFilename: string
|
||||
raucStatus: RaucStatus
|
||||
wsConnected: boolean
|
||||
}
|
||||
|
||||
export default class Updater extends Component<Props, State> {
|
||||
private dropzoneRef = createRef<Dropzone>()
|
||||
private conn: WebSocket | undefined
|
||||
|
||||
constructor(props?: Props | undefined, context?: any) {
|
||||
super(props, context)
|
||||
|
||||
this.state = {
|
||||
uploadStatus: new UploadStatus(false),
|
||||
uploadFilename: "",
|
||||
raucStatus: new RaucStatus(),
|
||||
wsConnected: false,
|
||||
}
|
||||
|
||||
this.connectWebsocket()
|
||||
}
|
||||
|
||||
private buttonClick = () => {
|
||||
if (!this.acceptUploads()) return
|
||||
|
||||
this.dropzoneRef.current?.openFileDialog()
|
||||
}
|
||||
|
||||
private onFilesAdded = (files: File[]) => {
|
||||
if (files.length === 0) return
|
||||
const newFile = files[0]
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("updateFile", newFile)
|
||||
|
||||
this.setState({
|
||||
uploadStatus: new UploadStatus(true, newFile.size, 0),
|
||||
uploadFilename: newFile.name,
|
||||
})
|
||||
|
||||
axios
|
||||
.post(apiUrl + "/update", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: (progressEvent: {loaded: number; total: number}) => {
|
||||
this.setState({
|
||||
uploadStatus: UploadStatus.fromProgressEvent(progressEvent),
|
||||
})
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.resetUpload()
|
||||
})
|
||||
.catch((reason: any) => {
|
||||
this.resetUpload(String(reason))
|
||||
})
|
||||
}
|
||||
|
||||
private resetUpload = (lastError = "") => {
|
||||
this.setState({
|
||||
uploadStatus: new UploadStatus(false, 0, 0, lastError),
|
||||
})
|
||||
this.dropzoneRef.current?.reset()
|
||||
}
|
||||
|
||||
private connectWebsocket = () => {
|
||||
if (window.WebSocket) {
|
||||
this.conn = new WebSocket(wsUrl)
|
||||
this.conn.onopen = () => {
|
||||
this.setState({wsConnected: true})
|
||||
console.log("WS connected")
|
||||
}
|
||||
this.conn.onclose = () => {
|
||||
this.setState({wsConnected: false})
|
||||
console.log("WS connection closed")
|
||||
window.setTimeout(this.connectWebsocket, 3000)
|
||||
}
|
||||
this.conn.onmessage = (evt) => {
|
||||
var messages = evt.data.split("\n")
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
this.setState({
|
||||
raucStatus: Object.assign(
|
||||
new RaucStatus(),
|
||||
JSON.parse(messages[i])
|
||||
),
|
||||
})
|
||||
|
||||
console.log(this.state.raucStatus)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("Your browser does not support WebSockets")
|
||||
}
|
||||
}
|
||||
|
||||
private acceptUploads(): boolean {
|
||||
return !this.state.uploadStatus.uploading && !this.state.raucStatus.installing
|
||||
}
|
||||
|
||||
private updateCompleted(): boolean {
|
||||
return (
|
||||
!this.state.raucStatus.installing &&
|
||||
this.state.raucStatus.percent === 100 &&
|
||||
this.state.raucStatus.last_error === ""
|
||||
)
|
||||
}
|
||||
|
||||
private uploadPercentage(): number {
|
||||
if (this.state.uploadStatus.uploading && this.state.uploadStatus.total > 0) {
|
||||
return Math.round(
|
||||
(this.state.uploadStatus.loaded / this.state.uploadStatus.total) * 100
|
||||
)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private circleColor(): string {
|
||||
if (this.state.raucStatus.installing) return "#FF0039"
|
||||
if (this.state.uploadStatus.uploading) return "#148420"
|
||||
return "#1f85de"
|
||||
}
|
||||
|
||||
private circlePercentage(): number {
|
||||
if (this.acceptUploads()) return 0
|
||||
if (this.state.raucStatus.installing) return this.state.raucStatus.percent
|
||||
if (this.state.uploadStatus.uploading) return this.uploadPercentage()
|
||||
return 0
|
||||
}
|
||||
|
||||
render() {
|
||||
const acceptUploads = this.acceptUploads()
|
||||
const circleColor = this.circleColor()
|
||||
const circlePercentage = this.circlePercentage()
|
||||
const updateCompleted = this.updateCompleted()
|
||||
|
||||
let topText = ""
|
||||
let bottomText = ""
|
||||
let bottomText2 = ""
|
||||
|
||||
if (this.state.uploadStatus.uploading) {
|
||||
topText = "Uploading"
|
||||
bottomText = this.state.uploadFilename
|
||||
bottomText2 = `${byteSize(this.state.uploadStatus.loaded, byteSizeOptions)}
|
||||
/
|
||||
${byteSize(this.state.uploadStatus.total, byteSizeOptions)}`
|
||||
} else if (this.state.raucStatus.installing) {
|
||||
topText = "Updating firmware"
|
||||
bottomText = this.state.raucStatus.message
|
||||
} else {
|
||||
topText = "Upload firmware package"
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="uploader">
|
||||
<div class="card upload">
|
||||
<div>
|
||||
<p class="top">{topText}</p>
|
||||
</div>
|
||||
<Dropzone
|
||||
ref={this.dropzoneRef}
|
||||
onFilesAdded={this.onFilesAdded}
|
||||
disabled={!acceptUploads}
|
||||
accept=".raucb"
|
||||
>
|
||||
<ProgressCircle
|
||||
ready={acceptUploads}
|
||||
progress={circlePercentage}
|
||||
color={circleColor}
|
||||
>
|
||||
<button onClick={this.buttonClick}>
|
||||
<Icon icon={mdiUpload} size={50} />
|
||||
</button>
|
||||
</ProgressCircle>
|
||||
</Dropzone>
|
||||
<div>
|
||||
<p>{bottomText}</p>
|
||||
<p>{bottomText2}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{this.state.wsConnected ? null : <Alert message="No connection" />}
|
||||
|
||||
{this.state.uploadStatus.lastError ? (
|
||||
<Alert
|
||||
source="Upload"
|
||||
message={this.state.uploadStatus.lastError}
|
||||
/>
|
||||
) : null}
|
||||
{this.state.raucStatus.last_error ? (
|
||||
<Alert
|
||||
source="Update"
|
||||
message={this.state.raucStatus.last_error}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{updateCompleted ? <Reboot /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
14
ui/src/components/app.tsx
Normal file
14
ui/src/components/app.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Component} from "preact"
|
||||
import Updater from "./Upload/Updater"
|
||||
import logo from "../assets/logo.svg"
|
||||
|
||||
export default class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<img src={logo} height="64" />
|
||||
<Updater />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
5
ui/src/main.tsx
Normal file
5
ui/src/main.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {render} from "preact"
|
||||
import App from "./components/app"
|
||||
import "./style/index.scss"
|
||||
|
||||
render(<App />, document.getElementById("app")!)
|
1
ui/src/preact.d.ts
vendored
Normal file
1
ui/src/preact.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
import JSX = preact.JSX
|
26
ui/src/style/index.scss
Normal file
26
ui/src/style/index.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
font-family: "Helvetica Neue", arial, sans-serif;
|
||||
font-weight: 400;
|
||||
color: #444;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
background-color: #222;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#app {
|
||||
padding-top: 100px;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.3em 0;
|
||||
}
|
12
ui/src/util/apiUrls.ts
Normal file
12
ui/src/util/apiUrls.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
let apiHost = document.location.host
|
||||
const httpProto = document.location.protocol
|
||||
const wsProto = httpProto === "https:" ? "wss:" : "ws:"
|
||||
|
||||
if (import.meta.env.VITE_API_HOST !== undefined) {
|
||||
apiHost = import.meta.env.VITE_API_HOST as string
|
||||
}
|
||||
|
||||
const apiUrl = `${httpProto}//${apiHost}/api`
|
||||
const wsUrl = `${wsProto}//${apiHost}/api/ws`
|
||||
|
||||
export {apiUrl, wsUrl}
|
1
ui/src/vite-env.d.ts
vendored
Normal file
1
ui/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
21
ui/tsconfig.json
Normal file
21
ui/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
}
|
||||
}
|
10
ui/ui.go
Normal file
10
ui/ui.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
const AssetsDir = "dist"
|
||||
|
||||
//go:embed dist/**
|
||||
var Assets embed.FS
|
7
ui/vite.config.ts
Normal file
7
ui/vite.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {defineConfig} from "vite"
|
||||
import preact from "@preact/preset-vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
})
|
Loading…
Reference in a new issue