diff --git a/.drone.yml b/.drone.yml index f379b26..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 --timeout 5m + - golangci-lint run -v --timeout 2m - go test -v ./src/... diff --git a/.vscode/launch.json b/.vscode/launch.json index 1123069..69ed1d3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "SEBRAUC server", + "name": "Launch SEBRAUC server", "type": "go", "request": "launch", "mode": "auto", diff --git a/go.mod b/go.mod index dd949c7..7d1b400 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module code.thetadev.de/TSGRain/SEBRAUC go 1.16 require ( + code.thetadev.de/ThetaDev/gotry v0.3.2 github.com/davecgh/go-spew v1.1.1 // indirect github.com/gofiber/fiber/v2 v2.21.0 github.com/gofiber/websocket/v2 v2.0.12 diff --git a/go.sum b/go.sum index fe6435e..ef5350b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +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 a9332ce..4daeb3e 100644 --- a/openapi.yml +++ b/openapi.yml @@ -45,22 +45,6 @@ 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 ee2f352..3646a5d 100644 --- a/src/fixtures/testutil.go +++ b/src/fixtures/testutil.go @@ -3,6 +3,8 @@ package fixtures import ( "os" "path/filepath" + + "code.thetadev.de/ThetaDev/gotry/try" ) func doesFileExist(filepath string) bool { @@ -11,10 +13,7 @@ func doesFileExist(filepath string) bool { } func getProjectRoot() string { - p, err := os.Getwd() - if err != nil { - panic(err) - } + p := try.String(os.Getwd()) for i := 0; i < 10; i++ { if doesFileExist(filepath.Join(p, "go.mod")) { @@ -28,10 +27,7 @@ func getProjectRoot() string { func CdProjectRoot() { root := getProjectRoot() - err := os.Chdir(root) - if err != nil { - panic(err) - } + try.Check(os.Chdir(root)) } func GetTestfilesDir() string { diff --git a/src/fixtures/testutil_test.go b/src/fixtures/testutil_test.go index 44b32ef..8cca85c 100644 --- a/src/fixtures/testutil_test.go +++ b/src/fixtures/testutil_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "code.thetadev.de/ThetaDev/gotry/try" "github.com/stretchr/testify/assert" ) @@ -16,10 +17,7 @@ func TestGetProjectRoot(t *testing.T) { t.Run("subdir", func(t *testing.T) { root1 := getProjectRoot() - err := os.Chdir(filepath.Join(root1, "src/rauc")) - if err != nil { - panic(err) - } + try.Check(os.Chdir(filepath.Join(root1, "src/rauc"))) root := getProjectRoot() assert.True(t, doesFileExist(filepath.Join(root, "go.sum"))) @@ -28,10 +26,7 @@ func TestGetProjectRoot(t *testing.T) { func TestCdProjectRoot(t *testing.T) { CdProjectRoot() - err := os.Chdir("src/rauc") - if err != nil { - panic(err) - } + try.Check(os.Chdir("src/rauc")) CdProjectRoot() assert.True(t, doesFileExist("go.sum")) } diff --git a/src/rauc/rauc.go b/src/rauc/rauc.go index 7d2f6d7..246f308 100644 --- a/src/rauc/rauc.go +++ b/src/rauc/rauc.go @@ -4,7 +4,6 @@ import ( "bufio" "encoding/json" "fmt" - "os" "os/exec" "regexp" "strconv" @@ -34,11 +33,9 @@ type RaucStatus struct { Log string `json:"log"` } -func (r *Rauc) completed(updateFile string) { +func (r *Rauc) completed() { r.status.Installing = false r.Broadcast <- r.GetStatusJson() - - _ = os.Remove(updateFile) } func (r *Rauc) RunRauc(updateFile string) error { @@ -101,7 +98,7 @@ func (r *Rauc) RunRauc(updateFile string) error { err := cmd.Start() if err != nil { - r.completed(updateFile) + r.completed() return err } @@ -110,7 +107,7 @@ func (r *Rauc) RunRauc(updateFile string) error { if err != nil { fmt.Printf("RAUC failed with %s\n", err) } - r.completed(updateFile) + r.completed() }() return nil diff --git a/src/server/server.go b/src/server/server.go index ee2d50b..63ef036 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -2,10 +2,8 @@ package server import ( "errors" - "fmt" "net/http" "strings" - "time" "code.thetadev.de/TSGRain/SEBRAUC/src/rauc" "code.thetadev.de/TSGRain/SEBRAUC/src/util" @@ -21,8 +19,6 @@ type SEBRAUCServer struct { address string raucUpdater *rauc.Rauc hub *MessageHub - tmpdir string - currentId int } type statusMessage struct { @@ -37,30 +33,23 @@ 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, } - 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, - DisableStartupMessage: true, + AppName: "SEBRAUC", + BodyLimit: 1024 * 1024 * 1024, + ErrorHandler: errorHandler, }) app.Use(logger.New()) @@ -86,9 +75,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() @@ -101,16 +90,12 @@ func (srv *SEBRAUCServer) controllerUpdate(c *fiber.Ctx) error { if err != nil { return err } - - srv.currentId++ - updateFile := fmt.Sprintf("%s/update_%d.raucb", srv.tmpdir, srv.currentId) - - err = c.SaveFile(file, updateFile) + err = c.SaveFile(file, "./update.raucb") if err != nil { return err } - err = srv.raucUpdater.RunRauc(updateFile) + err = srv.raucUpdater.RunRauc("./update.raucb") if err == nil { writeStatus(c, true, "Update started") } else if errors.Is(err, util.ErrAlreadyRunning) { @@ -127,10 +112,15 @@ func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error { return nil } -func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error { - go util.Reboot(5 * time.Second) - - writeStatus(c, true, "System is rebooting") +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 + } return nil } @@ -138,6 +128,7 @@ 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 147ca57..486b648 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -1,70 +1,8 @@ package util -import ( - "crypto/rand" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" -) - -const tmpdirPrefix = "sebrauc" +import "os" 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 c616e40..30b2ab0 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -1,12 +1,9 @@ package util import ( - "os" - "path/filepath" "testing" "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" - "github.com/stretchr/testify/assert" ) func TestDoesFileExist(t *testing.T) { @@ -39,30 +36,3 @@ 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/.env.development b/ui/.env.development deleted file mode 100644 index 029df76..0000000 --- a/ui/.env.development +++ /dev/null @@ -1 +0,0 @@ -VITE_API_HOST=127.0.0.1:8080 diff --git a/ui/package.json b/ui/package.json index ad6ef6c..3db08ef 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,8 +7,6 @@ "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 155f163..05b5ef9 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -1,9 +1,7 @@ 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 @@ -11,8 +9,6 @@ specifiers: vite: ^2.6.14 dependencies: - "@mdi/js": 6.5.95 - axios: 0.24.0 preact: 10.5.15 devDependencies: @@ -351,13 +347,6 @@ 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: { @@ -456,17 +445,6 @@ 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: { @@ -834,19 +812,6 @@ 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 deleted file mode 100644 index 9e705ae..0000000 --- a/ui/src/components/Dropzone/Dropzone.scss +++ /dev/null @@ -1,9 +0,0 @@ -.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 deleted file mode 100644 index c977dd4..0000000 --- a/ui/src/components/Dropzone/Dropzone.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import {Component, createRef, JSX, ComponentChild, ComponentChildren} from "preact" -import "./Dropzone.scss" - -type Props = { - disabled: boolean - clickable: boolean - multiple: boolean - accept?: string - onFilesAdded?: (files: File[]) => void - - children?: ComponentChild | ComponentChildren -} - -type State = { - highlight: boolean -} - -export default class Dropzone extends Component { - static defaultProps = { - disabled: false, - clickable: false, - multiple: false, - } - - private fileInputRef = createRef() - - openFileDialog() { - this.fileInputRef.current?.click() - } - - reset() { - const input = this.fileInputRef.current - if (input === null) return - input.value = "" - } - - private onClick = () => { - if (this.props.disabled || !this.props.clickable) return - this.openFileDialog() - } - - private onFilesAdded = (evt: JSX.TargetedEvent) => { - if (this.props.disabled || evt.target === null) return - - const input = evt.target as HTMLInputElement - const files = input.files - - if (this.props.onFilesAdded) { - const array = this.fileListToArray(files) - this.props.onFilesAdded(array) - } - } - - private onDragOver = (evt: DragEvent) => { - evt.preventDefault() - - if (this.props.disabled) return - - this.setState({highlight: true}) - } - - private onDragLeave = () => { - this.setState({highlight: false}) - } - - private onDrop = (evt: DragEvent) => { - evt.preventDefault() - this.setState({highlight: false}) - - if (this.props.disabled || evt.dataTransfer === null) return - - const files = evt.dataTransfer.files - - if (!this.props.multiple && files.length > 1) return - - if (this.props.onFilesAdded) { - const array = this.fileListToArray(files) - this.props.onFilesAdded(array) - } - } - - private fileListToArray(list: FileList | null): File[] { - const array: File[] = [] - if (list === null) return array - - for (var i = 0; i < list.length; i++) { - array.push(list.item(i)!) - } - return array - } - - render() { - return ( -
- - {this.props.children} -
- ) - } -} diff --git a/ui/src/components/Icon/Icon.scss b/ui/src/components/Icon/Icon.scss deleted file mode 100644 index d4796e7..0000000 --- a/ui/src/components/Icon/Icon.scss +++ /dev/null @@ -1,8 +0,0 @@ -.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 deleted file mode 100644 index 824f56c..0000000 --- a/ui/src/components/Icon/Icon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {Component} from "preact" -import "./Icon.scss" - -type Props = { - icon: string - size: number - color?: string -} - -export default class Icon extends Component { - static defaultProps = { - size: 24, - } - - render() { - return ( - - - - ) - } -} diff --git a/ui/src/components/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/ProgressCircle/ProgressCircle.scss b/ui/src/components/ProgressCircle/ProgressCircle.scss deleted file mode 100644 index 4878cb9..0000000 --- a/ui/src/components/ProgressCircle/ProgressCircle.scss +++ /dev/null @@ -1,42 +0,0 @@ -.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 deleted file mode 100644 index 507dcd5..0000000 --- a/ui/src/components/ProgressCircle/ProgressCircle.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {Component, ComponentChild, ComponentChildren} from "preact" -import "./ProgressCircle.scss" - -type Props = { - ready: boolean - progress: number - color: string - - children?: ComponentChild | ComponentChildren -} - -export default class ProgressCircle extends Component { - static defaultProps = { - ready: false, - progress: 0, - color: "#FDB900", - } - - render() { - const percentage = this.props.ready ? 0 : this.props.progress - const visible = !this.props.ready && this.props.progress > 0 - - return ( -
- - - - {visible ? ( - - ) : null} - - {visible ? ( - - {percentage}% - - ) : null} - - {visible ? null : this.props.children} -
- ) - } -} diff --git a/ui/src/components/Upload/Alert.tsx b/ui/src/components/Upload/Alert.tsx deleted file mode 100644 index 2d6f9dd..0000000 --- a/ui/src/components/Upload/Alert.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import {Component} from "preact" -import {mdiCheckCircleOutline, 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() - } - - 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 deleted file mode 100644 index c17e8ef..0000000 --- a/ui/src/components/Upload/Upload.scss +++ /dev/null @@ -1,47 +0,0 @@ -.uploader { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - margin: 0 auto; - max-width: 500px; - width: 90%; - - > * { - width: 100%; - } - - .card { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - padding: 15px 8px; - - 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 deleted file mode 100644 index d23387b..0000000 --- a/ui/src/components/Upload/Upload.tsx +++ /dev/null @@ -1,267 +0,0 @@ -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, lastError = "") { - this.uploading = uploading - this.total = total - this.loaded = loaded - this.lastError = lastError - } - - static fromProgressEvent(progressEvent: { - loaded: number - total: number - }): UploadStatus { - return new UploadStatus(true, progressEvent.total, progressEvent.loaded) - } -} - -class RaucStatus { - installing = false - percent = 0 - message = "" - last_error = "" - log = "" -} - -type Props = {} - -type State = { - uploadStatus: UploadStatus - uploadFilename: string - raucStatus: RaucStatus - wsConnected: boolean -} - -export default class Upload extends Component { - private apiUrl: string - private wsUrl: string - - 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: "", - raucStatus: new RaucStatus(), - wsConnected: false, - } - - this.connectWebsocket() - } - - private buttonClick = () => { - if (!this.acceptUploads()) return - - this.dropzoneRef.current?.openFileDialog() - } - - private onFilesAdded = (files: File[]) => { - if (files.length === 0) return - const newFile = files[0] - - const formData = new FormData() - formData.append("updateFile", newFile) - - this.setState({ - uploadStatus: new UploadStatus(true, newFile.size, 0), - uploadFilename: newFile.name, - }) - - axios - .post(this.apiUrl + "/update", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - onUploadProgress: (progressEvent: {loaded: number; total: number}) => { - this.setState({ - uploadStatus: UploadStatus.fromProgressEvent(progressEvent), - }) - }, - }) - .then(() => { - this.resetUpload() - }) - .catch((reason: any) => { - this.resetUpload(String(reason)) - }) - } - - private resetUpload = (lastError = "") => { - this.setState({ - uploadStatus: new UploadStatus(false, 0, 0, lastError), - }) - this.dropzoneRef.current?.reset() - } - - private connectWebsocket = () => { - if (window.WebSocket) { - this.conn = new WebSocket(this.wsUrl) - this.conn.onopen = () => { - this.setState({wsConnected: true}) - console.log("WS connected") - } - this.conn.onclose = () => { - this.setState({wsConnected: false}) - console.log("WS connection closed") - window.setTimeout(this.connectWebsocket, 3000) - } - this.conn.onmessage = (evt) => { - var messages = evt.data.split("\n") - for (var i = 0; i < messages.length; i++) { - this.setState({ - raucStatus: Object.assign( - new RaucStatus(), - JSON.parse(messages[i]) - ), - }) - - console.log(this.state.raucStatus) - } - } - } else { - console.log("Your browser does not support WebSockets") - } - } - - private 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 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} -
-
-
- {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/components/app.tsx b/ui/src/components/app.tsx index 1ab3565..bb17383 100644 --- a/ui/src/components/app.tsx +++ b/ui/src/components/app.tsx @@ -1,12 +1,53 @@ -import {Component} from "preact" -import Upload from "./Upload/Upload" +import LoadingText from "./LoadingText" +import {useState} from "preact/hooks" -export default class App extends Component { - render() { +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/preact.d.ts b/ui/src/preact.d.ts index ac79d62..edeaac5 100644 --- a/ui/src/preact.d.ts +++ b/ui/src/preact.d.ts @@ -1 +1,2 @@ +// eslint-disable-next-line import JSX = preact.JSX diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss index 426dadd..6b25e27 100644 --- a/ui/src/style/index.scss +++ b/ui/src/style/index.scss @@ -12,11 +12,19 @@ body { -moz-osx-font-smoothing: grayscale; } +* { + box-sizing: border-box; +} + #app { height: 100%; - padding-top: 100px; - + text-align: center; background-color: #673ab8; color: #fff; - font-size: 1.2em; + font-size: 1.5em; + padding-top: 100px; + + .link { + color: #fff; + } }