Compare commits
3 commits
3e29e04ac3
...
44001bb7e7
Author | SHA1 | Date | |
---|---|---|---|
44001bb7e7 | |||
7465ef3380 | |||
85c0073651 |
31 changed files with 1670 additions and 59 deletions
3
Makefile
3
Makefile
|
@ -17,6 +17,9 @@ build-server:
|
|||
|
||||
build: build-ui build-server
|
||||
|
||||
generate-apiclient:
|
||||
openapi-generator generate -i openapi.yml -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true"
|
||||
|
||||
clean:
|
||||
rm -f build/*
|
||||
rm -rf ${UI_DIR}/dist/**
|
||||
|
|
1
go.sum
1
go.sum
|
@ -19,6 +19,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
43
openapi.yml
43
openapi.yml
|
@ -118,6 +118,10 @@ components:
|
|||
SystemInfo:
|
||||
type: object
|
||||
properties:
|
||||
hostname:
|
||||
description: Hostname of the system
|
||||
type: string
|
||||
example: "raspberrypi3"
|
||||
os_name:
|
||||
description: Name of the os distribution
|
||||
type: string
|
||||
|
@ -138,21 +142,21 @@ components:
|
|||
description: Compatible firmware variant
|
||||
type: string
|
||||
example: "rpi-prod"
|
||||
rauc_booted:
|
||||
description: Currently booted rootfs
|
||||
type: string
|
||||
example: "rootfs.0"
|
||||
rauc_boot_primary:
|
||||
description: Primary rootfs to boot from
|
||||
type: string
|
||||
example: "rootfs.1"
|
||||
rauc_rootfs:
|
||||
description: List of RAUC root filesystems
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/RaucFS"
|
||||
$ref: "#/components/schemas/Rootfs"
|
||||
required:
|
||||
- hostname
|
||||
- os_name
|
||||
- os_version
|
||||
- uptime
|
||||
- rauc_compatible
|
||||
- rauc_variant
|
||||
- rauc_rootfs
|
||||
|
||||
RaucFS:
|
||||
Rootfs:
|
||||
type: object
|
||||
properties:
|
||||
device:
|
||||
|
@ -163,11 +167,6 @@ components:
|
|||
description: Filesystem
|
||||
type: string
|
||||
example: ext4
|
||||
state:
|
||||
description: Current state of filesystem
|
||||
type: string
|
||||
enum: [active, inactive, booted]
|
||||
example: booted
|
||||
mountpoint:
|
||||
description: Mount path (null when not mounted)
|
||||
type: string
|
||||
|
@ -176,6 +175,19 @@ components:
|
|||
bootable:
|
||||
description: "Is the filesystem bootable"
|
||||
type: boolean
|
||||
booted:
|
||||
description: "Is the filesystem booted"
|
||||
type: boolean
|
||||
primary:
|
||||
description: "Is the filesystem the next boot target"
|
||||
type: boolean
|
||||
required:
|
||||
- device
|
||||
- type
|
||||
- mountpoint
|
||||
- bootable
|
||||
- booted
|
||||
- primary
|
||||
|
||||
StatusMessage:
|
||||
type: object
|
||||
|
@ -188,4 +200,5 @@ components:
|
|||
type: string
|
||||
example: Update started
|
||||
required:
|
||||
- success
|
||||
- msg
|
||||
|
|
|
@ -41,22 +41,34 @@ LastError: Failed to check bundle identifier: Invalid identifier. ` +
|
|||
idle
|
||||
Installing ` + "/app/demo` failed"
|
||||
|
||||
const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` +
|
||||
`"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` +
|
||||
`"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` +
|
||||
`"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` +
|
||||
`{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` +
|
||||
`"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}`
|
||||
|
||||
func printLinesWithDelay(lines string) {
|
||||
for _, line := range strings.Split(lines, "\n") {
|
||||
fmt.Println(line)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
arg := ""
|
||||
if len(os.Args) > 1 {
|
||||
arg = os.Args[1]
|
||||
}
|
||||
|
||||
var lines string
|
||||
switch arg {
|
||||
case "fail":
|
||||
lines = outputFailure
|
||||
printLinesWithDelay(outputFailure)
|
||||
case "install":
|
||||
printLinesWithDelay(outputSuccess)
|
||||
case "status":
|
||||
fmt.Println(statusJson)
|
||||
default:
|
||||
lines = outputSuccess
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(lines, "\n") {
|
||||
fmt.Println(line)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
5
src/fixtures/testfiles/os-release
Normal file
5
src/fixtures/testfiles/os-release
Normal file
|
@ -0,0 +1,5 @@
|
|||
ID=tsgrain
|
||||
NAME="TSGRain distro"
|
||||
VERSION="0.0.1"
|
||||
VERSION_ID=0.0.1
|
||||
PRETTY_NAME="TSGRain distro 0.0.1"
|
|
@ -70,7 +70,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
|||
}
|
||||
r.broadcast <- r.GetStatusJson()
|
||||
|
||||
cmd := util.CommandFromString(fmt.Sprintf("%s %s", util.UpdateCmd, updateFile))
|
||||
cmd := util.CommandFromString(fmt.Sprintf("%s install %s", util.RaucCmd, updateFile))
|
||||
|
||||
readPipe, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo"
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/ui"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
@ -88,6 +89,7 @@ func (srv *SEBRAUCServer) Run() error {
|
|||
app.Get("/api/ws", websocket.New(srv.hub.Handler))
|
||||
app.Post("/api/update", srv.controllerUpdate)
|
||||
app.Get("/api/status", srv.controllerStatus)
|
||||
app.Get("/api/info", srv.controllerInfo)
|
||||
app.Post("/api/reboot", srv.controllerReboot)
|
||||
|
||||
// Start messaging hub
|
||||
|
@ -131,6 +133,17 @@ func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (srv *SEBRAUCServer) controllerInfo(c *fiber.Ctx) error {
|
||||
info, err := sysinfo.GetSysinfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Context().SetStatusCode(200)
|
||||
_ = c.JSON(info)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error {
|
||||
go util.Reboot(5 * time.Second)
|
||||
|
||||
|
|
180
src/sysinfo/sysinfo.go
Normal file
180
src/sysinfo/sysinfo.go
Normal file
|
@ -0,0 +1,180 @@
|
|||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OsName string `json:"os_name"`
|
||||
OsVersion string `json:"os_version"`
|
||||
Uptime int `json:"uptime"`
|
||||
RaucCompatible string `json:"rauc_compatible"`
|
||||
RaucVariant string `json:"rauc_variant"`
|
||||
RaucRootfs map[string]Rootfs `json:"rauc_rootfs"`
|
||||
}
|
||||
|
||||
type Rootfs struct {
|
||||
Device string `json:"device"`
|
||||
Type string `json:"type"`
|
||||
bootname string
|
||||
Mountpoint *string `json:"mountpoint"`
|
||||
Bootable bool `json:"bootable"`
|
||||
Booted bool `json:"booted"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
type raucInfo struct {
|
||||
Compatible string `json:"compatible"`
|
||||
Variant string `json:"variant"`
|
||||
Booted string `json:"booted"`
|
||||
BootPrimary string `json:"boot_primary"`
|
||||
Slots []map[string]raucFS `json:"slots"`
|
||||
}
|
||||
|
||||
type raucFS struct {
|
||||
Class string `json:"class"`
|
||||
Device string `json:"device"`
|
||||
Type string `json:"type"`
|
||||
Bootname string `json:"bootname"`
|
||||
State string `json:"state"`
|
||||
Mountpoint *string `json:"mountpoint"`
|
||||
BootStatus string `json:"boot_status"`
|
||||
}
|
||||
|
||||
type osRelease struct {
|
||||
OsName string `json:"os_name"`
|
||||
OsVersion string `json:"os_version"`
|
||||
}
|
||||
|
||||
var (
|
||||
rexpOsName = regexp.MustCompile(`(?m)^NAME="(.+)"`)
|
||||
rexpOsVersion = regexp.MustCompile(`(?m)^VERSION="(.+)"`)
|
||||
rexpUptime = regexp.MustCompile(`^\d+`)
|
||||
)
|
||||
|
||||
func parseRaucInfo(raucInfoJson []byte) (raucInfo, error) {
|
||||
res := raucInfo{}
|
||||
err := json.Unmarshal(raucInfoJson, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func parseOsRelease(osReleaseFile string) (osRelease, error) {
|
||||
osReleaseTxt, err := os.ReadFile(osReleaseFile)
|
||||
if err != nil {
|
||||
return osRelease{}, err
|
||||
}
|
||||
|
||||
nameMatch := rexpOsName.FindSubmatch(osReleaseTxt)
|
||||
versionMatch := rexpOsVersion.FindSubmatch(osReleaseTxt)
|
||||
|
||||
name := ""
|
||||
if nameMatch != nil {
|
||||
name = string(nameMatch[1])
|
||||
}
|
||||
|
||||
version := ""
|
||||
if versionMatch != nil {
|
||||
version = string(versionMatch[1])
|
||||
}
|
||||
|
||||
return osRelease{
|
||||
OsName: name,
|
||||
OsVersion: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapRootfs(rinf raucInfo) map[string]Rootfs {
|
||||
res := make(map[string]Rootfs)
|
||||
|
||||
for _, slot := range rinf.Slots {
|
||||
for name, fs := range slot {
|
||||
if fs.Class == "rootfs" {
|
||||
res[name] = Rootfs{
|
||||
Device: fs.Device,
|
||||
Type: fs.Type,
|
||||
bootname: fs.Bootname,
|
||||
Mountpoint: fs.Mountpoint,
|
||||
Bootable: fs.BootStatus == "good",
|
||||
Booted: fs.State == "booted",
|
||||
Primary: rinf.BootPrimary == name,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func getFSNameFromBootname(rfslist map[string]Rootfs, bootname string) string {
|
||||
for name, rfs := range rfslist {
|
||||
if rfs.bootname == bootname {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return "n/a"
|
||||
}
|
||||
|
||||
func mapSysinfo(rinf raucInfo, osr osRelease, uptime int, hostname string) SystemInfo {
|
||||
rfslist := mapRootfs(rinf)
|
||||
|
||||
return SystemInfo{
|
||||
Hostname: hostname,
|
||||
OsName: osr.OsName,
|
||||
OsVersion: osr.OsVersion,
|
||||
Uptime: uptime,
|
||||
RaucCompatible: rinf.Compatible,
|
||||
RaucVariant: rinf.Variant,
|
||||
RaucRootfs: rfslist,
|
||||
}
|
||||
}
|
||||
|
||||
func getUptime() (int, error) {
|
||||
uptimeRaw, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
uptimeChars := rexpUptime.Find(uptimeRaw)
|
||||
return strconv.Atoi(string(uptimeChars))
|
||||
}
|
||||
|
||||
func getHostname() string {
|
||||
hostname, err := os.ReadFile("/etc/hostname")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(hostname))
|
||||
}
|
||||
|
||||
func GetSysinfo() (SystemInfo, error) {
|
||||
cmd := util.CommandFromString(util.RaucCmd + " status")
|
||||
rinfJson, err := cmd.Output()
|
||||
if err != nil {
|
||||
return SystemInfo{}, err
|
||||
}
|
||||
|
||||
rinf, err := parseRaucInfo(rinfJson)
|
||||
if err != nil {
|
||||
return SystemInfo{}, err
|
||||
}
|
||||
|
||||
osinf, err := parseOsRelease("/etc/os-release")
|
||||
if err != nil {
|
||||
return SystemInfo{}, err
|
||||
}
|
||||
|
||||
uptime, err := getUptime()
|
||||
if err != nil {
|
||||
return SystemInfo{}, err
|
||||
}
|
||||
|
||||
hostname := getHostname()
|
||||
|
||||
return mapSysinfo(rinf, osinf, uptime, hostname), nil
|
||||
}
|
122
src/sysinfo/sysinfo_test.go
Normal file
122
src/sysinfo/sysinfo_test.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package sysinfo
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` +
|
||||
`"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` +
|
||||
`"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` +
|
||||
`"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` +
|
||||
`{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` +
|
||||
`"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}`
|
||||
|
||||
var mountRoot = "/"
|
||||
|
||||
var expectedRaucInfo = raucInfo{
|
||||
Compatible: "TSGRain",
|
||||
Variant: "dev",
|
||||
Booted: "A",
|
||||
BootPrimary: "rootfs.0",
|
||||
Slots: []map[string]raucFS{
|
||||
{
|
||||
"rootfs.1": {
|
||||
Class: "rootfs",
|
||||
Device: "/dev/mmcblk0p3",
|
||||
Type: "ext4",
|
||||
Bootname: "B",
|
||||
State: "inactive",
|
||||
Mountpoint: nil,
|
||||
BootStatus: "good",
|
||||
},
|
||||
},
|
||||
{
|
||||
"rootfs.0": {
|
||||
Class: "rootfs",
|
||||
Device: "/dev/mmcblk0p2",
|
||||
Type: "ext4",
|
||||
Bootname: "A",
|
||||
State: "booted",
|
||||
Mountpoint: &mountRoot,
|
||||
BootStatus: "good",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var expectedRootfsList = map[string]Rootfs{
|
||||
"rootfs.0": {
|
||||
Device: "/dev/mmcblk0p2",
|
||||
Type: "ext4",
|
||||
bootname: "A",
|
||||
Mountpoint: &mountRoot,
|
||||
Bootable: true,
|
||||
Booted: true,
|
||||
Primary: true,
|
||||
},
|
||||
"rootfs.1": {
|
||||
Device: "/dev/mmcblk0p3",
|
||||
Type: "ext4",
|
||||
bootname: "B",
|
||||
Mountpoint: nil,
|
||||
Bootable: true,
|
||||
Booted: false,
|
||||
Primary: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseRaucInfo(t *testing.T) {
|
||||
info, err := parseRaucInfo([]byte(statusJson))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRaucInfo, info)
|
||||
}
|
||||
|
||||
func TestParseOsRelease(t *testing.T) {
|
||||
testfiles := fixtures.GetTestfilesDir()
|
||||
osReleaseFile := filepath.Join(testfiles, "os-release")
|
||||
|
||||
osRel, err := parseOsRelease(osReleaseFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
expected := osRelease{
|
||||
OsName: "TSGRain distro",
|
||||
OsVersion: "0.0.1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, osRel)
|
||||
}
|
||||
|
||||
func TestMapRootfsList(t *testing.T) {
|
||||
rootfsList := mapRootfs(expectedRaucInfo)
|
||||
|
||||
assert.Equal(t, expectedRootfsList, rootfsList)
|
||||
}
|
||||
|
||||
func TestGetFSNameFromBootname(t *testing.T) {
|
||||
rootfsList := mapRootfs(expectedRaucInfo)
|
||||
|
||||
assert.Equal(t, "rootfs.0", getFSNameFromBootname(rootfsList, "A"))
|
||||
assert.Equal(t, "rootfs.1", getFSNameFromBootname(rootfsList, "B"))
|
||||
assert.Equal(t, "n/a", getFSNameFromBootname(rootfsList, "C"))
|
||||
}
|
||||
|
||||
func TestGetSysinfo(t *testing.T) {
|
||||
sysinfo, err := GetSysinfo()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Greater(t, sysinfo.Uptime, 0)
|
||||
assert.Equal(t, "TSGRain", sysinfo.RaucCompatible)
|
||||
assert.Equal(t, "dev", sysinfo.RaucVariant)
|
||||
assert.Equal(t, expectedRootfsList, sysinfo.RaucRootfs)
|
||||
}
|
|
@ -5,7 +5,7 @@ package util
|
|||
|
||||
const (
|
||||
RebootCmd = "shutdown -r 0"
|
||||
UpdateCmd = "rauc install"
|
||||
RaucCmd = "rauc"
|
||||
|
||||
TestMode = false
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ package util
|
|||
|
||||
const (
|
||||
RebootCmd = "touch /tmp/sebrauc_reboot_test"
|
||||
UpdateCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock"
|
||||
RaucCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock"
|
||||
|
||||
TestMode = true
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {Component} from "preact"
|
||||
import {mdiTriangleOutline} from "@mdi/js"
|
||||
import Icon from "../Icon/Icon"
|
||||
import colors from "../../util/colors"
|
||||
|
||||
type Props = {
|
||||
source?: string
|
||||
|
@ -20,7 +21,7 @@ export default class Alert extends Component<Props> {
|
|||
return (
|
||||
<div class="alert">
|
||||
<span>
|
||||
<Icon icon={mdiTriangleOutline} color="#FF0039" />
|
||||
<Icon icon={mdiTriangleOutline} color={colors.RED} />
|
||||
{msg}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js"
|
||||
import axios, {AxiosError, AxiosResponse} from "axios"
|
||||
import {Component} from "preact"
|
||||
import {apiUrl} from "../../util/apiUrls"
|
||||
import {sebraucApi} from "../../util/apiUrls"
|
||||
import Icon from "../Icon/Icon"
|
||||
|
||||
export default class Reboot extends Component {
|
||||
|
@ -9,9 +8,9 @@ export default class Reboot extends Component {
|
|||
const res = confirm("Reboot the system?")
|
||||
if (!res) return
|
||||
|
||||
axios
|
||||
.post(apiUrl + "/reboot")
|
||||
.then((response: AxiosResponse) => {
|
||||
sebraucApi
|
||||
.startReboot()
|
||||
.then((response) => {
|
||||
const msg = response.data.msg
|
||||
|
||||
if (msg !== undefined) {
|
||||
|
@ -20,7 +19,7 @@ export default class Reboot extends Component {
|
|||
alert("No response")
|
||||
}
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
const msg = error.response.data.msg
|
||||
|
||||
|
|
159
ui/src/components/Updater/SysinfoCard.tsx
Normal file
159
ui/src/components/Updater/SysinfoCard.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import {Component} from "preact"
|
||||
import {SystemInfo} from "../../sebrauc-client"
|
||||
import {sebraucApi} from "../../util/apiUrls"
|
||||
import {secondsToString} from "../../util/functions"
|
||||
import Icon from "../Icon/Icon"
|
||||
import {
|
||||
mdiAlphaVCircleOutline,
|
||||
mdiCheckCircleOutline,
|
||||
mdiCircleOutline,
|
||||
mdiClockOutline,
|
||||
mdiCloseCircleOutline,
|
||||
mdiMonitor,
|
||||
mdiPenguin,
|
||||
mdiTagMultipleOutline,
|
||||
mdiTagOutline,
|
||||
} from "@mdi/js"
|
||||
import colors from "../../util/colors"
|
||||
|
||||
type Props = {}
|
||||
|
||||
type State = {
|
||||
sysinfo: SystemInfo
|
||||
}
|
||||
|
||||
export default class SysinfoCard extends Component<Props, State> {
|
||||
constructor(props?: Props | undefined, context?: any) {
|
||||
super(props, context)
|
||||
this.fetchInfo()
|
||||
}
|
||||
|
||||
private fetchInfo = () => {
|
||||
sebraucApi
|
||||
.getInfo()
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
this.setState({sysinfo: response.data})
|
||||
} else {
|
||||
console.log("error fetching info", response.data)
|
||||
console.log("error fetching info", response.data)
|
||||
window.setTimeout(this.fetchInfo, 3000)
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
console.log("error fetching info", reason)
|
||||
window.setTimeout(this.fetchInfo, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
private renderSysinfo() {
|
||||
return (
|
||||
<div>
|
||||
<div className="card">
|
||||
<p class="top">System information</p>
|
||||
<table class="table no-bottom-border">
|
||||
<tr>
|
||||
<td>
|
||||
<Icon icon={mdiMonitor} /> Hostname
|
||||
</td>
|
||||
<td>{this.state.sysinfo.hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Icon icon={mdiPenguin} /> Operating system
|
||||
</td>
|
||||
<td>{this.state.sysinfo.os_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Icon icon={mdiAlphaVCircleOutline} /> OS version
|
||||
</td>
|
||||
<td>{this.state.sysinfo.os_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Icon icon={mdiClockOutline} /> Uptime
|
||||
</td>
|
||||
<td>{secondsToString(this.state.sysinfo.uptime)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Icon icon={mdiTagOutline} /> Compatible FW
|
||||
</td>
|
||||
<td>{this.state.sysinfo.rauc_compatible}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Icon icon={mdiTagMultipleOutline} /> Compatible FW
|
||||
variant
|
||||
</td>
|
||||
<td>{this.state.sysinfo.rauc_variant}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<p class="top">Rootfs slots</p>
|
||||
<div class="table-wrapper">
|
||||
<table class="table no-bottom-border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Device</th>
|
||||
<th>Mountpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(this.state.sysinfo.rauc_rootfs).map(
|
||||
(k, i) => {
|
||||
const rfs = this.state.sysinfo.rauc_rootfs[k]
|
||||
let icon = mdiCircleOutline
|
||||
let iconColor = colors.BLUE
|
||||
|
||||
if (!rfs.bootable) {
|
||||
icon = mdiCloseCircleOutline
|
||||
iconColor = colors.RED
|
||||
} else if (rfs.primary) {
|
||||
icon = mdiCheckCircleOutline
|
||||
iconColor = colors.GREEN
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<Icon
|
||||
icon={icon}
|
||||
color={iconColor}
|
||||
/>
|
||||
</td>
|
||||
<td>{k}</td>
|
||||
<td>{rfs.device}</td>
|
||||
<td>{rfs.mountpoint}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderLoadingAnimation() {
|
||||
return (
|
||||
<div className="card">
|
||||
<p>loading sysinfo...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.sysinfo) {
|
||||
return this.renderSysinfo()
|
||||
}
|
||||
return this.renderLoadingAnimation()
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
.uploader {
|
||||
.updater-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 0 auto;
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
|
||||
> * {
|
||||
|
@ -18,8 +18,8 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding: 15px 8px;
|
||||
margin: 8px 0;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
|
@ -29,6 +29,14 @@
|
|||
.top {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
&.pad {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
|
@ -42,3 +50,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-top-right {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ import Dropzone from "../Dropzone/Dropzone"
|
|||
import ProgressCircle from "../ProgressCircle/ProgressCircle"
|
||||
import Icon from "../Icon/Icon"
|
||||
import "./Updater.scss"
|
||||
import axios from "axios"
|
||||
import Alert from "./Alert"
|
||||
import Reboot from "./Reboot"
|
||||
import {apiUrl, wsUrl} from "../../util/apiUrls"
|
||||
import {sebraucApi, wsUrl} from "../../util/apiUrls"
|
||||
import colors from "../../util/colors"
|
||||
|
||||
class UploadStatus {
|
||||
uploading = false
|
||||
|
@ -50,7 +50,7 @@ type State = {
|
|||
wsConnected: boolean
|
||||
}
|
||||
|
||||
export default class Updater extends Component<Props, State> {
|
||||
export default class UpdaterCard extends Component<Props, State> {
|
||||
private dropzoneRef = createRef<Dropzone>()
|
||||
private conn: WebSocket | undefined
|
||||
|
||||
|
@ -77,19 +77,13 @@ export default class Updater extends Component<Props, State> {
|
|||
if (files.length === 0) return
|
||||
const newFile = files[0]
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("updateFile", newFile)
|
||||
|
||||
this.setState({
|
||||
uploadStatus: new UploadStatus(true, newFile.size, 0),
|
||||
uploadFilename: newFile.name,
|
||||
})
|
||||
|
||||
axios
|
||||
.post(apiUrl + "/update", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
sebraucApi
|
||||
.startUpdate(newFile, {
|
||||
onUploadProgress: (progressEvent: {loaded: number; total: number}) => {
|
||||
this.setState({
|
||||
uploadStatus: UploadStatus.fromProgressEvent(progressEvent),
|
||||
|
@ -132,8 +126,6 @@ export default class Updater extends Component<Props, State> {
|
|||
JSON.parse(messages[i])
|
||||
),
|
||||
})
|
||||
|
||||
console.log(this.state.raucStatus)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -163,9 +155,9 @@ export default class Updater extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private circleColor(): string {
|
||||
if (this.state.raucStatus.installing) return "#FF0039"
|
||||
if (this.state.uploadStatus.uploading) return "#148420"
|
||||
return "#1f85de"
|
||||
if (this.state.raucStatus.installing) return colors.RED
|
||||
if (this.state.uploadStatus.uploading) return colors.GREEN
|
||||
return colors.BLUE
|
||||
}
|
||||
|
||||
private circlePercentage(): number {
|
||||
|
@ -195,12 +187,13 @@ export default class Updater extends Component<Props, State> {
|
|||
topText = "Updating firmware"
|
||||
bottomText = this.state.raucStatus.message
|
||||
} else {
|
||||
topText = "Upload firmware package"
|
||||
topText = "Firmware update"
|
||||
bottomText = "Upload *.raucb FW package"
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="uploader">
|
||||
<div class="card upload">
|
||||
<div>
|
||||
<div class="card pad">
|
||||
<div>
|
||||
<p class="top">{topText}</p>
|
||||
</div>
|
48
ui/src/components/Updater/UpdaterView.tsx
Normal file
48
ui/src/components/Updater/UpdaterView.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import {mdiInformation, mdiUpload} from "@mdi/js"
|
||||
import {Component} from "preact"
|
||||
import Icon from "../Icon/Icon"
|
||||
import SysinfoCard from "./SysinfoCard"
|
||||
import UpdaterCard from "./UpdaterCard"
|
||||
import "./Updater.scss"
|
||||
|
||||
type Props = {}
|
||||
|
||||
type State = {
|
||||
flipped: boolean
|
||||
}
|
||||
|
||||
export default class UpdaterView extends Component<Props, State> {
|
||||
constructor(props?: Props | undefined, context?: any) {
|
||||
super(props, context)
|
||||
|
||||
this.state = {
|
||||
flipped: false,
|
||||
}
|
||||
}
|
||||
|
||||
private flipCard = () => {
|
||||
this.setState({flipped: !this.state.flipped})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
class="iconButton button-top-right"
|
||||
onClick={this.flipCard}
|
||||
aria-label={
|
||||
this.state.flipped
|
||||
? "Switch to updater"
|
||||
: "Switch to system info"
|
||||
}
|
||||
>
|
||||
<Icon icon={this.state.flipped ? mdiUpload : mdiInformation} />
|
||||
</button>
|
||||
|
||||
<div className="updater-view">
|
||||
{!this.state.flipped ? <UpdaterCard /> : <SysinfoCard />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {Component} from "preact"
|
||||
import Updater from "./Updater/Updater"
|
||||
import UpdaterView from "./Updater/UpdaterView"
|
||||
import logo from "../assets/logo.svg"
|
||||
import {version} from "../util/version"
|
||||
|
||||
|
@ -9,7 +9,7 @@ export default class App extends Component {
|
|||
<div>
|
||||
<img src={logo} alt="SEBRAUC" height="64" />
|
||||
{version}
|
||||
<Updater />
|
||||
<UpdaterView />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
27
ui/src/sebrauc-client/.openapi-generator-ignore
Normal file
27
ui/src/sebrauc-client/.openapi-generator-ignore
Normal file
|
@ -0,0 +1,27 @@
|
|||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
||||
|
||||
/.gitignore
|
||||
/.npmignore
|
||||
/git_push.sh
|
5
ui/src/sebrauc-client/.openapi-generator/FILES
Normal file
5
ui/src/sebrauc-client/.openapi-generator/FILES
Normal file
|
@ -0,0 +1,5 @@
|
|||
api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
index.ts
|
1
ui/src/sebrauc-client/.openapi-generator/VERSION
Normal file
1
ui/src/sebrauc-client/.openapi-generator/VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
5.3.0
|
539
ui/src/sebrauc-client/api.ts
Normal file
539
ui/src/sebrauc-client/api.ts
Normal file
|
@ -0,0 +1,539 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* SEBRAUC
|
||||
* REST API for the SEBRAUC firmware updater
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import {Configuration} from "./configuration"
|
||||
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "./common"
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "./base"
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RaucStatus
|
||||
*/
|
||||
export interface RaucStatus {
|
||||
/**
|
||||
* True if the installer is running
|
||||
* @type {boolean}
|
||||
* @memberof RaucStatus
|
||||
*/
|
||||
installing: boolean
|
||||
/**
|
||||
* Installation progress
|
||||
* @type {number}
|
||||
* @memberof RaucStatus
|
||||
*/
|
||||
percent: number
|
||||
/**
|
||||
* Current installation step
|
||||
* @type {string}
|
||||
* @memberof RaucStatus
|
||||
*/
|
||||
message: string
|
||||
/**
|
||||
* Installation error message
|
||||
* @type {string}
|
||||
* @memberof RaucStatus
|
||||
*/
|
||||
last_error: string
|
||||
/**
|
||||
* Full command line output of the current installation
|
||||
* @type {string}
|
||||
* @memberof RaucStatus
|
||||
*/
|
||||
log: string
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Rootfs
|
||||
*/
|
||||
export interface Rootfs {
|
||||
/**
|
||||
* Block device
|
||||
* @type {string}
|
||||
* @memberof Rootfs
|
||||
*/
|
||||
device: string
|
||||
/**
|
||||
* Filesystem
|
||||
* @type {string}
|
||||
* @memberof Rootfs
|
||||
*/
|
||||
type: string
|
||||
/**
|
||||
* Mount path (null when not mounted)
|
||||
* @type {string}
|
||||
* @memberof Rootfs
|
||||
*/
|
||||
mountpoint: string | null
|
||||
/**
|
||||
* Is the filesystem bootable
|
||||
* @type {boolean}
|
||||
* @memberof Rootfs
|
||||
*/
|
||||
bootable: boolean
|
||||
/**
|
||||
* Is the filesystem booted
|
||||
* @type {boolean}
|
||||
* @memberof Rootfs
|
||||
*/
|
||||
booted: boolean
|
||||
/**
|
||||
* Is the filesystem the next boot target
|
||||
* @type {boolean}
|
||||
* @memberof Rootfs
|
||||
*/
|
||||
primary: boolean
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface StatusMessage
|
||||
*/
|
||||
export interface StatusMessage {
|
||||
/**
|
||||
* Is operation successful
|
||||
* @type {boolean}
|
||||
* @memberof StatusMessage
|
||||
*/
|
||||
success: boolean
|
||||
/**
|
||||
* Success message
|
||||
* @type {string}
|
||||
* @memberof StatusMessage
|
||||
*/
|
||||
msg: string
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SystemInfo
|
||||
*/
|
||||
export interface SystemInfo {
|
||||
/**
|
||||
* Hostname of the system
|
||||
* @type {string}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
hostname: string
|
||||
/**
|
||||
* Name of the os distribution
|
||||
* @type {string}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
os_name: string
|
||||
/**
|
||||
* Operating system version
|
||||
* @type {string}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
os_version: string
|
||||
/**
|
||||
* System uptime in seconds
|
||||
* @type {number}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
uptime: number
|
||||
/**
|
||||
* Compatible firmware name
|
||||
* @type {string}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
rauc_compatible: string
|
||||
/**
|
||||
* Compatible firmware variant
|
||||
* @type {string}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
rauc_variant: string
|
||||
/**
|
||||
* List of RAUC root filesystems
|
||||
* @type {{ [key: string]: Rootfs; }}
|
||||
* @memberof SystemInfo
|
||||
*/
|
||||
rauc_rootfs: {[key: string]: Rootfs}
|
||||
}
|
||||
|
||||
/**
|
||||
* DefaultApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getInfo: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/info`
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
||||
let baseOptions
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
|
||||
const localVarHeaderParameter = {} as any
|
||||
const localVarQueryParameter = {} as any
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getStatus: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/status`
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
||||
let baseOptions
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
|
||||
const localVarHeaderParameter = {} as any
|
||||
const localVarQueryParameter = {} as any
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
startReboot: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/reboot`
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
||||
let baseOptions
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
|
||||
const localVarHeaderParameter = {} as any
|
||||
const localVarQueryParameter = {} as any
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {any} [updateFile]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
startUpdate: async (
|
||||
updateFile?: any,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/update`
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
|
||||
let baseOptions
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
|
||||
const localVarHeaderParameter = {} as any
|
||||
const localVarQueryParameter = {} as any
|
||||
const localVarFormParams = new ((configuration &&
|
||||
configuration.formDataCtor) ||
|
||||
FormData)()
|
||||
|
||||
if (updateFile !== undefined) {
|
||||
localVarFormParams.append("updateFile", updateFile as any)
|
||||
}
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "multipart/form-data"
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter)
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {}
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
}
|
||||
localVarRequestOptions.data = localVarFormParams
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DefaultApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiFp = function (configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getInfo(
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemInfo>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getInfo(options)
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getStatus(
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RaucStatus>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getStatus(options)
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async startReboot(
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusMessage>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.startReboot(
|
||||
options
|
||||
)
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {any} [updateFile]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async startUpdate(
|
||||
updateFile?: any,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusMessage>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.startUpdate(
|
||||
updateFile,
|
||||
options
|
||||
)
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DefaultApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = DefaultApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getInfo(options?: any): AxiosPromise<SystemInfo> {
|
||||
return localVarFp
|
||||
.getInfo(options)
|
||||
.then((request) => request(axios, basePath))
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getStatus(options?: any): AxiosPromise<RaucStatus> {
|
||||
return localVarFp
|
||||
.getStatus(options)
|
||||
.then((request) => request(axios, basePath))
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
startReboot(options?: any): AxiosPromise<StatusMessage> {
|
||||
return localVarFp
|
||||
.startReboot(options)
|
||||
.then((request) => request(axios, basePath))
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {any} [updateFile]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
startUpdate(updateFile?: any, options?: any): AxiosPromise<StatusMessage> {
|
||||
return localVarFp
|
||||
.startUpdate(updateFile, options)
|
||||
.then((request) => request(axios, basePath))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DefaultApi - object-oriented interface
|
||||
* @export
|
||||
* @class DefaultApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class DefaultApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public getInfo(options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration)
|
||||
.getInfo(options)
|
||||
.then((request) => request(this.axios, this.basePath))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public getStatus(options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration)
|
||||
.getStatus(options)
|
||||
.then((request) => request(this.axios, this.basePath))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public startReboot(options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration)
|
||||
.startReboot(options)
|
||||
.then((request) => request(this.axios, this.basePath))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} [updateFile]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public startUpdate(updateFile?: any, options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration)
|
||||
.startUpdate(updateFile, options)
|
||||
.then((request) => request(this.axios, this.basePath))
|
||||
}
|
||||
}
|
74
ui/src/sebrauc-client/base.ts
Normal file
74
ui/src/sebrauc-client/base.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* SEBRAUC
|
||||
* REST API for the SEBRAUC firmware updater
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import {Configuration} from "./configuration"
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
|
||||
|
||||
export const BASE_PATH = "http://localhost:8080/api".replace(/\/+$/, "")
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const COLLECTION_FORMATS = {
|
||||
csv: ",",
|
||||
ssv: " ",
|
||||
tsv: "\t",
|
||||
pipes: "|",
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RequestArgs
|
||||
*/
|
||||
export interface RequestArgs {
|
||||
url: string
|
||||
options: AxiosRequestConfig
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class BaseAPI
|
||||
*/
|
||||
export class BaseAPI {
|
||||
protected configuration: Configuration | undefined
|
||||
|
||||
constructor(
|
||||
configuration?: Configuration,
|
||||
protected basePath: string = BASE_PATH,
|
||||
protected axios: AxiosInstance = globalAxios
|
||||
) {
|
||||
if (configuration) {
|
||||
this.configuration = configuration
|
||||
this.basePath = configuration.basePath || this.basePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class RequiredError
|
||||
* @extends {Error}
|
||||
*/
|
||||
export class RequiredError extends Error {
|
||||
name: "RequiredError" = "RequiredError"
|
||||
constructor(public field: string, msg?: string) {
|
||||
super(msg)
|
||||
}
|
||||
}
|
181
ui/src/sebrauc-client/common.ts
Normal file
181
ui/src/sebrauc-client/common.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* SEBRAUC
|
||||
* REST API for the SEBRAUC firmware updater
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import {Configuration} from "./configuration"
|
||||
import {RequiredError, RequestArgs} from "./base"
|
||||
import {AxiosInstance, AxiosResponse} from "axios"
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const DUMMY_BASE_URL = "https://example.com"
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws {RequiredError}
|
||||
* @export
|
||||
*/
|
||||
export const assertParamExists = function (
|
||||
functionName: string,
|
||||
paramName: string,
|
||||
paramValue: unknown
|
||||
) {
|
||||
if (paramValue === null || paramValue === undefined) {
|
||||
throw new RequiredError(
|
||||
paramName,
|
||||
`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setApiKeyToObject = async function (
|
||||
object: any,
|
||||
keyParamName: string,
|
||||
configuration?: Configuration
|
||||
) {
|
||||
if (configuration && configuration.apiKey) {
|
||||
const localVarApiKeyValue =
|
||||
typeof configuration.apiKey === "function"
|
||||
? await configuration.apiKey(keyParamName)
|
||||
: await configuration.apiKey
|
||||
object[keyParamName] = localVarApiKeyValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBasicAuthToObject = function (
|
||||
object: any,
|
||||
configuration?: Configuration
|
||||
) {
|
||||
if (configuration && (configuration.username || configuration.password)) {
|
||||
object["auth"] = {
|
||||
username: configuration.username,
|
||||
password: configuration.password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBearerAuthToObject = async function (
|
||||
object: any,
|
||||
configuration?: Configuration
|
||||
) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const accessToken =
|
||||
typeof configuration.accessToken === "function"
|
||||
? await configuration.accessToken()
|
||||
: await configuration.accessToken
|
||||
object["Authorization"] = "Bearer " + accessToken
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setOAuthToObject = async function (
|
||||
object: any,
|
||||
name: string,
|
||||
scopes: string[],
|
||||
configuration?: Configuration
|
||||
) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const localVarAccessTokenValue =
|
||||
typeof configuration.accessToken === "function"
|
||||
? await configuration.accessToken(name, scopes)
|
||||
: await configuration.accessToken
|
||||
object["Authorization"] = "Bearer " + localVarAccessTokenValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setSearchParams = function (url: URL, ...objects: any[]) {
|
||||
const searchParams = new URLSearchParams(url.search)
|
||||
for (const object of objects) {
|
||||
for (const key in object) {
|
||||
if (Array.isArray(object[key])) {
|
||||
searchParams.delete(key)
|
||||
for (const item of object[key]) {
|
||||
searchParams.append(key, item)
|
||||
}
|
||||
} else {
|
||||
searchParams.set(key, object[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
url.search = searchParams.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const serializeDataIfNeeded = function (
|
||||
value: any,
|
||||
requestOptions: any,
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const nonString = typeof value !== "string"
|
||||
const needsSerialization =
|
||||
nonString && configuration && configuration.isJsonMime
|
||||
? configuration.isJsonMime(requestOptions.headers["Content-Type"])
|
||||
: nonString
|
||||
return needsSerialization
|
||||
? JSON.stringify(value !== undefined ? value : {})
|
||||
: value || ""
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const toPathString = function (url: URL) {
|
||||
return url.pathname + url.search + url.hash
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const createRequestFunction = function (
|
||||
axiosArgs: RequestArgs,
|
||||
globalAxios: AxiosInstance,
|
||||
BASE_PATH: string,
|
||||
configuration?: Configuration
|
||||
) {
|
||||
return <T = unknown, R = AxiosResponse<T>>(
|
||||
axios: AxiosInstance = globalAxios,
|
||||
basePath: string = BASE_PATH
|
||||
) => {
|
||||
const axiosRequestArgs = {
|
||||
...axiosArgs.options,
|
||||
url: (configuration?.basePath || basePath) + axiosArgs.url,
|
||||
}
|
||||
return axios.request<T, R>(axiosRequestArgs)
|
||||
}
|
||||
}
|
123
ui/src/sebrauc-client/configuration.ts
Normal file
123
ui/src/sebrauc-client/configuration.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* SEBRAUC
|
||||
* REST API for the SEBRAUC firmware updater
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
export interface ConfigurationParameters {
|
||||
apiKey?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name: string) => string)
|
||||
| ((name: string) => Promise<string>)
|
||||
username?: string
|
||||
password?: string
|
||||
accessToken?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name?: string, scopes?: string[]) => string)
|
||||
| ((name?: string, scopes?: string[]) => Promise<string>)
|
||||
basePath?: string
|
||||
baseOptions?: any
|
||||
formDataCtor?: new () => any
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
/**
|
||||
* parameter for apiKey security
|
||||
* @param name security name
|
||||
* @memberof Configuration
|
||||
*/
|
||||
apiKey?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name: string) => string)
|
||||
| ((name: string) => Promise<string>)
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
username?: string
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
password?: string
|
||||
/**
|
||||
* parameter for oauth2 security
|
||||
* @param name security name
|
||||
* @param scopes oauth2 scope
|
||||
* @memberof Configuration
|
||||
*/
|
||||
accessToken?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name?: string, scopes?: string[]) => string)
|
||||
| ((name?: string, scopes?: string[]) => Promise<string>)
|
||||
/**
|
||||
* override base path
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
basePath?: string
|
||||
/**
|
||||
* base options for axios calls
|
||||
*
|
||||
* @type {any}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
baseOptions?: any
|
||||
/**
|
||||
* The FormData constructor that will be used to create multipart form data
|
||||
* requests. You can inject this here so that execution environments that
|
||||
* do not support the FormData class can still run the generated client.
|
||||
*
|
||||
* @type {new () => FormData}
|
||||
*/
|
||||
formDataCtor?: new () => any
|
||||
|
||||
constructor(param: ConfigurationParameters = {}) {
|
||||
this.apiKey = param.apiKey
|
||||
this.username = param.username
|
||||
this.password = param.password
|
||||
this.accessToken = param.accessToken
|
||||
this.basePath = param.basePath
|
||||
this.baseOptions = param.baseOptions
|
||||
this.formDataCtor = param.formDataCtor
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given MIME is a JSON MIME.
|
||||
* JSON MIME examples:
|
||||
* application/json
|
||||
* application/json; charset=UTF8
|
||||
* APPLICATION/JSON
|
||||
* application/vnd.company+json
|
||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||
* @return True if the given MIME is JSON, false otherwise.
|
||||
*/
|
||||
public isJsonMime(mime: string): boolean {
|
||||
const jsonMime: RegExp = new RegExp(
|
||||
"^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$",
|
||||
"i"
|
||||
)
|
||||
return (
|
||||
mime !== null &&
|
||||
(jsonMime.test(mime) ||
|
||||
mime.toLowerCase() === "application/json-patch+json")
|
||||
)
|
||||
}
|
||||
}
|
16
ui/src/sebrauc-client/index.ts
Normal file
16
ui/src/sebrauc-client/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* SEBRAUC
|
||||
* REST API for the SEBRAUC firmware updater
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
export * from "./api"
|
||||
export * from "./configuration"
|
|
@ -1,3 +1,5 @@
|
|||
@use "table";
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
|
|
47
ui/src/style/table.scss
Normal file
47
ui/src/style/table.scss
Normal file
|
@ -0,0 +1,47 @@
|
|||
.table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
&.no-bottom-border {
|
||||
&,
|
||||
> tr:last-child,
|
||||
:not(thead) tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table caption {
|
||||
font-size: 1.5em;
|
||||
margin: 0.5em 0 0.75em;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 0.35em;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.625em;
|
||||
text-align: left;
|
||||
|
||||
.icon {
|
||||
color: #1f85de;
|
||||
}
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.085em;
|
||||
text-transform: uppercase;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import {Configuration, DefaultApi} from "../sebrauc-client"
|
||||
|
||||
let apiHost = document.location.host
|
||||
const httpProto = document.location.protocol
|
||||
const wsProto = httpProto === "https:" ? "wss:" : "ws:"
|
||||
|
@ -9,4 +11,10 @@ if (import.meta.env.VITE_API_HOST !== undefined) {
|
|||
const apiUrl = `${httpProto}//${apiHost}/api`
|
||||
const wsUrl = `${wsProto}//${apiHost}/api/ws`
|
||||
|
||||
export {apiUrl, wsUrl}
|
||||
let apicfg = new Configuration({
|
||||
basePath: apiUrl,
|
||||
})
|
||||
|
||||
const sebraucApi = new DefaultApi(apicfg)
|
||||
|
||||
export {apiUrl, wsUrl, sebraucApi}
|
||||
|
|
7
ui/src/util/colors.ts
Normal file
7
ui/src/util/colors.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
class colors {
|
||||
static readonly RED = "#FF0039"
|
||||
static readonly GREEN = "#148420"
|
||||
static readonly BLUE = "#1f85de"
|
||||
}
|
||||
|
||||
export default colors
|
18
ui/src/util/functions.ts
Normal file
18
ui/src/util/functions.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
function secondsToString(seconds: number): string {
|
||||
const numyears = Math.floor(seconds / 31536000)
|
||||
const numdays = Math.floor((seconds % 31536000) / 86400)
|
||||
const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600)
|
||||
const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60)
|
||||
const numseconds = (((seconds % 31536000) % 86400) % 3600) % 60
|
||||
|
||||
let res = []
|
||||
if (numyears > 0) res.push(numyears + "yr")
|
||||
if (numdays > 0) res.push(numdays + "d")
|
||||
if (numhours > 0) res.push(numhours + "h")
|
||||
if (numminutes > 0) res.push(numminutes + "m")
|
||||
if (seconds < 60) res.push(numseconds + "s")
|
||||
|
||||
return res.join(" ")
|
||||
}
|
||||
|
||||
export {secondsToString}
|
Loading…
Reference in a new issue