diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..9520ff3 --- /dev/null +++ b/.air.toml @@ -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 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..f379b26 --- /dev/null +++ b/.drone.yml @@ -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/... diff --git a/.editorconfig b/.editorconfig index 7bad95f..23d0200 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9a5aec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03c9e76..bf556ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,22 @@ repos: -- repo: https://github.com/tekwizely/pre-commit-golang - rev: v1.0.0-beta.4 - hooks: - - id: golangci-lint-repo-mod - name: GolangCI Lint - - id: go-test-repo-mod - name: Test + - repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-beta.4 + hooks: + - id: golangci-lint-repo-mod + name: GolangCI Lint + - id: go-test-repo-mod + 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 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..c55f4de --- /dev/null +++ b/.prettierrc.json @@ -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 + } + } + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index d54d614..1123069 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,12 +1,21 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Launch SEBRAUC server", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/src" - } - ] + "version": "0.2.0", + "configurations": [ + { + "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": ["/**"], + "type": "pwa-node" + } + ] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..c074425 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +![SEBRAUC](ui/src/assets/logo_border.svg) diff --git a/go.mod b/go.mod index 6e55f44..dd949c7 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0ffa4ca..fe6435e 100644 --- a/go.sum +++ b/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= diff --git a/openapi.yml b/openapi.yml index 4daeb3e..a9332ce 100644 --- a/openapi.yml +++ b/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: diff --git a/src/assets/files/index.html b/src/assets/files/index.html index e5d8e9d..0554e60 100644 --- a/src/assets/files/index.html +++ b/src/assets/files/index.html @@ -1,88 +1,88 @@ - - Chat Example - - - - -
+ #form { + padding: 0 0.5em 0 0.5em; + margin: 0; + position: absolute; + bottom: 1em; + left: 0px; + width: 100%; + overflow: hidden; + } + + + +
-
- - -
- +
+ + +
+ diff --git a/src/fixtures/testutil.go b/src/fixtures/testutil.go index 3646a5d..ee2f352 100644 --- a/src/fixtures/testutil.go +++ b/src/fixtures/testutil.go @@ -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 { diff --git a/src/fixtures/testutil_test.go b/src/fixtures/testutil_test.go index 8cca85c..44b32ef 100644 --- a/src/fixtures/testutil_test.go +++ b/src/fixtures/testutil_test.go @@ -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")) } diff --git a/src/rauc/rauc.go b/src/rauc/rauc.go index 246f308..fe465e4 100644 --- a/src/rauc/rauc.go +++ b/src/rauc/rauc.go @@ -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 diff --git a/src/server/hub.go b/src/server/hub.go index 9470a36..77e1dfd 100644 --- a/src/server/hub.go +++ b/src/server/hub.go @@ -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), } } diff --git a/src/server/server.go b/src/server/server.go index c13a7b2..e29f1a0 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -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,31 +34,43 @@ type statusMessage struct { func NewServer(address string) *SEBRAUCServer { hub := NewHub() - raucUpdater := &rauc.Rauc{ - Command: "go", - Args: []string{ - "run", - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock", - }, - Broadcast: hub.Broadcast, + raucUpdater := rauc.NewRauc("go", []string{ + "run", + "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock", + }, hub.Broadcast) + + tmpdir, err := util.NewTmpdir() + if err != nil { + panic(err) } return &SEBRAUCServer{ address: address, raucUpdater: raucUpdater, hub: hub, + tmpdir: tmpdir, } } func (srv *SEBRAUCServer) Run() error { app := fiber.New(fiber.Config{ - AppName: "SEBRAUC", - BodyLimit: 1024 * 1024 * 1024, - ErrorHandler: errorHandler, + 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 } diff --git a/src/util/util.go b/src/util/util.go index 486b648..93062c8 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -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") +} diff --git a/src/util/util_test.go b/src/util/util_test.go index 30b2ab0..f6a1f60 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -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) +} diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 0000000..029df76 --- /dev/null +++ b/ui/.env.development @@ -0,0 +1 @@ +VITE_API_HOST=127.0.0.1:8080 diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..d451ff1 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..cf876c6 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + SEBRAUC + + +
+ + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..5efdbac --- /dev/null +++ b/ui/package.json @@ -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" + } +} diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml new file mode 100644 index 0000000..1110f3a --- /dev/null +++ b/ui/pnpm-lock.yaml @@ -0,0 +1,1222 @@ +lockfileVersion: 5.3 + +specifiers: + "@mdi/js": ^6.5.95 + "@preact/preset-vite": ^2.1.5 + "@types/byte-size": ^8.1.0 + axios: ^0.24.0 + byte-size: ^8.1.0 + preact: ^10.5.15 + prettier: ^2.4.1 + sass: ^1.43.4 + typescript: ^4.5.2 + vite: ^2.6.14 + +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_preact@10.5.15+vite@2.6.14 + "@types/byte-size": 8.1.0 + prettier: 2.4.1 + sass: 1.43.4 + typescript: 4.5.2 + vite: 2.6.14_sass@1.43.4 + +packages: + /@babel/code-frame/7.16.0: + resolution: + { + integrity: sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/highlight": 7.16.0 + dev: true + + /@babel/compat-data/7.16.4: + resolution: + { + integrity: sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/core/7.16.0: + resolution: + { + integrity: sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/code-frame": 7.16.0 + "@babel/generator": 7.16.0 + "@babel/helper-compilation-targets": 7.16.3_@babel+core@7.16.0 + "@babel/helper-module-transforms": 7.16.0 + "@babel/helpers": 7.16.3 + "@babel/parser": 7.16.4 + "@babel/template": 7.16.0 + "@babel/traverse": 7.16.3 + "@babel/types": 7.16.0 + convert-source-map: 1.8.0 + debug: 4.3.2 + gensync: 1.0.0-beta.2 + json5: 2.2.0 + semver: 6.3.0 + source-map: 0.5.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator/7.16.0: + resolution: + { + integrity: sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + jsesc: 2.5.2 + source-map: 0.5.7 + dev: true + + /@babel/helper-annotate-as-pure/7.16.0: + resolution: + { + integrity: sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-compilation-targets/7.16.3_@babel+core@7.16.0: + resolution: + { + integrity: sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA==, + } + engines: {node: ">=6.9.0"} + peerDependencies: + "@babel/core": ^7.0.0 + dependencies: + "@babel/compat-data": 7.16.4 + "@babel/core": 7.16.0 + "@babel/helper-validator-option": 7.14.5 + browserslist: 4.18.1 + semver: 6.3.0 + dev: true + + /@babel/helper-function-name/7.16.0: + resolution: + { + integrity: sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-get-function-arity": 7.16.0 + "@babel/template": 7.16.0 + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-get-function-arity/7.16.0: + resolution: + { + integrity: sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-hoist-variables/7.16.0: + resolution: + { + integrity: sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-member-expression-to-functions/7.16.0: + resolution: + { + integrity: sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-module-imports/7.16.0: + resolution: + { + integrity: sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-module-transforms/7.16.0: + resolution: + { + integrity: sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-module-imports": 7.16.0 + "@babel/helper-replace-supers": 7.16.0 + "@babel/helper-simple-access": 7.16.0 + "@babel/helper-split-export-declaration": 7.16.0 + "@babel/helper-validator-identifier": 7.15.7 + "@babel/template": 7.16.0 + "@babel/traverse": 7.16.3 + "@babel/types": 7.16.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression/7.16.0: + resolution: + { + integrity: sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-plugin-utils/7.14.5: + resolution: + { + integrity: sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/helper-replace-supers/7.16.0: + resolution: + { + integrity: sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-member-expression-to-functions": 7.16.0 + "@babel/helper-optimise-call-expression": 7.16.0 + "@babel/traverse": 7.16.3 + "@babel/types": 7.16.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-simple-access/7.16.0: + resolution: + { + integrity: sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-split-export-declaration/7.16.0: + resolution: + { + integrity: sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.16.0 + dev: true + + /@babel/helper-validator-identifier/7.15.7: + resolution: + { + integrity: sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/helper-validator-option/7.14.5: + resolution: + { + integrity: sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/helpers/7.16.3: + resolution: + { + integrity: sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/template": 7.16.0 + "@babel/traverse": 7.16.3 + "@babel/types": 7.16.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight/7.16.0: + resolution: + { + integrity: sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-validator-identifier": 7.15.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser/7.16.4: + resolution: + { + integrity: sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==, + } + engines: {node: ">=6.0.0"} + hasBin: true + dev: true + + /@babel/plugin-syntax-jsx/7.16.0: + resolution: + { + integrity: sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==, + } + engines: {node: ">=6.9.0"} + peerDependencies: + "@babel/core": ^7.0.0-0 + dependencies: + "@babel/helper-plugin-utils": 7.14.5 + dev: true + + /@babel/plugin-transform-react-jsx/7.16.0: + resolution: + { + integrity: sha512-rqDgIbukZ44pqq7NIRPGPGNklshPkvlmvqjdx3OZcGPk4zGIenYkxDTvl3LsSL8gqcc3ZzGmXPE6hR/u/voNOw==, + } + engines: {node: ">=6.9.0"} + peerDependencies: + "@babel/core": ^7.0.0-0 + dependencies: + "@babel/helper-annotate-as-pure": 7.16.0 + "@babel/helper-module-imports": 7.16.0 + "@babel/helper-plugin-utils": 7.14.5 + "@babel/plugin-syntax-jsx": 7.16.0 + "@babel/types": 7.16.0 + dev: true + + /@babel/template/7.16.0: + resolution: + { + integrity: sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/code-frame": 7.16.0 + "@babel/parser": 7.16.4 + "@babel/types": 7.16.0 + dev: true + + /@babel/traverse/7.16.3: + resolution: + { + integrity: sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/code-frame": 7.16.0 + "@babel/generator": 7.16.0 + "@babel/helper-function-name": 7.16.0 + "@babel/helper-hoist-variables": 7.16.0 + "@babel/helper-split-export-declaration": 7.16.0 + "@babel/parser": 7.16.4 + "@babel/types": 7.16.0 + debug: 4.3.2 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types/7.16.0: + resolution: + { + integrity: sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-validator-identifier": 7.15.7 + to-fast-properties: 2.0.0 + dev: true + + /@mdi/js/6.5.95: + resolution: + { + integrity: sha512-x/bwEoAGP+Mo10Dfk5audNIPi7Yz8ZBrILcbXLW3ShOI/njpgodzpgpC2WYK3D2ZSC392peRRemIFb/JsyzzYQ==, + } + dev: false + + /@preact/preset-vite/2.1.5_preact@10.5.15+vite@2.6.14: + resolution: + { + integrity: sha512-G+Op0d940lVwOH93Tkhz4BTgE88VpQBFWHqnz8QjHpo6IwmfP3HHQJHrp9dEt5Di2f2HQ4bpmhDqBl7+bzir6g==, + } + peerDependencies: + "@babel/core": 7.x + vite: 2.x + dependencies: + "@babel/plugin-transform-react-jsx": 7.16.0 + "@prefresh/vite": 2.2.4_preact@10.5.15+vite@2.6.14 + babel-plugin-transform-hook-names: 1.0.2 + debug: 4.3.2 + kolorist: 1.5.0 + resolve: 1.20.0 + vite: 2.6.14_sass@1.43.4 + transitivePeerDependencies: + - preact + - supports-color + dev: true + + /@prefresh/babel-plugin/0.4.1: + resolution: + { + integrity: sha512-gj3ekiYtHlZNz0zFI1z6a9mcYX80Qacw84+2++7V1skvO7kQoV2ux56r8bJkTBbKMVxwAgaYrxxIdUCYlclE7Q==, + } + dev: true + + /@prefresh/core/1.3.2_preact@10.5.15: + resolution: + { + integrity: sha512-Iv+uI698KDgWsrKpLvOgN3hmAMyvhVgn09mcnhZ98BUNdg/qrxE7tcUf5yFCImkgqED5/Dcn8G5hFy4IikEDvg==, + } + peerDependencies: + preact: ^10.0.0 + dependencies: + preact: 10.5.15 + dev: true + + /@prefresh/utils/1.1.1: + resolution: + { + integrity: sha512-MUhT5m2XNN5NsZl4GnpuvlzLo6VSTa/+wBfBd3fiWUvHGhv0GF9hnA1pd//v0uJaKwUnVRQ1hYElxCV7DtYsCQ==, + } + dev: true + + /@prefresh/vite/2.2.4_preact@10.5.15+vite@2.6.14: + resolution: + { + integrity: sha512-rBBb3tagdigz2ukSc5Rtg8PrbmdvEtJhE37FJRQdHISMcVdN1OkL4003eH2DEOA1231ydbGiKqpaKZisI+zSEg==, + } + peerDependencies: + preact: ^10.4.0 + vite: ">=2.0.0-beta.3" + dependencies: + "@babel/core": 7.16.0 + "@prefresh/babel-plugin": 0.4.1 + "@prefresh/core": 1.3.2_preact@10.5.15 + "@prefresh/utils": 1.1.1 + "@rollup/pluginutils": 4.1.1 + preact: 10.5.15 + vite: 2.6.14_sass@1.43.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@rollup/pluginutils/4.1.1: + resolution: + { + integrity: sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==, + } + engines: {node: ">= 8.0.0"} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.0 + dev: true + + /@types/byte-size/8.1.0: + resolution: + { + integrity: sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==, + } + dev: true + + /ansi-styles/3.2.1: + resolution: + { + integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==, + } + engines: {node: ">=4"} + dependencies: + color-convert: 1.9.3 + dev: true + + /anymatch/3.1.2: + resolution: + { + integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==, + } + engines: {node: ">= 8"} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.0 + dev: true + + /axios/0.24.0: + resolution: + { + integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==, + } + dependencies: + follow-redirects: 1.14.5 + transitivePeerDependencies: + - debug + dev: false + + /babel-plugin-transform-hook-names/1.0.2: + resolution: + { + integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==, + } + peerDependencies: + "@babel/core": ^7.12.10 + dev: true + + /binary-extensions/2.2.0: + resolution: + { + integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==, + } + engines: {node: ">=8"} + dev: true + + /braces/3.0.2: + resolution: + { + integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==, + } + engines: {node: ">=8"} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist/4.18.1: + resolution: + { + integrity: sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==, + } + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001282 + electron-to-chromium: 1.3.903 + escalade: 3.1.1 + node-releases: 2.0.1 + picocolors: 1.0.0 + dev: true + + /byte-size/8.1.0: + resolution: + { + integrity: sha512-FkgMTAg44I0JtEaUAvuZTtU2a2YDmBRbQxdsQNSMtLCjhG0hMcF5b1IMN9UjSCJaU4nvlj/GER7B9sI4nKdCgA==, + } + engines: {node: ">=12.17"} + dev: false + + /caniuse-lite/1.0.30001282: + resolution: + { + integrity: sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==, + } + dev: true + + /chalk/2.4.2: + resolution: + { + integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==, + } + engines: {node: ">=4"} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chokidar/3.5.2: + resolution: + { + integrity: sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==, + } + engines: {node: ">= 8.10.0"} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /color-convert/1.9.3: + resolution: + { + integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==, + } + dependencies: + color-name: 1.1.3 + dev: true + + /color-name/1.1.3: + resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} + dev: true + + /convert-source-map/1.8.0: + resolution: + { + integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==, + } + dependencies: + safe-buffer: 5.1.2 + dev: true + + /debug/4.3.2: + resolution: + { + integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==, + } + engines: {node: ">=6.0"} + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /electron-to-chromium/1.3.903: + resolution: + { + integrity: sha512-+PnYAyniRRTkNq56cqYDLq9LyklZYk0hqoDy9GpcU11H5QjRmFZVDbxtgHUMK/YzdNTcn1XWP5gb+hFlSCr20g==, + } + dev: true + + /esbuild-android-arm64/0.13.14: + resolution: + { + integrity: sha512-Q+Xhfp827r+ma8/DJgpMRUbDZfefsk13oePFEXEIJ4gxFbNv5+vyiYXYuKm43/+++EJXpnaYmEnu4hAKbAWYbA==, + } + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.13.14: + resolution: + { + integrity: sha512-YmOhRns6QBNSjpVdTahi/yZ8dscx9ai7a6OY6z5ACgOuQuaQ2Qk2qgJ0/siZ6LgD0gJFMV8UINFV5oky5TFNQQ==, + } + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.13.14: + resolution: + { + integrity: sha512-Lp00VTli2jqZghSa68fx3fEFCPsO1hK59RMo1PRap5RUjhf55OmaZTZYnCDI0FVlCtt+gBwX5qwFt4lc6tI1xg==, + } + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.13.14: + resolution: + { + integrity: sha512-BKosI3jtvTfnmsCW37B1TyxMUjkRWKqopR0CE9AF2ratdpkxdR24Vpe3gLKNyWiZ7BE96/SO5/YfhbPUzY8wKw==, + } + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.13.14: + resolution: + { + integrity: sha512-yd2uh0yf+fWv5114+SYTl4/1oDWtr4nN5Op+PGxAkMqHfYfLjFKpcxwCo/QOS/0NWqPVE8O41IYZlFhbEN2B8Q==, + } + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.13.14: + resolution: + { + integrity: sha512-a8rOnS1oWSfkkYWXoD2yXNV4BdbDKA7PNVQ1klqkY9SoSApL7io66w5H44mTLsfyw7G6Z2vLlaLI2nz9MMAowA==, + } + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.13.14: + resolution: + { + integrity: sha512-yPZSoMs9W2MC3Dw+6kflKt5FfQm6Dicex9dGIr1OlHRsn3Hm7yGMUTctlkW53KknnZdOdcdd5upxvbxqymczVQ==, + } + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.13.14: + resolution: + { + integrity: sha512-8chZE4pkKRvJ/M/iwsNQ1KqsRg2RyU5eT/x2flNt/f8F2TVrDreR7I0HEeCR50wLla3B1C3wTIOzQBmjuc6uWg==, + } + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.13.14: + resolution: + { + integrity: sha512-Lvo391ln9PzC334e+jJ2S0Rt0cxP47eoH5gFyv/E8HhOnEJTvm7A+RRnMjjHnejELacTTfYgFGQYPjLsi/jObQ==, + } + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.13.14: + resolution: + { + integrity: sha512-MZhgxbmrWbpY3TOE029O6l5tokG9+Yoj2hW7vdit/d/VnmneqeGrSHADuDL6qXM8L5jaCiaivb4VhsyVCpdAbQ==, + } + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.13.14: + resolution: + { + integrity: sha512-un7KMwS7fX1Un6BjfSZxTT8L5cV/8Uf4SAhM7WYy2XF8o8TI+uRxxD03svZnRNIPsN2J5cl6qV4n7Iwz+yhhVw==, + } + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.13.14: + resolution: + { + integrity: sha512-5ekKx/YbOmmlTeNxBjh38Uh5TGn5C4uyqN17i67k18pS3J+U2hTVD7rCxcFcRS1AjNWumkVL3jWqYXadFwMS0Q==, + } + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.13.14: + resolution: + { + integrity: sha512-9bzvwewHjct2Cv5XcVoE1yW5YTW12Sk838EYfA46abgnhxGoFSD1mFcaztp5HHC43AsF+hQxbSFG/RilONARUA==, + } + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.13.14: + resolution: + { + integrity: sha512-mjMrZB76M6FmoiTvj/RGWilrioR7gVwtFBRVugr9qLarXMIU1W/pQx+ieEOtflrW61xo8w1fcxyHsVVGRvoQ0w==, + } + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.13.14: + resolution: + { + integrity: sha512-GZa6mrx2rgfbH/5uHg0Rdw50TuOKbdoKCpEBitzmG5tsXBdce+cOL+iFO5joZc6fDVCLW3Y6tjxmSXRk/v20Hg==, + } + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.13.14: + resolution: + { + integrity: sha512-Lsgqah24bT7ClHjLp/Pj3A9wxjhIAJyWQcrOV4jqXAFikmrp2CspA8IkJgw7HFjx6QrJuhpcKVbCAe/xw0i2yw==, + } + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.13.14: + resolution: + { + integrity: sha512-KP8FHVlWGhM7nzYtURsGnskXb/cBCPTfj0gOKfjKq2tHtYnhDZywsUG57nk7TKhhK0fL11LcejHG3LRW9RF/9A==, + } + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.13.14: + resolution: + { + integrity: sha512-xu4D+1ji9x53ocuomcY+KOrwAnWzhBu/wTEjpdgZ8I1c8i5vboYIeigMdzgY1UowYBKa2vZgVgUB32bu7gkxeg==, + } + hasBin: true + requiresBuild: true + optionalDependencies: + esbuild-android-arm64: 0.13.14 + esbuild-darwin-64: 0.13.14 + esbuild-darwin-arm64: 0.13.14 + esbuild-freebsd-64: 0.13.14 + esbuild-freebsd-arm64: 0.13.14 + esbuild-linux-32: 0.13.14 + esbuild-linux-64: 0.13.14 + esbuild-linux-arm: 0.13.14 + esbuild-linux-arm64: 0.13.14 + esbuild-linux-mips64le: 0.13.14 + esbuild-linux-ppc64le: 0.13.14 + esbuild-netbsd-64: 0.13.14 + esbuild-openbsd-64: 0.13.14 + esbuild-sunos-64: 0.13.14 + esbuild-windows-32: 0.13.14 + esbuild-windows-64: 0.13.14 + esbuild-windows-arm64: 0.13.14 + dev: true + + /escalade/3.1.1: + resolution: + { + integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==, + } + engines: {node: ">=6"} + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + engines: {node: ">=0.8.0"} + dev: true + + /estree-walker/2.0.2: + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, + } + dev: true + + /fill-range/7.0.1: + resolution: + { + integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==, + } + engines: {node: ">=8"} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /follow-redirects/1.14.5: + resolution: + { + integrity: sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==, + } + engines: {node: ">=4.0"} + peerDependencies: + debug: "*" + peerDependenciesMeta: + debug: + optional: true + dev: false + + /fsevents/2.3.2: + resolution: + { + integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, + } + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: + { + integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==, + } + dev: true + + /gensync/1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: {node: ">=6.9.0"} + dev: true + + /glob-parent/5.1.2: + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: {node: ">= 6"} + dependencies: + is-glob: 4.0.3 + dev: true + + /globals/11.12.0: + resolution: + { + integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, + } + engines: {node: ">=4"} + dev: true + + /has-flag/3.0.0: + resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + engines: {node: ">=4"} + dev: true + + /has/1.0.3: + resolution: + { + integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==, + } + engines: {node: ">= 0.4.0"} + dependencies: + function-bind: 1.1.1 + dev: true + + /is-binary-path/2.1.0: + resolution: + { + integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, + } + engines: {node: ">=8"} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module/2.8.0: + resolution: + { + integrity: sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==, + } + dependencies: + has: 1.0.3 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} + engines: {node: ">=0.10.0"} + dev: true + + /is-glob/4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: {node: ">=0.10.0"} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: {node: ">=0.12.0"} + dev: true + + /js-tokens/4.0.0: + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } + dev: true + + /jsesc/2.5.2: + resolution: + { + integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==, + } + engines: {node: ">=4"} + hasBin: true + dev: true + + /json5/2.2.0: + resolution: + { + integrity: sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==, + } + engines: {node: ">=6"} + hasBin: true + dependencies: + minimist: 1.2.5 + dev: true + + /kolorist/1.5.0: + resolution: + { + integrity: sha512-pPobydIHK884YBtkS/tWSZXpSAEpcMbilyun3KL37ot935qL2HNKm/tI45i/Rd+MxdIWEhm7/LmUQzWZYK+Qhg==, + } + dev: true + + /minimist/1.2.5: + resolution: + { + integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==, + } + dev: true + + /ms/2.1.2: + resolution: + { + integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, + } + dev: true + + /nanoid/3.1.30: + resolution: + { + integrity: sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==, + } + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /node-releases/2.0.1: + resolution: + { + integrity: sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==, + } + dev: true + + /normalize-path/3.0.0: + resolution: + { + integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, + } + engines: {node: ">=0.10.0"} + dev: true + + /path-parse/1.0.7: + resolution: + { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, + } + dev: true + + /picocolors/1.0.0: + resolution: + { + integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, + } + dev: true + + /picomatch/2.3.0: + resolution: + { + integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==, + } + engines: {node: ">=8.6"} + dev: true + + /postcss/8.3.11: + resolution: + { + integrity: sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==, + } + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.1.30 + picocolors: 1.0.0 + source-map-js: 0.6.2 + dev: true + + /preact/10.5.15: + resolution: + { + integrity: sha512-5chK29n6QcJc3m1lVrKQSQ+V7K1Gb8HeQY6FViQ5AxCAEGu3DaHffWNDkC9+miZgsLvbvU9rxbV1qinGHMHzqA==, + } + dev: false + + /prettier/2.4.1: + resolution: + { + integrity: sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==, + } + engines: {node: ">=10.13.0"} + hasBin: true + dev: true + + /readdirp/3.6.0: + resolution: + { + integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, + } + engines: {node: ">=8.10.0"} + dependencies: + picomatch: 2.3.0 + dev: true + + /resolve/1.20.0: + resolution: + { + integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==, + } + dependencies: + is-core-module: 2.8.0 + path-parse: 1.0.7 + dev: true + + /rollup/2.60.0: + resolution: + { + integrity: sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==, + } + engines: {node: ">=10.0.0"} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /safe-buffer/5.1.2: + resolution: + { + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, + } + dev: true + + /sass/1.43.4: + resolution: + { + integrity: sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==, + } + engines: {node: ">=8.9.0"} + hasBin: true + dependencies: + chokidar: 3.5.2 + dev: true + + /semver/6.3.0: + resolution: + { + integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==, + } + hasBin: true + dev: true + + /source-map-js/0.6.2: + resolution: + { + integrity: sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==, + } + engines: {node: ">=0.10.0"} + dev: true + + /source-map/0.5.7: + resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} + engines: {node: ">=0.10.0"} + dev: true + + /supports-color/5.5.0: + resolution: + { + integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==, + } + engines: {node: ">=4"} + dependencies: + has-flag: 3.0.0 + dev: true + + /to-fast-properties/2.0.0: + resolution: {integrity: sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=} + engines: {node: ">=4"} + dev: true + + /to-regex-range/5.0.1: + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: {node: ">=8.0"} + dependencies: + is-number: 7.0.0 + dev: true + + /typescript/4.5.2: + resolution: + { + integrity: sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==, + } + engines: {node: ">=4.2.0"} + hasBin: true + dev: true + + /vite/2.6.14_sass@1.43.4: + resolution: + { + integrity: sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA==, + } + engines: {node: ">=12.2.0"} + hasBin: true + peerDependencies: + less: "*" + sass: "*" + stylus: "*" + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + dependencies: + esbuild: 0.13.14 + postcss: 8.3.11 + resolve: 1.20.0 + rollup: 2.60.0 + sass: 1.43.4 + optionalDependencies: + fsevents: 2.3.2 + dev: true diff --git a/ui/src/assets/favicon.svg b/ui/src/assets/favicon.svg new file mode 100644 index 0000000..cf2fd88 --- /dev/null +++ b/ui/src/assets/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/logo.svg b/ui/src/assets/logo.svg new file mode 100644 index 0000000..94fb959 --- /dev/null +++ b/ui/src/assets/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/logo_border.svg b/ui/src/assets/logo_border.svg new file mode 100644 index 0000000..2282a96 --- /dev/null +++ b/ui/src/assets/logo_border.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/logo_square_src.svg b/ui/src/assets/logo_square_src.svg new file mode 100644 index 0000000..da8178f --- /dev/null +++ b/ui/src/assets/logo_square_src.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/assets/logo_src.svg b/ui/src/assets/logo_src.svg new file mode 100644 index 0000000..2daaae9 --- /dev/null +++ b/ui/src/assets/logo_src.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/components/Dropzone/Dropzone.scss b/ui/src/components/Dropzone/Dropzone.scss new file mode 100644 index 0000000..9e705ae --- /dev/null +++ b/ui/src/components/Dropzone/Dropzone.scss @@ -0,0 +1,9 @@ +.dropzone { + &.highlight { + filter: brightness(0.5); + } + + .fileholder { + display: none; + } +} diff --git a/ui/src/components/Dropzone/Dropzone.tsx b/ui/src/components/Dropzone/Dropzone.tsx new file mode 100644 index 0000000..c977dd4 --- /dev/null +++ b/ui/src/components/Dropzone/Dropzone.tsx @@ -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 { + static defaultProps = { + disabled: false, + clickable: false, + multiple: false, + } + + private fileInputRef = createRef() + + 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 ( +
+ + {this.props.children} +
+ ) + } +} diff --git a/ui/src/components/Icon/Icon.scss b/ui/src/components/Icon/Icon.scss new file mode 100644 index 0000000..7d64473 --- /dev/null +++ b/ui/src/components/Icon/Icon.scss @@ -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; + } +} diff --git a/ui/src/components/Icon/Icon.tsx b/ui/src/components/Icon/Icon.tsx new file mode 100644 index 0000000..824f56c --- /dev/null +++ b/ui/src/components/Icon/Icon.tsx @@ -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 { + static defaultProps = { + size: 24, + } + + render() { + return ( + + + + ) + } +} diff --git a/ui/src/components/ProgressCircle/ProgressCircle.scss b/ui/src/components/ProgressCircle/ProgressCircle.scss new file mode 100644 index 0000000..c410e1a --- /dev/null +++ b/ui/src/components/ProgressCircle/ProgressCircle.scss @@ -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; + } + } +} diff --git a/ui/src/components/ProgressCircle/ProgressCircle.tsx b/ui/src/components/ProgressCircle/ProgressCircle.tsx new file mode 100644 index 0000000..507dcd5 --- /dev/null +++ b/ui/src/components/ProgressCircle/ProgressCircle.tsx @@ -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 { + 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 ( +
+ + + + {visible ? ( + + ) : null} + + {visible ? ( + + {percentage}% + + ) : null} + + {visible ? null : this.props.children} +
+ ) + } +} diff --git a/ui/src/components/Upload/Alert.tsx b/ui/src/components/Upload/Alert.tsx new file mode 100644 index 0000000..6eb16a9 --- /dev/null +++ b/ui/src/components/Upload/Alert.tsx @@ -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 { + 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 ( +
+ + + {msg} + +
+ ) + } +} diff --git a/ui/src/components/Upload/Reboot.tsx b/ui/src/components/Upload/Reboot.tsx new file mode 100644 index 0000000..c53666e --- /dev/null +++ b/ui/src/components/Upload/Reboot.tsx @@ -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 ( +
+ + + Reboot the system to apply the update + + +
+ ) + } +} diff --git a/ui/src/components/Upload/Updater.scss b/ui/src/components/Upload/Updater.scss new file mode 100644 index 0000000..90b798e --- /dev/null +++ b/ui/src/components/Upload/Updater.scss @@ -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; + } + } +} diff --git a/ui/src/components/Upload/Updater.tsx b/ui/src/components/Upload/Updater.tsx new file mode 100644 index 0000000..49057b1 --- /dev/null +++ b/ui/src/components/Upload/Updater.tsx @@ -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 { + private dropzoneRef = createRef() + 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 ( +
+
+
+

{topText}

+
+ + + + + +
+

{bottomText}

+

{bottomText2}

+
+
+
+ {this.state.wsConnected ? null : } + + {this.state.uploadStatus.lastError ? ( + + ) : null} + {this.state.raucStatus.last_error ? ( + + ) : null} +
+ {updateCompleted ? : null} +
+ ) + } +} diff --git a/ui/src/components/app.tsx b/ui/src/components/app.tsx new file mode 100644 index 0000000..b4804c5 --- /dev/null +++ b/ui/src/components/app.tsx @@ -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 ( +
+ + +
+ ) + } +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 0000000..66f7e6b --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,5 @@ +import {render} from "preact" +import App from "./components/app" +import "./style/index.scss" + +render(, document.getElementById("app")!) diff --git a/ui/src/preact.d.ts b/ui/src/preact.d.ts new file mode 100644 index 0000000..ac79d62 --- /dev/null +++ b/ui/src/preact.d.ts @@ -0,0 +1 @@ +import JSX = preact.JSX diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss new file mode 100644 index 0000000..5774a08 --- /dev/null +++ b/ui/src/style/index.scss @@ -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; +} diff --git a/ui/src/util/apiUrls.ts b/ui/src/util/apiUrls.ts new file mode 100644 index 0000000..4a9d51b --- /dev/null +++ b/ui/src/util/apiUrls.ts @@ -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} diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..dd76ca6 --- /dev/null +++ b/ui/tsconfig.json @@ -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" + } +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..1d28f93 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,10 @@ +package ui + +import ( + "embed" +) + +const AssetsDir = "dist" + +//go:embed dist/** +var Assets embed.FS diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..80a2f42 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from "vite" +import preact from "@preact/preset-vite" + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [preact()], +})