From 1f8546cb37a6a4cbf08b1dd83c7db6b4e21ce1a1 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Fri, 19 Nov 2021 20:31:33 +0100 Subject: [PATCH 1/8] Add preact framework --- .drone.yml | 20 + .editorconfig | 10 +- .pre-commit-config.yaml | 28 +- .prettierignore | 2 + .prettierrc.json | 15 + .vscode/launch.json | 29 +- go.mod | 1 - src/assets/files/index.html | 156 ++-- src/server/server.go | 13 +- ui/.gitignore | 5 + ui/index.html | 13 + ui/package.json | 19 + ui/pnpm-lock.yaml | 1168 +++++++++++++++++++++++++++++ ui/src/assets/favicon.svg | 15 + ui/src/components/LoadingText.tsx | 7 + ui/src/components/app.tsx | 53 ++ ui/src/components/logo.tsx | 47 ++ ui/src/main.tsx | 5 + ui/src/preact.d.ts | 2 + ui/src/style/index.scss | 30 + ui/src/vite-env.d.ts | 1 + ui/tsconfig.json | 21 + ui/ui.go | 10 + ui/vite.config.ts | 7 + 24 files changed, 1571 insertions(+), 106 deletions(-) create mode 100644 .drone.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 ui/.gitignore create mode 100644 ui/index.html create mode 100644 ui/package.json create mode 100644 ui/pnpm-lock.yaml create mode 100644 ui/src/assets/favicon.svg create mode 100644 ui/src/components/LoadingText.tsx create mode 100644 ui/src/components/app.tsx create mode 100644 ui/src/components/logo.tsx create mode 100644 ui/src/main.tsx create mode 100644 ui/src/preact.d.ts create mode 100644 ui/src/style/index.scss create mode 100644 ui/src/vite-env.d.ts create mode 100644 ui/tsconfig.json create mode 100644 ui/ui.go create mode 100644 ui/vite.config.ts diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..3745ed4 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,20 @@ +kind: pipeline +name: default +type: docker + +steps: + - name: Backend lint + image: golangci/golangci-lint:latest + commands: + - golangci-lint run + - name: Backend tests + image: golangci/golangci-lint:latest + commands: + - go test -v ./src/... + - name: Frontend build + image: node:16-alpine + commands: + - cd ui + - npm install -g pnpm + - pnpm install + - pnpm run build 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/.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..69ed1d3 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": "Launch 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/go.mod b/go.mod index 6e55f44..7d1b400 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ 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/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/server/server.go b/src/server/server.go index c13a7b2..63ef036 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -5,10 +5,11 @@ import ( "net/http" "strings" - "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/cors" "github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/websocket/v2" @@ -32,7 +33,7 @@ func NewServer(address string) *SEBRAUCServer { Command: "go", Args: []string{ "run", - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock", + "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock", "fail", }, Broadcast: hub.Broadcast, } @@ -53,6 +54,9 @@ func (srv *SEBRAUCServer) Run() error { app.Use(logger.New()) + // 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,8 +68,9 @@ 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 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..ec0528d --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..3db08ef --- /dev/null +++ b/ui/package.json @@ -0,0 +1,19 @@ +{ + "name": "ui", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "preact": "^10.5.15" + }, + "devDependencies": { + "@preact/preset-vite": "^2.1.5", + "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..05b5ef9 --- /dev/null +++ b/ui/pnpm-lock.yaml @@ -0,0 +1,1168 @@ +lockfileVersion: 5.3 + +specifiers: + "@preact/preset-vite": ^2.1.5 + preact: ^10.5.15 + prettier: ^2.4.1 + sass: ^1.43.4 + typescript: ^4.5.2 + vite: ^2.6.14 + +dependencies: + preact: 10.5.15 + +devDependencies: + "@preact/preset-vite": 2.1.5_preact@10.5.15+vite@2.6.14 + 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 + + /@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 + + /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 + + /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 + + /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 + + /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..de4aedd --- /dev/null +++ b/ui/src/assets/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ui/src/components/LoadingText.tsx b/ui/src/components/LoadingText.tsx new file mode 100644 index 0000000..5e31b3a --- /dev/null +++ b/ui/src/components/LoadingText.tsx @@ -0,0 +1,7 @@ +interface Props { + isLoading: boolean +} + +export default function LoadingText(props: Props) { + return
{props.isLoading ?

Loading...

:

Fertig geladen

}
+} diff --git a/ui/src/components/app.tsx b/ui/src/components/app.tsx new file mode 100644 index 0000000..bb17383 --- /dev/null +++ b/ui/src/components/app.tsx @@ -0,0 +1,53 @@ +import LoadingText from "./LoadingText" +import {useState} from "preact/hooks" + +type ImageDataT = { + id: string + author: string + width: number + height: number + url: string + download_url: string +} + +type ImageProps = { + image?: ImageDataT +} + +function Image(props: ImageProps) { + if (props.image === undefined) { + return ( +
+ +
+ ) + } + return ( +
+ dog +

Fotograf: {props.image.author}

+
+ ) +} + +export default function App() { + const [imageData, setImageData] = useState(undefined) + + const fetchImageData = async () => { + const response = await fetch("https://picsum.photos/id/237/info") + const data = await response.json() + + setImageData(data) + + console.log({data}) + } + + return ( +
+

React Tutorial

+

Time now: {new Date().toISOString()}

+ + +
+ ) +} diff --git a/ui/src/components/logo.tsx b/ui/src/components/logo.tsx new file mode 100644 index 0000000..d7cee17 --- /dev/null +++ b/ui/src/components/logo.tsx @@ -0,0 +1,47 @@ +export const Logo = () => ( + +) 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..edeaac5 --- /dev/null +++ b/ui/src/preact.d.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line +import JSX = preact.JSX diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss new file mode 100644 index 0000000..6b25e27 --- /dev/null +++ b/ui/src/style/index.scss @@ -0,0 +1,30 @@ +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; +} + +* { + box-sizing: border-box; +} + +#app { + height: 100%; + text-align: center; + background-color: #673ab8; + color: #fff; + font-size: 1.5em; + padding-top: 100px; + + .link { + color: #fff; + } +} 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()], +}) From 14a4d7fef42c3c79661e7257e547067dadf79baa Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Fri, 19 Nov 2021 20:36:57 +0100 Subject: [PATCH 2/8] reordered CI steps --- .drone.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.drone.yml b/.drone.yml index 3745ed4..b909161 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,14 +3,6 @@ name: default type: docker steps: - - name: Backend lint - image: golangci/golangci-lint:latest - commands: - - golangci-lint run - - name: Backend tests - image: golangci/golangci-lint:latest - commands: - - go test -v ./src/... - name: Frontend build image: node:16-alpine commands: @@ -18,3 +10,8 @@ steps: - npm install -g pnpm - pnpm install - pnpm run build + - name: Backend test + image: golangci/golangci-lint:latest + commands: + - golangci-lint run + - go test -v ./src/... From 92fc0b057e3a51c201f3634d75e9a70779dcb842 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Fri, 19 Nov 2021 20:43:16 +0100 Subject: [PATCH 3/8] update ci: download go mods before --- .drone.yml | 1 + go.sum | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index b909161..a1d272d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,5 +13,6 @@ steps: - name: Backend test image: golangci/golangci-lint:latest commands: + - go mod download - golangci-lint run - go test -v ./src/... diff --git a/go.sum b/go.sum index 0ffa4ca..ef5350b 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,11 @@ code.thetadev.de/ThetaDev/gotry v0.3.2 h1:x5JOBszLbCo4FDe9V8ynHsV6EfvALV7wUqnJ/5 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 +42,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= From 75a3d67402c88e11851f357911456847bd484986 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Fri, 19 Nov 2021 20:47:24 +0100 Subject: [PATCH 4/8] ci: verbose golanci output --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index a1d272d..84cd1f5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,5 +14,5 @@ steps: image: golangci/golangci-lint:latest commands: - go mod download - - golangci-lint run + - golangci-lint run -v --timeout 2m - go test -v ./src/... From 2f893e458c39766dd4179afb93d68cc4ade0c5a4 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sat, 20 Nov 2021 20:11:40 +0100 Subject: [PATCH 5/8] Add uploader component --- .drone.yml | 2 +- .vscode/launch.json | 2 +- src/server/server.go | 2 +- ui/.env | 1 + ui/.env.production | 1 + ui/package.json | 2 + ui/pnpm-lock.yaml | 35 +++ ui/src/components/Dropzone/Dropzone.scss | 9 + ui/src/components/Dropzone/Dropzone.tsx | 110 ++++++++++ ui/src/components/Icon/Icon.scss | 4 + ui/src/components/Icon/Icon.tsx | 28 +++ ui/src/components/LoadingText.tsx | 7 - .../ProgressCircle/ProgressCircle.scss | 42 ++++ .../ProgressCircle/ProgressCircle.tsx | 60 +++++ ui/src/components/Upload/Upload.scss | 17 ++ ui/src/components/Upload/Upload.tsx | 205 ++++++++++++++++++ ui/src/components/app.tsx | 53 +---- ui/src/components/logo.tsx | 47 ---- ui/src/preact.d.ts | 1 - ui/src/style/index.scss | 12 +- 20 files changed, 525 insertions(+), 115 deletions(-) create mode 100644 ui/.env create mode 100644 ui/.env.production create mode 100644 ui/src/components/Dropzone/Dropzone.scss create mode 100644 ui/src/components/Dropzone/Dropzone.tsx create mode 100644 ui/src/components/Icon/Icon.scss create mode 100644 ui/src/components/Icon/Icon.tsx delete mode 100644 ui/src/components/LoadingText.tsx create mode 100644 ui/src/components/ProgressCircle/ProgressCircle.scss create mode 100644 ui/src/components/ProgressCircle/ProgressCircle.tsx create mode 100644 ui/src/components/Upload/Upload.scss create mode 100644 ui/src/components/Upload/Upload.tsx delete mode 100644 ui/src/components/logo.tsx diff --git a/.drone.yml b/.drone.yml index 84cd1f5..f379b26 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,5 +14,5 @@ steps: image: golangci/golangci-lint:latest commands: - go mod download - - golangci-lint run -v --timeout 2m + - golangci-lint run --timeout 5m - go test -v ./src/... diff --git a/.vscode/launch.json b/.vscode/launch.json index 69ed1d3..1123069 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch SEBRAUC server", + "name": "SEBRAUC server", "type": "go", "request": "launch", "mode": "auto", diff --git a/src/server/server.go b/src/server/server.go index 63ef036..3c3109a 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -33,7 +33,7 @@ func NewServer(address string) *SEBRAUCServer { Command: "go", Args: []string{ "run", - "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock", "fail", + "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock", }, Broadcast: hub.Broadcast, } diff --git a/ui/.env b/ui/.env new file mode 100644 index 0000000..db19551 --- /dev/null +++ b/ui/.env @@ -0,0 +1 @@ +VITE_API_URL=http://127.0.0.1:8080/api diff --git a/ui/.env.production b/ui/.env.production new file mode 100644 index 0000000..e82c617 --- /dev/null +++ b/ui/.env.production @@ -0,0 +1 @@ +VITE_API_URL=/api diff --git a/ui/package.json b/ui/package.json index 3db08ef..ad6ef6c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,6 +7,8 @@ "serve": "vite preview" }, "dependencies": { + "@mdi/js": "^6.5.95", + "axios": "^0.24.0", "preact": "^10.5.15" }, "devDependencies": { diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 05b5ef9..155f163 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -1,7 +1,9 @@ lockfileVersion: 5.3 specifiers: + "@mdi/js": ^6.5.95 "@preact/preset-vite": ^2.1.5 + axios: ^0.24.0 preact: ^10.5.15 prettier: ^2.4.1 sass: ^1.43.4 @@ -9,6 +11,8 @@ specifiers: vite: ^2.6.14 dependencies: + "@mdi/js": 6.5.95 + axios: 0.24.0 preact: 10.5.15 devDependencies: @@ -347,6 +351,13 @@ packages: 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: { @@ -445,6 +456,17 @@ packages: 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: { @@ -812,6 +834,19 @@ packages: 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: { 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..e02f746 --- /dev/null +++ b/ui/src/components/Dropzone/Dropzone.tsx @@ -0,0 +1,110 @@ +import {Component, createRef, JSX, ComponentChild, ComponentChildren} from "preact" +import "./Dropzone.scss" + +type Props = { + disabled: boolean + clickable: boolean + multiple: boolean + 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() + } + + 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 files = (evt.target as HTMLInputElement).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..0cd9c7f --- /dev/null +++ b/ui/src/components/Icon/Icon.scss @@ -0,0 +1,4 @@ +.icon > svg { + color: inherit; + fill: currentColor; +} diff --git a/ui/src/components/Icon/Icon.tsx b/ui/src/components/Icon/Icon.tsx new file mode 100644 index 0000000..598279d --- /dev/null +++ b/ui/src/components/Icon/Icon.tsx @@ -0,0 +1,28 @@ +import {Component} from "preact" +import "./Icon.scss" + +type Props = { + icon: string + size: number +} + +export default class Icon extends Component { + static defaultProps = { + size: 24, + } + + render() { + return ( + + + + ) + } +} diff --git a/ui/src/components/LoadingText.tsx b/ui/src/components/LoadingText.tsx deleted file mode 100644 index 5e31b3a..0000000 --- a/ui/src/components/LoadingText.tsx +++ /dev/null @@ -1,7 +0,0 @@ -interface Props { - isLoading: boolean -} - -export default function LoadingText(props: Props) { - return
{props.isLoading ?

Loading...

:

Fertig geladen

}
-} diff --git a/ui/src/components/ProgressCircle/ProgressCircle.scss b/ui/src/components/ProgressCircle/ProgressCircle.scss new file mode 100644 index 0000000..4878cb9 --- /dev/null +++ b/ui/src/components/ProgressCircle/ProgressCircle.scss @@ -0,0 +1,42 @@ +.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: #444; + } + } +} 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/Upload.scss b/ui/src/components/Upload/Upload.scss new file mode 100644 index 0000000..bcd7d80 --- /dev/null +++ b/ui/src/components/Upload/Upload.scss @@ -0,0 +1,17 @@ +.uploader { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 90%; + max-width: 500px; + + margin: 0 auto; + padding: 15px 8px; + + text-align: center; + + border: 3px solid #fff; + border-radius: 16px; +} diff --git a/ui/src/components/Upload/Upload.tsx b/ui/src/components/Upload/Upload.tsx new file mode 100644 index 0000000..1a02012 --- /dev/null +++ b/ui/src/components/Upload/Upload.tsx @@ -0,0 +1,205 @@ +import {mdiUpload} from "@mdi/js" +import {Component, createRef} from "preact" +import Dropzone from "../Dropzone/Dropzone" +import ProgressCircle from "../ProgressCircle/ProgressCircle" +import Icon from "../Icon/Icon" +import "./Upload.scss" +import axios from "axios" + +class UploadStatus { + uploading = false + total = 0 + loaded = 0 + + constructor(uploading: boolean, total = 0, loaded = 0) { + this.uploading = uploading + this.total = total + this.loaded = loaded + } + + 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 = "" +} + +type Props = {} + +type State = { + uploadStatus: UploadStatus + uploadFilename: string + raucStatus: RaucStatus +} + +export default class Upload extends Component { + private static readonly apiUrl = import.meta.env.VITE_API_URL as string + private static readonly postUrl = Upload.apiUrl + "/update" + private static readonly wsUrl = Upload.apiUrl.replace("http", "ws") + "/ws" + + 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(), + } + + 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(Upload.postUrl, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: (progressEvent: {loaded: number; total: number}) => { + this.setState({ + uploadStatus: UploadStatus.fromProgressEvent(progressEvent), + }) + }, + }) + .then(() => { + console.log("SUCCESS!!") + this.setState({ + uploadStatus: new UploadStatus(false), + }) + }) + .catch(() => { + console.log("FAILURE!!") + this.setState({ + uploadStatus: new UploadStatus(false), + }) + }) + } + + private connectWebsocket() { + if (window.WebSocket) { + this.conn = new WebSocket(Upload.wsUrl) + this.conn.onopen = () => { + console.log("Connected") + } + this.conn.onclose = () => { + console.log("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 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() + + let topText = "" + let bottomText = "" + + if (this.state.uploadStatus.uploading) { + topText = "Uploading" + bottomText = `${this.state.uploadFilename} ${this.state.uploadStatus.loaded} / ${this.state.uploadStatus.total} bytes` + } else if (this.state.raucStatus.installing) { + topText = "Upadating firmware" + bottomText = this.state.raucStatus.message + } else { + topText = "Upload firmware package" + } + + return ( +
+
+ {topText} +
+ + + + + +
+ {bottomText} +
+
+ ) + } +} diff --git a/ui/src/components/app.tsx b/ui/src/components/app.tsx index bb17383..1ab3565 100644 --- a/ui/src/components/app.tsx +++ b/ui/src/components/app.tsx @@ -1,53 +1,12 @@ -import LoadingText from "./LoadingText" -import {useState} from "preact/hooks" +import {Component} from "preact" +import Upload from "./Upload/Upload" -type ImageDataT = { - id: string - author: string - width: number - height: number - url: string - download_url: string -} - -type ImageProps = { - image?: ImageDataT -} - -function Image(props: ImageProps) { - if (props.image === undefined) { +export default class App extends Component { + render() { return ( -
- +
+
) } - return ( -
- dog -

Fotograf: {props.image.author}

-
- ) -} - -export default function App() { - const [imageData, setImageData] = useState(undefined) - - const fetchImageData = async () => { - const response = await fetch("https://picsum.photos/id/237/info") - const data = await response.json() - - setImageData(data) - - console.log({data}) - } - - return ( -
-

React Tutorial

-

Time now: {new Date().toISOString()}

- - -
- ) } diff --git a/ui/src/components/logo.tsx b/ui/src/components/logo.tsx deleted file mode 100644 index d7cee17..0000000 --- a/ui/src/components/logo.tsx +++ /dev/null @@ -1,47 +0,0 @@ -export const Logo = () => ( - -) diff --git a/ui/src/preact.d.ts b/ui/src/preact.d.ts index edeaac5..ac79d62 100644 --- a/ui/src/preact.d.ts +++ b/ui/src/preact.d.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import JSX = preact.JSX diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss index 6b25e27..38a43c7 100644 --- a/ui/src/style/index.scss +++ b/ui/src/style/index.scss @@ -12,19 +12,11 @@ body { -moz-osx-font-smoothing: grayscale; } -* { - box-sizing: border-box; -} - #app { height: 100%; - text-align: center; + padding-top: 100px; + background-color: #673ab8; color: #fff; font-size: 1.5em; - padding-top: 100px; - - .link { - color: #fff; - } } From 191d74076e393c02dcf4d169fb223148a65b31ca Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sat, 20 Nov 2021 20:35:02 +0100 Subject: [PATCH 6/8] Fix api urls for prod --- ui/.env | 1 - ui/.env.development | 1 + ui/.env.production | 1 - ui/src/components/Upload/Upload.tsx | 21 ++++++++++++++++----- 4 files changed, 17 insertions(+), 7 deletions(-) delete mode 100644 ui/.env create mode 100644 ui/.env.development delete mode 100644 ui/.env.production diff --git a/ui/.env b/ui/.env deleted file mode 100644 index db19551..0000000 --- a/ui/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_API_URL=http://127.0.0.1:8080/api 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/.env.production b/ui/.env.production deleted file mode 100644 index e82c617..0000000 --- a/ui/.env.production +++ /dev/null @@ -1 +0,0 @@ -VITE_API_URL=/api diff --git a/ui/src/components/Upload/Upload.tsx b/ui/src/components/Upload/Upload.tsx index 1a02012..7b31430 100644 --- a/ui/src/components/Upload/Upload.tsx +++ b/ui/src/components/Upload/Upload.tsx @@ -42,9 +42,8 @@ type State = { } export default class Upload extends Component { - private static readonly apiUrl = import.meta.env.VITE_API_URL as string - private static readonly postUrl = Upload.apiUrl + "/update" - private static readonly wsUrl = Upload.apiUrl.replace("http", "ws") + "/ws" + private postUrl: string + private wsUrl: string private dropzoneRef = createRef() private conn: WebSocket | undefined @@ -52,6 +51,18 @@ export default class Upload extends Component { constructor(props?: Props | undefined, context?: any) { super(props, context) + // Get API urls + 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 + } + + this.postUrl = `${httpProto}//${apiHost}/api/update` + this.wsUrl = `${wsProto}//${apiHost}/api/ws` + this.state = { uploadStatus: new UploadStatus(false), uploadFilename: "", @@ -80,7 +91,7 @@ export default class Upload extends Component { }) axios - .post(Upload.postUrl, formData, { + .post(this.postUrl, formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -106,7 +117,7 @@ export default class Upload extends Component { private connectWebsocket() { if (window.WebSocket) { - this.conn = new WebSocket(Upload.wsUrl) + this.conn = new WebSocket(this.wsUrl) this.conn.onopen = () => { console.log("Connected") } From 51eb9c0cac1cce6199c02464eff8c1397f9f3d34 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sun, 21 Nov 2021 01:30:58 +0100 Subject: [PATCH 7/8] finished uploader component --- go.mod | 1 - go.sum | 2 - openapi.yml | 16 +++ src/fixtures/testutil.go | 12 ++- src/fixtures/testutil_test.go | 11 ++- src/rauc/rauc.go | 9 +- src/server/server.go | 41 +++++--- src/util/util.go | 64 +++++++++++- src/util/util_test.go | 30 ++++++ ui/src/components/Dropzone/Dropzone.tsx | 11 ++- ui/src/components/Icon/Icon.scss | 10 +- ui/src/components/Icon/Icon.tsx | 3 +- ui/src/components/Upload/Alert.tsx | 44 +++++++++ ui/src/components/Upload/Upload.scss | 44 +++++++-- ui/src/components/Upload/Upload.tsx | 123 +++++++++++++++++------- ui/src/style/index.scss | 2 +- 16 files changed, 344 insertions(+), 79 deletions(-) create mode 100644 ui/src/components/Upload/Alert.tsx diff --git a/go.mod b/go.mod index 7d1b400..dd949c7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ 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/gofiber/fiber/v2 v2.21.0 github.com/gofiber/websocket/v2 v2.0.12 diff --git a/go.sum b/go.sum index ef5350b..fe6435e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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..7d2f6d7 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" @@ -33,9 +34,11 @@ type RaucStatus struct { Log string `json:"log"` } -func (r *Rauc) completed() { +func (r *Rauc) completed(updateFile string) { r.status.Installing = false r.Broadcast <- r.GetStatusJson() + + _ = os.Remove(updateFile) } func (r *Rauc) RunRauc(updateFile string) error { @@ -98,7 +101,7 @@ func (r *Rauc) RunRauc(updateFile string) error { err := cmd.Start() if err != nil { - r.completed() + r.completed(updateFile) return err } @@ -107,7 +110,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/server.go b/src/server/server.go index 3c3109a..ee2d50b 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -2,8 +2,10 @@ package server import ( "errors" + "fmt" "net/http" "strings" + "time" "code.thetadev.de/TSGRain/SEBRAUC/src/rauc" "code.thetadev.de/TSGRain/SEBRAUC/src/util" @@ -19,6 +21,8 @@ type SEBRAUCServer struct { address string raucUpdater *rauc.Rauc hub *MessageHub + tmpdir string + currentId int } type statusMessage struct { @@ -38,18 +42,25 @@ func NewServer(address string) *SEBRAUCServer { Broadcast: 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()) @@ -75,9 +86,9 @@ func (srv *SEBRAUCServer) Run() error { // 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() @@ -90,12 +101,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) { @@ -112,15 +127,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 } @@ -128,7 +138,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..147ca57 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -1,8 +1,70 @@ package util -import "os" +import ( + "crypto/rand" + "fmt" + "os" + "os/exec" + "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() +} diff --git a/src/util/util_test.go b/src/util/util_test.go index 30b2ab0..c616e40 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,30 @@ func TestDoesFileExist(t *testing.T) { }) } } + +func TestTmpdir(t *testing.T) { + 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/src/components/Dropzone/Dropzone.tsx b/ui/src/components/Dropzone/Dropzone.tsx index e02f746..c977dd4 100644 --- a/ui/src/components/Dropzone/Dropzone.tsx +++ b/ui/src/components/Dropzone/Dropzone.tsx @@ -5,6 +5,7 @@ type Props = { disabled: boolean clickable: boolean multiple: boolean + accept?: string onFilesAdded?: (files: File[]) => void children?: ComponentChild | ComponentChildren @@ -27,6 +28,12 @@ export default class Dropzone extends Component { 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() @@ -35,7 +42,8 @@ export default class Dropzone extends Component { private onFilesAdded = (evt: JSX.TargetedEvent) => { if (this.props.disabled || evt.target === null) return - const files = (evt.target as HTMLInputElement).files + const input = evt.target as HTMLInputElement + const files = input.files if (this.props.onFilesAdded) { const array = this.fileListToArray(files) @@ -101,6 +109,7 @@ export default class Dropzone extends Component { class="fileholder" type="file" multiple={this.props.multiple} + accept={this.props.accept} onInput={this.onFilesAdded} /> {this.props.children} diff --git a/ui/src/components/Icon/Icon.scss b/ui/src/components/Icon/Icon.scss index 0cd9c7f..d4796e7 100644 --- a/ui/src/components/Icon/Icon.scss +++ b/ui/src/components/Icon/Icon.scss @@ -1,4 +1,8 @@ -.icon > svg { - color: inherit; - fill: currentColor; +.icon { + vertical-align: sub; + + > svg { + color: inherit; + fill: currentColor; + } } diff --git a/ui/src/components/Icon/Icon.tsx b/ui/src/components/Icon/Icon.tsx index 598279d..824f56c 100644 --- a/ui/src/components/Icon/Icon.tsx +++ b/ui/src/components/Icon/Icon.tsx @@ -4,6 +4,7 @@ import "./Icon.scss" type Props = { icon: string size: number + color?: string } export default class Icon extends Component { @@ -13,7 +14,7 @@ export default class Icon extends Component { render() { return ( - + void +} + +export default class Alert extends Component { + static defaultProps = { + type: "error", + } + + 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 ( +

+ {(() => { + switch (this.props.type) { + case "success": + return + default: + return + } + })()} + + {msg} +

+ ) + } +} diff --git a/ui/src/components/Upload/Upload.scss b/ui/src/components/Upload/Upload.scss index bcd7d80..c17e8ef 100644 --- a/ui/src/components/Upload/Upload.scss +++ b/ui/src/components/Upload/Upload.scss @@ -4,14 +4,44 @@ justify-content: center; align-items: center; - width: 90%; - max-width: 500px; - margin: 0 auto; - padding: 15px 8px; + max-width: 500px; + width: 90%; - text-align: center; + > * { + width: 100%; + } - border: 3px solid #fff; - border-radius: 16px; + .card { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + padding: 15px 8px; + + text-align: center; + + border: 3px solid #fff; + border-radius: 16px; + + .top { + font-size: 1.5em; + } + } + + .alert { + margin: 0.6em 0; + // border: 2px solid transparent; + border-radius: 16px; + + .icon { + margin-right: 0.4em; + } + + &.clickable:hover { + cursor: pointer; + background: #ffffff66; + } + } } diff --git a/ui/src/components/Upload/Upload.tsx b/ui/src/components/Upload/Upload.tsx index 7b31430..d23387b 100644 --- a/ui/src/components/Upload/Upload.tsx +++ b/ui/src/components/Upload/Upload.tsx @@ -1,20 +1,23 @@ -import {mdiUpload} from "@mdi/js" import {Component, createRef} from "preact" +import {mdiUpload} from "@mdi/js" import Dropzone from "../Dropzone/Dropzone" import ProgressCircle from "../ProgressCircle/ProgressCircle" import Icon from "../Icon/Icon" import "./Upload.scss" import axios from "axios" +import Alert from "./Alert" class UploadStatus { uploading = false total = 0 loaded = 0 + lastError = "" - constructor(uploading: boolean, total = 0, loaded = 0) { + constructor(uploading: boolean, total = 0, loaded = 0, lastError = "") { this.uploading = uploading this.total = total this.loaded = loaded + this.lastError = lastError } static fromProgressEvent(progressEvent: { @@ -39,10 +42,11 @@ type State = { uploadStatus: UploadStatus uploadFilename: string raucStatus: RaucStatus + wsConnected: boolean } export default class Upload extends Component { - private postUrl: string + private apiUrl: string private wsUrl: string private dropzoneRef = createRef() @@ -60,13 +64,14 @@ export default class Upload extends Component { apiHost = import.meta.env.VITE_API_HOST as string } - this.postUrl = `${httpProto}//${apiHost}/api/update` + this.apiUrl = `${httpProto}//${apiHost}/api` this.wsUrl = `${wsProto}//${apiHost}/api/ws` this.state = { uploadStatus: new UploadStatus(false), uploadFilename: "", raucStatus: new RaucStatus(), + wsConnected: false, } this.connectWebsocket() @@ -91,7 +96,7 @@ export default class Upload extends Component { }) axios - .post(this.postUrl, formData, { + .post(this.apiUrl + "/update", formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -102,27 +107,30 @@ export default class Upload extends Component { }, }) .then(() => { - console.log("SUCCESS!!") - this.setState({ - uploadStatus: new UploadStatus(false), - }) + this.resetUpload() }) - .catch(() => { - console.log("FAILURE!!") - this.setState({ - uploadStatus: new UploadStatus(false), - }) + .catch((reason: any) => { + this.resetUpload(String(reason)) }) } - private connectWebsocket() { + 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(this.wsUrl) this.conn.onopen = () => { - console.log("Connected") + this.setState({wsConnected: true}) + console.log("WS connected") } this.conn.onclose = () => { - console.log("Connection closed") + this.setState({wsConnected: false}) + console.log("WS connection closed") window.setTimeout(this.connectWebsocket, 3000) } this.conn.onmessage = (evt) => { @@ -143,6 +151,20 @@ export default class Upload extends Component { } } + private triggerReboot = () => { + const res = confirm("Reboot the system?") + if (!res) return + + axios + .post(this.apiUrl + "/reboot") + .then(() => { + alert("System is rebooting") + }) + .catch((reason: any) => { + alert(String(reason)) + }) + } + private acceptUploads(): boolean { return !this.state.uploadStatus.uploading && !this.state.raucStatus.installing } @@ -188,27 +210,56 @@ export default class Upload extends Component { } return ( -
-
- {topText} -
- - +
+
+ {topText} +
+ - - - + + + + +
+ {bottomText} +
+
- {bottomText} + {this.state.wsConnected ? null : } + + {!this.state.raucStatus.installing && + this.state.raucStatus.percent === 100 && + this.state.raucStatus.last_error === "" ? ( + + ) : null} + + {this.state.uploadStatus.lastError ? ( + + ) : null} + {this.state.raucStatus.last_error ? ( + + ) : null}
) diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss index 38a43c7..426dadd 100644 --- a/ui/src/style/index.scss +++ b/ui/src/style/index.scss @@ -18,5 +18,5 @@ body { background-color: #673ab8; color: #fff; - font-size: 1.5em; + font-size: 1.2em; } From bc2df3accf326ae5ee9496f43dddade35e410f3c Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sun, 21 Nov 2021 15:43:48 +0100 Subject: [PATCH 8/8] add logo, reboot notice --- .air.toml | 32 +++++ .gitignore | 1 + README.md | 1 + src/rauc/rauc.go | 26 ++-- src/server/hub.go | 2 +- src/server/server.go | 19 +-- src/util/util.go | 8 +- src/util/util_test.go | 2 + ui/index.html | 2 +- ui/package.json | 2 + ui/pnpm-lock.yaml | 19 +++ ui/src/assets/favicon.svg | 18 +-- ui/src/assets/logo.svg | 3 + ui/src/assets/logo_border.svg | 3 + ui/src/assets/logo_square_src.svg | 95 +++++++++++++++ ui/src/assets/logo_src.svg | 111 ++++++++++++++++++ ui/src/components/Icon/Icon.scss | 22 ++++ .../ProgressCircle/ProgressCircle.scss | 6 +- ui/src/components/Upload/Alert.tsx | 29 ++--- ui/src/components/Upload/Reboot.tsx | 51 ++++++++ .../Upload/{Upload.scss => Updater.scss} | 9 +- .../Upload/{Upload.tsx => Updater.tsx} | 77 +++++------- ui/src/components/app.tsx | 6 +- ui/src/style/index.scss | 12 +- ui/src/util/apiUrls.ts | 12 ++ 25 files changed, 450 insertions(+), 118 deletions(-) create mode 100644 .air.toml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ui/src/assets/logo.svg create mode 100644 ui/src/assets/logo_border.svg create mode 100644 ui/src/assets/logo_square_src.svg create mode 100644 ui/src/assets/logo_src.svg create mode 100644 ui/src/components/Upload/Reboot.tsx rename ui/src/components/Upload/{Upload.scss => Updater.scss} (82%) rename ui/src/components/Upload/{Upload.tsx => Updater.tsx} (78%) create mode 100644 ui/src/util/apiUrls.ts 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/.gitignore b/.gitignore new file mode 100644 index 0000000..a9a5aec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp 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/src/rauc/rauc.go b/src/rauc/rauc.go index 7d2f6d7..fe465e4 100644 --- a/src/rauc/rauc.go +++ b/src/rauc/rauc.go @@ -19,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 } @@ -34,9 +34,21 @@ type RaucStatus struct { Log string `json:"log"` } +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) } @@ -61,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 @@ -94,7 +106,7 @@ func (r *Rauc) RunRauc(updateFile string) error { } if hasUpdate { - r.Broadcast <- r.GetStatusJson() + r.broadcast <- r.GetStatusJson() } } }() 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 ee2d50b..e29f1a0 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -11,6 +11,7 @@ import ( "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" @@ -33,14 +34,10 @@ 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 { @@ -65,6 +62,12 @@ func (srv *SEBRAUCServer) Run() error { 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()) diff --git a/src/util/util.go b/src/util/util.go index 147ca57..93062c8 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "fmt" "os" - "os/exec" "path/filepath" "strings" "time" @@ -65,6 +64,9 @@ func PurgeTmpdirs() (count int) { func Reboot(t time.Duration) { time.Sleep(t) - cmd := exec.Command("shutdown", "-r", "0") - _ = cmd.Run() + /* + 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 c616e40..f6a1f60 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -41,6 +41,8 @@ func TestDoesFileExist(t *testing.T) { } func TestTmpdir(t *testing.T) { + PurgeTmpdirs() + td, err := NewTmpdir() if err != nil { panic(err) diff --git a/ui/index.html b/ui/index.html index ec0528d..cf876c6 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,7 +4,7 @@ - Vite App + SEBRAUC
diff --git a/ui/package.json b/ui/package.json index ad6ef6c..5efdbac 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,10 +9,12 @@ "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", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 155f163..1110f3a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -3,7 +3,9 @@ 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 @@ -13,10 +15,12 @@ specifiers: 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 @@ -435,6 +439,13 @@ packages: 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: { @@ -509,6 +520,14 @@ packages: 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: { diff --git a/ui/src/assets/favicon.svg b/ui/src/assets/favicon.svg index de4aedd..cf2fd88 100644 --- a/ui/src/assets/favicon.svg +++ b/ui/src/assets/favicon.svg @@ -1,15 +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/Icon/Icon.scss b/ui/src/components/Icon/Icon.scss index d4796e7..7d64473 100644 --- a/ui/src/components/Icon/Icon.scss +++ b/ui/src/components/Icon/Icon.scss @@ -6,3 +6,25 @@ 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/ProgressCircle/ProgressCircle.scss b/ui/src/components/ProgressCircle/ProgressCircle.scss index 4878cb9..c410e1a 100644 --- a/ui/src/components/ProgressCircle/ProgressCircle.scss +++ b/ui/src/components/ProgressCircle/ProgressCircle.scss @@ -36,7 +36,11 @@ border-radius: 50%; &:hover { - background-color: #444; + background-color: #666; + } + + &:active { + background-color: #555; } } } diff --git a/ui/src/components/Upload/Alert.tsx b/ui/src/components/Upload/Alert.tsx index 2d6f9dd..6eb16a9 100644 --- a/ui/src/components/Upload/Alert.tsx +++ b/ui/src/components/Upload/Alert.tsx @@ -1,19 +1,13 @@ import {Component} from "preact" -import {mdiCheckCircleOutline, mdiTriangleOutline} from "@mdi/js" +import {mdiTriangleOutline} from "@mdi/js" import Icon from "../Icon/Icon" type Props = { - type: string source?: string message: string - onClick?: () => void } export default class Alert extends Component { - static defaultProps = { - type: "error", - } - private stripMessage(message: string): string { return message.replace(/^error:/i, "").trim() } @@ -24,21 +18,12 @@ export default class Alert extends Component { msg += this.stripMessage(this.props.message) return ( -

- {(() => { - switch (this.props.type) { - case "success": - return - default: - return - } - })()} - - {msg} -

+
+ + + {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/Upload.scss b/ui/src/components/Upload/Updater.scss similarity index 82% rename from ui/src/components/Upload/Upload.scss rename to ui/src/components/Upload/Updater.scss index c17e8ef..90b798e 100644 --- a/ui/src/components/Upload/Upload.scss +++ b/ui/src/components/Upload/Updater.scss @@ -19,6 +19,7 @@ align-items: center; padding: 15px 8px; + margin: 8px 0; text-align: center; @@ -31,17 +32,13 @@ } .alert { + display: flex; + justify-content: space-between; margin: 0.6em 0; - // border: 2px solid transparent; border-radius: 16px; .icon { margin-right: 0.4em; } - - &.clickable:hover { - cursor: pointer; - background: #ffffff66; - } } } diff --git a/ui/src/components/Upload/Upload.tsx b/ui/src/components/Upload/Updater.tsx similarity index 78% rename from ui/src/components/Upload/Upload.tsx rename to ui/src/components/Upload/Updater.tsx index d23387b..49057b1 100644 --- a/ui/src/components/Upload/Upload.tsx +++ b/ui/src/components/Upload/Updater.tsx @@ -1,11 +1,14 @@ 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 "./Upload.scss" +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 @@ -36,6 +39,8 @@ class RaucStatus { log = "" } +const byteSizeOptions = {units: "metric"} + type Props = {} type State = { @@ -45,28 +50,13 @@ type State = { wsConnected: boolean } -export default class Upload extends Component { - private apiUrl: string - private wsUrl: string - +export default class Updater extends Component { private dropzoneRef = createRef() private conn: WebSocket | undefined constructor(props?: Props | undefined, context?: any) { super(props, context) - // Get API urls - 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 - } - - this.apiUrl = `${httpProto}//${apiHost}/api` - this.wsUrl = `${wsProto}//${apiHost}/api/ws` - this.state = { uploadStatus: new UploadStatus(false), uploadFilename: "", @@ -96,7 +86,7 @@ export default class Upload extends Component { }) axios - .post(this.apiUrl + "/update", formData, { + .post(apiUrl + "/update", formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -123,7 +113,7 @@ export default class Upload extends Component { private connectWebsocket = () => { if (window.WebSocket) { - this.conn = new WebSocket(this.wsUrl) + this.conn = new WebSocket(wsUrl) this.conn.onopen = () => { this.setState({wsConnected: true}) console.log("WS connected") @@ -151,24 +141,18 @@ export default class Upload extends Component { } } - private triggerReboot = () => { - const res = confirm("Reboot the system?") - if (!res) return - - axios - .post(this.apiUrl + "/reboot") - .then(() => { - alert("System is rebooting") - }) - .catch((reason: any) => { - alert(String(reason)) - }) - } - 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( @@ -195,15 +179,20 @@ export default class Upload extends Component { 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} ${this.state.uploadStatus.loaded} / ${this.state.uploadStatus.total} bytes` + bottomText = this.state.uploadFilename + bottomText2 = `${byteSize(this.state.uploadStatus.loaded, byteSizeOptions)} + / + ${byteSize(this.state.uploadStatus.total, byteSizeOptions)}` } else if (this.state.raucStatus.installing) { - topText = "Upadating firmware" + topText = "Updating firmware" bottomText = this.state.raucStatus.message } else { topText = "Upload firmware package" @@ -211,9 +200,9 @@ export default class Upload extends Component { return (
-
+
- {topText} +

{topText}

{
- {bottomText} +

{bottomText}

+

{bottomText2}

{this.state.wsConnected ? null : } - {!this.state.raucStatus.installing && - this.state.raucStatus.percent === 100 && - this.state.raucStatus.last_error === "" ? ( - - ) : null} - {this.state.uploadStatus.lastError ? ( { /> ) : null}
+ {updateCompleted ? : null}
) } diff --git a/ui/src/components/app.tsx b/ui/src/components/app.tsx index 1ab3565..b4804c5 100644 --- a/ui/src/components/app.tsx +++ b/ui/src/components/app.tsx @@ -1,11 +1,13 @@ import {Component} from "preact" -import Upload from "./Upload/Upload" +import Updater from "./Upload/Updater" +import logo from "../assets/logo.svg" export default class App extends Component { render() { return (
- + +
) } diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss index 426dadd..5774a08 100644 --- a/ui/src/style/index.scss +++ b/ui/src/style/index.scss @@ -10,13 +10,17 @@ body { color: #444; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + background-color: #222; + color: #fff; } #app { - height: 100%; padding-top: 100px; - - background-color: #673ab8; - color: #fff; 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}