diff --git a/.air.toml b/.air.toml index 91be4cb..61477a2 100644 --- a/.air.toml +++ b/.air.toml @@ -16,7 +16,7 @@ tmp_dir = "tmp" kill_delay = "0s" log = "build-errors.log" send_interrupt = false - stop_on_error = true + stop_on_error = false [color] app = "" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82c5dcc..cc4ae18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,16 +7,16 @@ repos: - id: go-test-repo-mod name: Backend tests - # - 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 + - 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 - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.4.1 diff --git a/.vscode/launch.json b/.vscode/launch.json index c4595ef..3ced20b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,9 +2,10 @@ "version": "0.2.0", "configurations": [ { - "name": "SEBRAUC server", + "name": "TSGRain WebUI server", "type": "go", "request": "launch", + "cwd": "${workspaceFolder}", "mode": "auto", "program": "${workspaceFolder}/src" }, @@ -16,13 +17,6 @@ "runtimeExecutable": "npm", "skipFiles": ["/**"], "type": "pwa-node" - }, - { - "name": "Test program", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/tmp" } ] } diff --git a/Makefile b/Makefile index 229a82d..6557106 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,10 @@ protoc: generate-apidoc: SWAGGER_GENERATE_EXTENSION=false swagger generate spec --scan-models -o ${APIDOC_FILE} + swagger validate ${APIDOC_FILE} generate-apiclient: - openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true" + openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/tsgrain-client -p "supportsES6=true" cd ${UI_DIR} && npm run format clean: diff --git a/proto/tsgrain.proto b/proto/tsgrain.proto index a356af8..f9c7fdf 100644 --- a/proto/tsgrain.proto +++ b/proto/tsgrain.proto @@ -87,6 +87,7 @@ message Task { message TaskList { repeated Task tasks = 1; Timestamp now = 2; + bool auto_mode = 3; } message Job { diff --git a/src/model/job.go b/src/model/job.go new file mode 100644 index 0000000..9a8aa76 --- /dev/null +++ b/src/model/job.go @@ -0,0 +1,74 @@ +package model + +// Job model +// +// Job stellt einen Bewässerungszeitplan dar. +// +//swagger:model Job +type Job struct { + // ID des Jobs + // required: true + // example: 1 + Id int32 `json:"id"` + + // Zritstempel des Bewässerungsjobs + // required: true + // example: 1643756107 + Date int64 `json:"date"` + + // Bewässerungsdauer in Sekunden + // required: true + // example: 300 + Duration int32 `json:"duration"` + + // Zu bewässernde Zonen + // required: true + // example: [2, 3] + Zones []int32 `json:"zones"` + + // Job aktiviert? + // required: true + Enable bool `json:"enable"` + + // Job täglich wiederholen + // required: true + Repeat bool `json:"repeat"` +} + +// NewJob model +// +// NewJob stellt einen zu erstellenden Bewässerungszeitplan dar. +// +//swagger:model NewJob +type NewJob struct { + // Zritstempel des Bewässerungsjobs + // required: true + // example: 1643756107 + Date int64 `json:"date"` + + // Bewässerungsdauer in Sekunden + // required: true + // example: 300 + Duration int32 `json:"duration"` + + // Zu bewässernde Zonen + // required: true + // example: [2, 3] + Zones []int32 `json:"zones"` + + // Job aktiviert? + // required: true + Enable bool `json:"enable"` + + // Job täglich wiederholen + // required: true + Repeat bool `json:"repeat"` +} + +//swagger:model JobList +type JobList []Job + +//swagger:model JobID +type JobID struct { + Id int32 `json:"id" binding:"required"` +} diff --git a/src/model/options.go b/src/model/options.go new file mode 100644 index 0000000..a2fb2db --- /dev/null +++ b/src/model/options.go @@ -0,0 +1,38 @@ +package model + +// AutoMode model +// +// Zustand des Automatikmodus +// +//swagger:model AutoMode +type AutoMode struct { + // required: true + State bool `json:"state" binding:"required"` +} + +// DefaultIrrigationTime model +// +// Manuelle Bewässerungszeit in Sekunden +// +//swagger:model DefaultIrrigationTime +type DefaultIrrigationTime struct { + // required: true + Time int32 `json:"time" binding:"required"` +} + +// ConfigTime model +// +// Aktuelle Systemzeit/Zeitzone +// +//swagger:model ConfigTime +type ConfigTime struct { + // Aktuelle Systemzeit + // + // required: true + Time int64 `json:"time"` + + // Aktuelle Zeitzone + // + // required: true + Timezone string `json:"timezone"` +} diff --git a/src/model/task.go b/src/model/task.go new file mode 100644 index 0000000..bb398e5 --- /dev/null +++ b/src/model/task.go @@ -0,0 +1,65 @@ +package model + +// Task model +// +// Task stellt eine Bewässerungsaufgabe dar. +// +//swagger:model Task +type Task struct { + // Quelle der Bewässerungsaufgabe (0: MANUAL, 1: SCHEDULE) + // required: true + // example: 0 + Source int32 `json:"source"` + + // Nummer der Bewässerungszone + // required: true + // example: 2 + ZoneId int32 `json:"zone_id"` + + // Bewässerungsdauer in Sekunden + // required: true + // example: 300 + Duration int32 `json:"duration"` + + // Zeitstempel, wann die Bewässerung gestartet wurde + // required: true + // nullable: true + // example: 1643756107 + DatetimeStarted *int64 `json:"datetime_started"` + + // Zeitstempel, wann die Bewässerung beendet sein wird + // required: true + // nullable: true + // example: 1643756407 + DatetimeFinished *int64 `json:"datetime_finished"` +} + +// TaskList model +// +// TaskList ist eine Liste der aktuell laufenden Bewässerungsaufgaben. +// +//swagger:model TaskList +type TaskList struct { + // Aktueller Zeitstempel + // required: true + // example: 1643756107 + Now int64 `json:"now"` + + // Liste der laufenden Tasks + // required: true + Tasks []Task `json:"tasks"` + + // Automatikmodus aktiv + // required: true + AutoMode bool `json:"auto_mode"` +} + +// TaskRequestResult model +// +// TaskRequestResult wird beim Starten eines Tasks zurückgegeben +// +//swagger:model TaskRequestResult +type TaskRequestResult struct { + Started bool + Stopped bool +} diff --git a/src/server/server.go b/src/server/server.go index 0aaf0c2..bec8cf5 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -24,14 +24,17 @@ import ( "code.thetadev.de/TSGRain/WebUI/src/tsgrain_rpc" "code.thetadev.de/TSGRain/WebUI/src/util" "code.thetadev.de/TSGRain/WebUI/src/util/mode" + "code.thetadev.de/TSGRain/WebUI/ui" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" ) type WebUIServer struct { config *config.Config streamer *stream.API rpc *tsgrain_rpc.RPCClient + nZones int32 } func NewServer(config *config.Config) *WebUIServer { @@ -81,12 +84,26 @@ func (srv *WebUIServer) getRouter() *gin.Engine { api.GET("/panic", srv.controllerPanic) } + api.POST("/task/manual", srv.controllerStartTask) + + api.GET("/jobs", srv.controllerGetJobs) + api.POST("/job", srv.controllerCreateJob) + api.PUT("/job", srv.controllerUpdateJob) + api.DELETE("/job", srv.controllerDeleteJob) + + api.GET("config/auto", srv.controllerGetAutoMode) + api.POST("config/auto", srv.controllerSetAutoMode) + api.GET("config/defaultIrrigationTime", srv.controllerGetDefaultIrrigationTime) + api.POST("config/defaultIrrigationTime", srv.controllerSetDefaultIrrigationTime) + api.GET("config/time", srv.controllerGetConfigTime) + api.POST("config/time", srv.controllerSetConfigTime) + // UI uiGroup := router.Group("/", middleware.Compression( srv.config.Server.Compression.Gzip, srv.config.Server.Compression.Brotli), ) - // ui.Register(uiGroup) + ui.Register(uiGroup, srv.nZones) swagger.Register(uiGroup) return router @@ -98,12 +115,496 @@ func (srv *WebUIServer) Run() error { return err } + // Get TSGRain info + nZones, err := srv.rpc.GetNZones() + if err != nil { + return err + } + srv.nZones = nZones + go srv.rpc.StreamTasks(srv.streamer) router := srv.getRouter() - return router.Run(fmt.Sprintf("%s:%d", + err = router.Run(fmt.Sprintf("%s:%d", srv.config.Server.Address, srv.config.Server.Port)) + + _ = srv.rpc.Stop() + + return err +} + +// swagger:operation POST /task/manual startManualTask +// +// Starte/stoppe manuell eine neue Bewässerungsaufgabe +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - in: body +// name: taskRequest +// schema: +// type: object +// properties: +// zone_id: +// description: Nummer der Bewässerungszone +// type: integer +// duration: +// description: "Bewässerungsdauer in Sekunden (0: Standarddauer)" +// type: integer +// queuing: +// description: | +// Aufgabe in die Warteschlange einreihen, +// wenn sie nicht sofort ausgeführt werden kann. +// type: boolean +// cancelling: +// description: | +// Aufgabe stoppen/aus der Warteschlange entfernen, +// wenn sie bereits läuft oder sich in der Warteschlange befindet. +// type: boolean +// required: +// - zone_id +// - duration +// - queuing +// - cancelling +// responses: +// 200: +// description: Bewässerungsaufgabe erfolgreich gestarted/gestoppt +// schema: +// $ref: "#/definitions/StatusMessage" +// 400: +// description: Bewässerungsaufgabe läuft schon und kann nicht gestoppt werden. +// schema: +// $ref: "#/definitions/Error" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerStartTask(c *gin.Context) { + var params struct { + ZoneID int32 `json:"zone_id"` + Duration int32 `json:"duration"` + Queuing bool `json:"queuing"` + Cancelling bool `json:"cancelling"` + } + + if err := c.ShouldBindJSON(¶ms); err != nil { + log.Error().Err(err).Msg("StartTask input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + if !srv.validateZoneID(params.ZoneID) { + writeStatus(c, http.StatusBadRequest, "invalid zone_id") + return + } + + res, err := srv.rpc.StartManualTask( + params.ZoneID, params.Duration, params.Queuing, params.Cancelling) + if err != nil { + log.Error().Err(err).Msg("StartManualTask") + writeStatus(c, http.StatusInternalServerError, "error starting task") + return + } + + statusCode := 200 + if !res.Started && !res.Stopped { + statusCode = 400 + } + + c.JSON(statusCode, res) +} + +// swagger:operation GET /jobs getJobs +// +// Rufe alle gespeicherten Zeitpläne ab. +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Job-Liste +// schema: +// "$ref": "#/definitions/JobList" +// 500: +// description: Serverfehler +// schema: +// "$ref": "#/definitions/Error" +func (srv *WebUIServer) controllerGetJobs(c *gin.Context) { + jobs, err := srv.rpc.GetJobs() + if err != nil { + log.Error().Err(err).Msg("GetJobs") + writeStatus(c, http.StatusInternalServerError, "error getting jobs") + return + } + + c.JSON(200, jobs) +} + +// swagger:operation GET /job getJob +// +// Rufe einen gespeicherten Zeitplan ab. +// +// --- +// produces: [application/json] +// parameters: +// - name: id +// in: query +// description: Job-ID +// type: integer +// required: true +// responses: +// 200: +// description: Job +// schema: +// $ref: "#/definitions/Job" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerGetJob(c *gin.Context) { + var params struct { + Id int32 `uri:"id" binding:"required"` + } + + if err := c.ShouldBindUri(¶ms); err != nil { + log.Error().Err(err).Msg("GetJob input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + job, err := srv.rpc.GetJob(params.Id) + if err != nil { + log.Error().Err(err).Msg("GetJob") + writeStatus(c, http.StatusInternalServerError, "error getting job") + return + } + + c.JSON(200, job) +} + +// swagger:operation POST /job createJob +// +// Erstelle einen neuen Zeitplan. +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - name: job +// in: body +// description: Neuer Job +// schema: +// $ref: '#/definitions/NewJob' +// required: true +// responses: +// 200: +// description: OK +// schema: +// $ref: "#/definitions/JobID" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerCreateJob(c *gin.Context) { + var newJob model.NewJob + if err := c.ShouldBindJSON(&newJob); err != nil { + log.Error().Err(err).Msg("CreateJob input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + jobId, err := srv.rpc.CreateJob(newJob) + if err != nil { + log.Error().Err(err).Msg("CreateJob") + writeStatus(c, http.StatusInternalServerError, "error creating job") + return + } + + c.JSON(200, model.JobID{Id: jobId}) +} + +// swagger:operation PUT /job updateJob +// +// Aktualisiere einen gespeicherten Zeitplan. +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - name: job +// in: body +// description: Aktualisierter Job +// schema: +// $ref: '#/definitions/Job' +// required: true +// responses: +// 200: +// description: OK +// schema: +// $ref: "#/definitions/StatusMessage" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerUpdateJob(c *gin.Context) { + var job model.Job + if err := c.ShouldBindJSON(&job); err != nil { + log.Error().Err(err).Msg("UpdateJob input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + err := srv.rpc.UpdateJob(job) + if err != nil { + log.Error().Err(err).Msg("UpdateJob") + writeStatus(c, http.StatusInternalServerError, "error updating job") + return + } + + writeStatus(c, http.StatusOK, "updated job") +} + +// swagger:operation DELETE /job deleteJob +// +// Lösche einen gespeicherten Zeitplan. +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - name: id +// in: body +// schema: +// type: integer +// responses: +// 200: +// description: OK +// schema: +// $ref: "#/definitions/StatusMessage" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerDeleteJob(c *gin.Context) { + var jobId model.JobID + if err := c.ShouldBindJSON(&jobId); err != nil { + log.Error().Err(err).Msg("DeleteJob input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + err := srv.rpc.DeleteJob(jobId.Id) + if err != nil { + log.Error().Err(err).Msg("DeleteJob") + writeStatus(c, http.StatusInternalServerError, "error deleting job") + return + } + + writeStatus(c, http.StatusOK, "deleted job") +} + +// swagger:operation GET /config/auto getAutoMode +// +// Rufe ab, ob der Automatikmodus aktiviert ist. +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Status des Automatikmodus +// schema: +// $ref: "#/definitions/AutoMode" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerGetAutoMode(c *gin.Context) { + state, err := srv.rpc.GetAutoMode() + if err != nil { + log.Error().Err(err).Msg("GetAutoMode") + writeStatus(c, http.StatusInternalServerError, "error getting autoMode") + return + } + + c.JSON(200, model.AutoMode{State: state}) +} + +// swagger:operation POST /config/auto setAutoMode +// +// Automatikmodus aktivieren/deaktivieren +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - in: body +// name: state +// description: Zustand des Automatikmodus +// schema: +// type: object +// properties: +// state: +// type: boolean +// required: +// - state +// responses: +// 200: +// description: OK +// schema: +// $ref: "#/definitions/StatusMessage" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerSetAutoMode(c *gin.Context) { + var autoMode model.AutoMode + if err := c.ShouldBindJSON(&autoMode); err != nil { + log.Error().Err(err).Msg("SetAutoMode input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + err := srv.rpc.SetAutoMode(autoMode.State) + if err != nil { + log.Error().Err(err).Msg("SetAutoMode") + writeStatus(c, http.StatusInternalServerError, "error setting autoMode") + return + } + + writeStatus(c, http.StatusOK, "set autoMode") +} + +// swagger:operation GET /config/time getConfigTime +// +// Rufe die aktuelle Systemzeit/Zeitzone ab +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Aktuelle Systemzeit/Zeitzone +// schema: +// $ref: "#/definitions/ConfigTime" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerGetConfigTime(c *gin.Context) { + configTime, err := srv.rpc.GetConfigTime() + if err != nil { + log.Error().Err(err).Msg("GetConfigTime") + writeStatus(c, http.StatusInternalServerError, "error setting configTime") + return + } + + c.JSON(200, configTime) +} + +// swagger:operation POST /config/time setConfigTime +// +// Automatikmodus aktivieren/deaktivieren +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - in: body +// name: configTime +// schema: +// $ref: "#/definitions/ConfigTime" +// responses: +// 200: +// description: OK +// schema: +// $ref: "#/definitions/StatusMessage" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerSetConfigTime(c *gin.Context) { + var configTime model.ConfigTime + if err := c.ShouldBindJSON(&configTime); err != nil { + log.Error().Err(err).Msg("SetConfigTime input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + err := srv.rpc.SetConfigTime(configTime) + if err != nil { + log.Error().Err(err).Msg("SetConfigTime") + writeStatus(c, http.StatusInternalServerError, "error setting configTime") + return + } + + writeStatus(c, http.StatusOK, "set configTime") +} + +// swagger:operation GET /config/defaultIrrigationTime getDefaultIrrigationTime +// +// Rufe die Standardzeit bei manueller Bewässerung ab. +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Manuelle Bewässerungszeit in Sekunden +// schema: +// $ref: "#/definitions/DefaultIrrigationTime" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerGetDefaultIrrigationTime(c *gin.Context) { + defaultIrrigationTime, err := srv.rpc.GetDefaultIrrigationTime() + if err != nil { + log.Error().Err(err).Msg("GetDefaultIrrigationTime") + writeStatus(c, http.StatusInternalServerError, + "error getting defaultIrrigationTime") + return + } + + c.JSON(200, defaultIrrigationTime) +} + +// swagger:operation POST /config/defaultIrrigationTime setDefaultIrrigationTime +// +// Setze die die Standardzeit bei manueller Bewässerung. +// +// --- +// consumes: [application/json] +// produces: [application/json] +// parameters: +// - in: body +// name: defaultIrrigationTime +// schema: +// $ref: "#/definitions/DefaultIrrigationTime" +// responses: +// 200: +// description: OK +// schema: +// $ref: "#/definitions/StatusMessage" +// 500: +// description: Serverfehler +// schema: +// $ref: "#/definitions/Error" +func (srv *WebUIServer) controllerSetDefaultIrrigationTime(c *gin.Context) { + var defaultIrrigationTime model.DefaultIrrigationTime + if err := c.ShouldBindJSON(&defaultIrrigationTime); err != nil { + log.Error().Err(err).Msg("SetDefaultIrrigationTime input error") + writeStatus(c, http.StatusBadRequest, "invalid input data") + return + } + + err := srv.rpc.SetDefaultIrrigationTime(defaultIrrigationTime.Time) + if err != nil { + log.Error().Err(err).Msg("SetDefaultIrrigationTime") + writeStatus(c, http.StatusInternalServerError, + "error setting defaultIrrigationTime") + return + } + + writeStatus(c, http.StatusOK, "set defaultIrrigationTime") } // controllerError throws an error for testing @@ -116,9 +617,13 @@ func (srv *WebUIServer) controllerPanic(c *gin.Context) { panic(errors.New("panic message")) } -func writeStatus(c *gin.Context, success bool, msg string) { - c.JSON(http.StatusOK, model.StatusMessage{ - Success: success, +func (srv *WebUIServer) validateZoneID(zoneId int32) bool { + return 0 < zoneId && zoneId <= srv.nZones +} + +func writeStatus(c *gin.Context, code int, msg string) { + c.JSON(code, model.StatusMessage{ + Success: code == http.StatusOK, Msg: msg, }) } diff --git a/src/server/swagger/swagger.html b/src/server/swagger/swagger.html index cc6855f..f2e1497 100644 --- a/src/server/swagger/swagger.html +++ b/src/server/swagger/swagger.html @@ -1,7 +1,7 @@ - SEBRAUC API documentation + TSGRain WebUI API documentation diff --git a/src/server/swagger/swagger.yaml b/src/server/swagger/swagger.yaml index c9c5523..a5d2036 100644 --- a/src/server/swagger/swagger.yaml +++ b/src/server/swagger/swagger.yaml @@ -1,4 +1,38 @@ definitions: + AutoMode: + description: Zustand des Automatikmodus + properties: + state: + type: boolean + required: + - state + title: AutoMode model + type: object + ConfigTime: + description: Aktuelle Systemzeit/Zeitzone + properties: + time: + description: Aktuelle Systemzeit + format: int64 + type: integer + timezone: + description: Aktuelle Zeitzone + type: string + required: + - time + - timezone + title: ConfigTime model + type: object + DefaultIrrigationTime: + description: Manuelle Bewässerungszeit in Sekunden + properties: + time: + format: int32 + type: integer + required: + - time + title: DefaultIrrigationTime model + type: object Error: description: The Error contains error relevant information. properties: @@ -21,6 +55,94 @@ definitions: - msg title: Error model type: object + Job: + description: Job stellt einen Bewässerungszeitplan dar. + properties: + date: + description: Zritstempel des Bewässerungsjobs + example: 1643756107 + format: int64 + type: integer + duration: + description: Bewässerungsdauer in Sekunden + example: 300 + format: int32 + type: integer + enable: + description: Job aktiviert? + type: boolean + id: + description: ID des Jobs + example: 1 + format: int32 + type: integer + repeat: + description: Job täglich wiederholen + type: boolean + zones: + description: Zu bewässernde Zonen + example: + - 2 + - 3 + items: + format: int32 + type: integer + type: array + required: + - id + - date + - duration + - zones + - enable + - repeat + title: Job model + type: object + JobID: + properties: + id: + format: int32 + type: integer + type: object + JobList: + items: + $ref: "#/definitions/Job" + type: array + NewJob: + description: NewJob stellt einen zu erstellenden Bewässerungszeitplan dar. + properties: + date: + description: Zritstempel des Bewässerungsjobs + example: 1643756107 + format: int64 + type: integer + duration: + description: Bewässerungsdauer in Sekunden + example: 300 + format: int32 + type: integer + enable: + description: Job aktiviert? + type: boolean + repeat: + description: Job täglich wiederholen + type: boolean + zones: + description: Zu bewässernde Zonen + example: + - 2 + - 3 + items: + format: int32 + type: integer + type: array + required: + - date + - duration + - zones + - enable + - repeat + title: NewJob model + type: object StatusMessage: description: StatusMessage contains the status of an operation. properties: @@ -36,13 +158,345 @@ definitions: - msg title: StatusMessage model type: object + Task: + description: Task stellt eine Bewässerungsaufgabe dar. + properties: + datetime_finished: + description: Zeitstempel, wann die Bewässerung beendet sein wird + example: 1643756407 + format: int64 + type: integer + datetime_started: + description: Zeitstempel, wann die Bewässerung gestartet wurde + example: 1643756107 + format: int64 + type: integer + duration: + description: Bewässerungsdauer in Sekunden + example: 300 + format: int32 + type: integer + source: + description: "Quelle der Bewässerungsaufgabe (0: MANUAL, 1: SCHEDULE)" + example: 0 + format: int32 + type: integer + zone_id: + description: Nummer der Bewässerungszone + example: 2 + format: int32 + type: integer + required: + - source + - zone_id + - duration + - datetime_started + - datetime_finished + title: Task model + type: object + TaskList: + description: TaskList ist eine Liste der aktuell laufenden Bewässerungsaufgaben. + properties: + auto_mode: + description: Automatikmodus aktiv + type: boolean + now: + description: Aktueller Zeitstempel + example: 1643756107 + format: int64 + type: integer + tasks: + description: Liste der laufenden Tasks + items: + $ref: "#/definitions/Task" + type: array + required: + - now + - tasks + - auto_mode + title: TaskList model + type: object + TaskRequestResult: + description: TaskRequestResult wird beim Starten eines Tasks zurückgegeben + properties: + Started: + type: boolean + Stopped: + type: boolean + title: TaskRequestResult model + type: object info: description: REST API for the TSGRain WebUI license: name: MIT title: TSGRain WebUI version: 0.1.0 -paths: {} +paths: + /config/auto: + get: + operationId: getAutoMode + produces: + - application/json + responses: + "200": + description: Status des Automatikmodus + schema: + $ref: "#/definitions/AutoMode" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Rufe ab, ob der Automatikmodus aktiviert ist. + post: + consumes: + - application/json + description: Automatikmodus aktivieren/deaktivieren + operationId: setAutoMode + parameters: + - description: Zustand des Automatikmodus + in: body + name: state + schema: + properties: + state: + type: boolean + required: + - state + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/StatusMessage" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + /config/defaultIrrigationTime: + get: + operationId: getDefaultIrrigationTime + produces: + - application/json + responses: + "200": + description: Manuelle Bewässerungszeit in Sekunden + schema: + $ref: "#/definitions/DefaultIrrigationTime" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Rufe die Standardzeit bei manueller Bewässerung ab. + post: + consumes: + - application/json + operationId: setDefaultIrrigationTime + parameters: + - in: body + name: defaultIrrigationTime + schema: + $ref: "#/definitions/DefaultIrrigationTime" + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/StatusMessage" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Setze die die Standardzeit bei manueller Bewässerung. + /config/time: + get: + description: Rufe die aktuelle Systemzeit/Zeitzone ab + operationId: getConfigTime + produces: + - application/json + responses: + "200": + description: Aktuelle Systemzeit/Zeitzone + schema: + $ref: "#/definitions/ConfigTime" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + post: + consumes: + - application/json + description: Automatikmodus aktivieren/deaktivieren + operationId: setConfigTime + parameters: + - in: body + name: configTime + schema: + $ref: "#/definitions/ConfigTime" + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/StatusMessage" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + /job: + delete: + consumes: + - application/json + operationId: deleteJob + parameters: + - in: body + name: id + schema: + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/StatusMessage" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Lösche einen gespeicherten Zeitplan. + get: + operationId: getJob + parameters: + - description: Job-ID + in: query + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Job + schema: + $ref: "#/definitions/Job" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Rufe einen gespeicherten Zeitplan ab. + post: + consumes: + - application/json + operationId: createJob + parameters: + - description: Neuer Job + in: body + name: job + required: true + schema: + $ref: "#/definitions/NewJob" + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/JobID" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Erstelle einen neuen Zeitplan. + put: + consumes: + - application/json + operationId: updateJob + parameters: + - description: Aktualisierter Job + in: body + name: job + required: true + schema: + $ref: "#/definitions/Job" + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: "#/definitions/StatusMessage" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Aktualisiere einen gespeicherten Zeitplan. + /jobs: + get: + operationId: getJobs + produces: + - application/json + responses: + "200": + description: Job-Liste + schema: + $ref: "#/definitions/JobList" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" + summary: Rufe alle gespeicherten Zeitpläne ab. + /task/manual: + post: + consumes: + - application/json + description: Starte/stoppe manuell eine neue Bewässerungsaufgabe + operationId: startManualTask + parameters: + - in: body + name: taskRequest + schema: + properties: + cancelling: + description: | + Aufgabe stoppen/aus der Warteschlange entfernen, + wenn sie bereits läuft oder sich in der Warteschlange befindet. + type: boolean + duration: + description: "Bewässerungsdauer in Sekunden (0: Standarddauer)" + type: integer + queuing: + description: | + Aufgabe in die Warteschlange einreihen, + wenn sie nicht sofort ausgeführt werden kann. + type: boolean + zone_id: + description: Nummer der Bewässerungszone + type: integer + required: + - zone_id + - duration + - queuing + - cancelling + type: object + produces: + - application/json + responses: + "200": + description: Bewässerungsaufgabe erfolgreich gestarted/gestoppt + schema: + $ref: "#/definitions/StatusMessage" + "400": + description: Bewässerungsaufgabe läuft schon und kann nicht gestoppt werden. + schema: + $ref: "#/definitions/Error" + "500": + description: Serverfehler + schema: + $ref: "#/definitions/Error" schemes: - http - https diff --git a/src/tsgrain_rpc/proto/tsgrain.pb.go b/src/tsgrain_rpc/proto/tsgrain.pb.go index a2c32d0..923581a 100644 --- a/src/tsgrain_rpc/proto/tsgrain.pb.go +++ b/src/tsgrain_rpc/proto/tsgrain.pb.go @@ -335,8 +335,9 @@ type TaskList struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Tasks []*Task `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` - Now *Timestamp `protobuf:"bytes,2,opt,name=now,proto3" json:"now,omitempty"` + Tasks []*Task `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + Now *Timestamp `protobuf:"bytes,2,opt,name=now,proto3" json:"now,omitempty"` + AutoMode bool `protobuf:"varint,3,opt,name=auto_mode,json=autoMode,proto3" json:"auto_mode,omitempty"` } func (x *TaskList) Reset() { @@ -385,6 +386,13 @@ func (x *TaskList) GetNow() *Timestamp { return nil } +func (x *TaskList) GetAutoMode() bool { + if x != nil { + return x.AutoMode + } + return false +} + type Job struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -659,89 +667,91 @@ var file_proto_tsgrain_proto_rawDesc = []byte{ 0x12, 0x37, 0x0a, 0x11, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, - 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x22, 0x45, 0x0a, 0x08, 0x54, 0x61, 0x73, + 0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x22, 0x62, 0x0a, 0x08, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x1c, 0x0a, 0x03, 0x6e, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x6e, 0x6f, 0x77, - 0x22, 0x97, 0x01, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x05, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x05, 0x4a, 0x6f, - 0x62, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x02, 0x69, 0x64, 0x22, 0x23, 0x0a, 0x07, 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, - 0x0a, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4a, - 0x6f, 0x62, 0x52, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x22, 0x50, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, - 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x2a, 0x26, 0x0a, 0x0a, 0x54, 0x61, - 0x73, 0x6b, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x41, 0x4e, 0x55, - 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, - 0x10, 0x01, 0x32, 0xc7, 0x06, 0x0a, 0x07, 0x54, 0x53, 0x47, 0x52, 0x61, 0x69, 0x6e, 0x12, 0x2f, - 0x0a, 0x09, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0c, 0x2e, 0x54, 0x61, - 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x54, 0x61, 0x73, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, - 0x2f, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x97, 0x01, + 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x05, + 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x05, 0x4a, 0x6f, 0x62, 0x49, 0x44, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, + 0x22, 0x23, 0x0a, 0x07, 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x04, 0x6a, + 0x6f, 0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x52, + 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x22, 0x50, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, + 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x2a, 0x26, 0x0a, 0x0a, 0x54, 0x61, 0x73, 0x6b, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x41, 0x4e, 0x55, 0x41, 0x4c, 0x10, + 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x01, 0x32, + 0xc7, 0x06, 0x0a, 0x07, 0x54, 0x53, 0x47, 0x52, 0x61, 0x69, 0x6e, 0x12, 0x2f, 0x0a, 0x09, 0x53, + 0x74, 0x61, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0c, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x08, + 0x47, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x34, 0x0a, + 0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x22, + 0x00, 0x30, 0x01, 0x12, 0x1b, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, + 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x22, 0x00, + 0x12, 0x18, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, 0x4a, 0x6f, 0x62, + 0x49, 0x44, 0x1a, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x07, 0x47, 0x65, + 0x74, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x08, 0x2e, + 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x09, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x09, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, - 0x12, 0x34, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, - 0x73, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x1b, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x4a, 0x6f, 0x62, 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, - 0x44, 0x22, 0x00, 0x12, 0x18, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, - 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x1a, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x22, 0x00, 0x12, 0x2d, 0x0a, - 0x07, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x08, 0x2e, 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x09, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x09, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x41, - 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a, - 0x0b, 0x53, 0x65, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x2e, 0x67, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x6f, + 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, - 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, - 0x69, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x53, 0x65, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x0b, 0x2e, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x51, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x49, 0x72, 0x72, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, + 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0b, 0x53, 0x65, + 0x74, 0x41, 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, + 0x36, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x0b, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, + 0x51, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x49, 0x72, 0x72, + 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x22, 0x00, 0x12, 0x51, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x49, 0x72, 0x72, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x49, 0x72, 0x72, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x4e, - 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x61, 0x64, 0x65, 0x76, 0x2e, 0x64, 0x65, - 0x2f, 0x54, 0x53, 0x47, 0x52, 0x61, 0x69, 0x6e, 0x2f, 0x57, 0x65, 0x62, 0x55, 0x49, 0x2f, 0x73, - 0x72, 0x63, 0x2f, 0x74, 0x73, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x5f, 0x72, 0x70, 0x63, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x4e, 0x5a, 0x6f, 0x6e, + 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, + 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x61, 0x64, 0x65, 0x76, 0x2e, 0x64, 0x65, 0x2f, 0x54, 0x53, + 0x47, 0x52, 0x61, 0x69, 0x6e, 0x2f, 0x57, 0x65, 0x62, 0x55, 0x49, 0x2f, 0x73, 0x72, 0x63, 0x2f, + 0x74, 0x73, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x5f, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/src/tsgrain_rpc/tsgrain_rpc.go b/src/tsgrain_rpc/tsgrain_rpc.go index f1314cb..46202e3 100644 --- a/src/tsgrain_rpc/tsgrain_rpc.go +++ b/src/tsgrain_rpc/tsgrain_rpc.go @@ -7,12 +7,14 @@ import ( "io" "time" + "code.thetadev.de/TSGRain/WebUI/src/model" tsgrain_grpc "code.thetadev.de/TSGRain/WebUI/src/tsgrain_rpc/proto" "code.thetadev.de/TSGRain/WebUI/src/util" "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/wrapperspb" ) type RPCClient struct { @@ -50,6 +52,78 @@ func (c *RPCClient) Stop() error { return c.conn.Close() } +func mapTask(pbTask *tsgrain_grpc.Task) model.Task { + task := model.Task{ + Source: int32(pbTask.Source), + ZoneId: pbTask.ZoneId, + Duration: pbTask.Duration, + } + + if pbTask.DatetimeStarted != nil { + task.DatetimeStarted = &pbTask.DatetimeStarted.Seconds + } + if pbTask.DatetimeFinished != nil { + task.DatetimeFinished = &pbTask.DatetimeFinished.Seconds + } + return task +} + +func mapTaskList(pbTaskList *tsgrain_grpc.TaskList) model.TaskList { + taskList := model.TaskList{ + Now: pbTaskList.Now.Seconds, + Tasks: []model.Task{}, + AutoMode: pbTaskList.AutoMode, + } + + for _, pbTask := range pbTaskList.Tasks { + if pbTask != nil { + taskList.Tasks = append(taskList.Tasks, mapTask(pbTask)) + } + } + return taskList +} + +func (c *RPCClient) StartManualTask( + zone_id int32, duration int32, queuing bool, cancelling bool) ( + model.TaskRequestResult, error) { + res, err := c.tsgrain.StartTask(c.ctx, &tsgrain_grpc.TaskRequest{ + Source: tsgrain_grpc.TaskSource_MANUAL, + ZoneId: zone_id, + Duration: duration, + Queuing: queuing, + Cancelling: cancelling, + }) + if err != nil { + return model.TaskRequestResult{}, err + } + + return model.TaskRequestResult{ + Started: res.Started, + Stopped: res.Stopped, + }, nil +} + +func (c *RPCClient) GetTasks() (model.TaskList, error) { + pbTaskList, err := c.tsgrain.GetTasks(c.ctx, &emptypb.Empty{}) + if err != nil { + return model.TaskList{}, err + } + return mapTaskList(pbTaskList), nil +} + +func broadcastTaskList(bc util.Broadcaster, pbTaskList *tsgrain_grpc.TaskList) error { + taskList := mapTaskList(pbTaskList) + + taskListJson, err := json.Marshal(taskList) + if err != nil { + return err + } + + bc.Broadcast(taskListJson) + log.Debug().RawJSON("task_list", taskListJson).Msg("TaskList received") + return nil +} + func (c *RPCClient) streamTasks(bc util.Broadcaster) error { stream, err := c.tsgrain.StreamTasks(c.ctx, &emptypb.Empty{}) if err != nil { @@ -57,25 +131,25 @@ func (c *RPCClient) streamTasks(bc util.Broadcaster) error { } for { - taskList, err := stream.Recv() + pbTaskList, err := stream.Recv() if errors.Is(err, io.EOF) { return nil } if err != nil { return err } else { - taskListJson, err := json.Marshal(taskList) - if err != nil { - return err - } - - bc.Broadcast(taskListJson) - log.Debug().RawJSON("task_list", taskListJson).Msg("TaskList received") + _ = broadcastTaskList(bc, pbTaskList) } } } func (c *RPCClient) StreamTasks(bc util.Broadcaster) { + // Get initial state + pbTaskList, err := c.tsgrain.GetTasks(c.ctx, &emptypb.Empty{}) + if err == nil { + _ = broadcastTaskList(bc, pbTaskList) + } + // Keep stream running if it errors for { select { @@ -90,3 +164,142 @@ func (c *RPCClient) StreamTasks(bc util.Broadcaster) { } } } + +func (c *RPCClient) CreateJob(job model.NewJob) (int32, error) { + pbJob := &tsgrain_grpc.Job{ + Date: &tsgrain_grpc.Timestamp{Seconds: job.Date}, + Duration: job.Duration, + Zones: job.Zones, + Enable: job.Enable, + Repeat: job.Repeat, + } + + res, err := c.tsgrain.CreateJob(c.ctx, pbJob) + if err != nil { + return 0, err + } + + return res.Id, nil +} + +func (c *RPCClient) GetJobs() (model.JobList, error) { + jobs := []model.Job{} + + res, err := c.tsgrain.GetJobs(c.ctx, &emptypb.Empty{}) + if err != nil { + return jobs, err + } + + for _, pbJob := range res.Jobs { + job := model.Job{ + Id: pbJob.Id, + Date: pbJob.Date.Seconds, + Duration: pbJob.Duration, + Zones: pbJob.Zones, + Enable: pbJob.Enable, + Repeat: pbJob.Repeat, + } + jobs = append(jobs, job) + } + + return jobs, nil +} + +func (c *RPCClient) GetJob(id int32) (model.Job, error) { + res, err := c.tsgrain.GetJob(c.ctx, &tsgrain_grpc.JobID{Id: id}) + if err != nil { + return model.Job{}, err + } + + job := model.Job{ + Id: res.Id, + Date: res.Date.Seconds, + Duration: res.Duration, + Zones: res.Zones, + Enable: res.Enable, + Repeat: res.Repeat, + } + return job, nil +} + +func (c *RPCClient) UpdateJob(job model.Job) error { + pbJob := tsgrain_grpc.Job{ + Id: job.Id, + Date: &tsgrain_grpc.Timestamp{Seconds: job.Date}, + Duration: job.Duration, + Zones: job.Zones, + Enable: job.Enable, + Repeat: job.Repeat, + } + + _, err := c.tsgrain.UpdateJob(c.ctx, &pbJob) + return err +} + +func (c *RPCClient) DeleteJob(job_id int32) error { + _, err := c.tsgrain.DeleteJob(c.ctx, &tsgrain_grpc.JobID{Id: job_id}) + return err +} + +func (c *RPCClient) GetAutoMode() (bool, error) { + res, err := c.tsgrain.GetAutoMode(c.ctx, &emptypb.Empty{}) + if err != nil { + return false, err + } + + return res.Value, nil +} + +func (c *RPCClient) SetAutoMode(state bool) error { + _, err := c.tsgrain.SetAutoMode(c.ctx, wrapperspb.Bool(state)) + return err +} + +func (c *RPCClient) GetConfigTime() (model.ConfigTime, error) { + configTime, err := c.tsgrain.GetConfigTime(c.ctx, &emptypb.Empty{}) + if err != nil { + return model.ConfigTime{}, err + } + + return model.ConfigTime{ + Time: configTime.Datetime.Seconds, + Timezone: configTime.Timezone, + }, nil +} + +func (c *RPCClient) SetConfigTime(configTime model.ConfigTime) error { + _, err := c.tsgrain.SetConfigTime(c.ctx, &tsgrain_grpc.ConfigTime{ + Datetime: &tsgrain_grpc.Timestamp{Seconds: configTime.Time}, + Timezone: configTime.Timezone, + }) + return err +} + +func (c *RPCClient) SetSystemTimezone(timezone string) error { + _, err := c.tsgrain.SetConfigTime(c.ctx, &tsgrain_grpc.ConfigTime{ + Timezone: timezone, + }) + return err +} + +func (c *RPCClient) GetDefaultIrrigationTime() (int32, error) { + res, err := c.tsgrain.GetDefaultIrrigationTime(c.ctx, &emptypb.Empty{}) + if err != nil { + return 0, err + } + + return res.Value, nil +} + +func (c *RPCClient) SetDefaultIrrigationTime(defaultTime int32) error { + _, err := c.tsgrain.SetDefaultIrrigationTime(c.ctx, wrapperspb.Int32(defaultTime)) + return err +} + +func (c *RPCClient) GetNZones() (int32, error) { + val, err := c.tsgrain.GetNZones(c.ctx, &emptypb.Empty{}) + if err != nil { + return 0, err + } + return val.Value, nil +} diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 0000000..45af11d --- /dev/null +++ b/ui/.env.development @@ -0,0 +1 @@ +VITE_API_HOST=127.0.0.1:8001 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/.prettierignore b/ui/.prettierignore new file mode 100644 index 0000000..d638edf --- /dev/null +++ b/ui/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +tmp +.tmp diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..ebabc86 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,17 @@ + + + + + + + TSGRain + + + +
+ + + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..c27f082 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,24 @@ +{ + "name": "ui", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview", + "lint": "tsc", + "format": "prettier --write ../" + }, + "dependencies": { + "@mdi/js": "^6.5.95", + "axios": "^0.24.0", + "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", + "@babel/core": ">=7.12.10 <8.0.0" + } +} diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml new file mode 100644 index 0000000..a0e06fe --- /dev/null +++ b/ui/pnpm-lock.yaml @@ -0,0 +1,1242 @@ +lockfileVersion: 5.3 + +specifiers: + "@babel/core": ">=7.12.10 <8.0.0" + "@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 + typescript: ^4.5.2 + vite: ^2.6.14 + +dependencies: + "@mdi/js": 6.5.95 + axios: 0.24.0 + preact: 10.6.5 + +devDependencies: + "@babel/core": 7.17.0 + "@preact/preset-vite": 2.1.7_8bad03c14e079d36b71a4a5e3cd3eb6f + prettier: 2.5.1 + sass: 1.49.7 + typescript: 4.5.5 + vite: 2.7.13_sass@1.49.7 + +packages: + /@ampproject/remapping/2.0.2: + resolution: + { + integrity: sha512-sE8Gx+qSDMLoJvb3QarJJlDQK7SSY4rK3hxp4XsiANeFOmjU46ZI7Y9adAQRJrmbz8zbtZkp3mJTT+rGxtF0XA==, + } + engines: {node: ">=6.0.0"} + dependencies: + "@jridgewell/trace-mapping": 0.2.5 + sourcemap-codec: 1.4.8 + dev: true + + /@babel/code-frame/7.16.7: + resolution: + { + integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/highlight": 7.16.10 + dev: true + + /@babel/compat-data/7.17.0: + resolution: + { + integrity: sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/core/7.17.0: + resolution: + { + integrity: sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@ampproject/remapping": 2.0.2 + "@babel/code-frame": 7.16.7 + "@babel/generator": 7.17.0 + "@babel/helper-compilation-targets": 7.16.7_@babel+core@7.17.0 + "@babel/helper-module-transforms": 7.16.7 + "@babel/helpers": 7.17.0 + "@babel/parser": 7.17.0 + "@babel/template": 7.16.7 + "@babel/traverse": 7.17.0 + "@babel/types": 7.17.0 + convert-source-map: 1.8.0 + debug: 4.3.3 + gensync: 1.0.0-beta.2 + json5: 2.2.0 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator/7.17.0: + resolution: + { + integrity: sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + jsesc: 2.5.2 + source-map: 0.5.7 + dev: true + + /@babel/helper-annotate-as-pure/7.16.7: + resolution: + { + integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-compilation-targets/7.16.7_@babel+core@7.17.0: + resolution: + { + integrity: sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==, + } + engines: {node: ">=6.9.0"} + peerDependencies: + "@babel/core": ^7.0.0 + dependencies: + "@babel/compat-data": 7.17.0 + "@babel/core": 7.17.0 + "@babel/helper-validator-option": 7.16.7 + browserslist: 4.19.1 + semver: 6.3.0 + dev: true + + /@babel/helper-environment-visitor/7.16.7: + resolution: + { + integrity: sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-function-name/7.16.7: + resolution: + { + integrity: sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-get-function-arity": 7.16.7 + "@babel/template": 7.16.7 + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-get-function-arity/7.16.7: + resolution: + { + integrity: sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-hoist-variables/7.16.7: + resolution: + { + integrity: sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-module-imports/7.16.7: + resolution: + { + integrity: sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-module-transforms/7.16.7: + resolution: + { + integrity: sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-environment-visitor": 7.16.7 + "@babel/helper-module-imports": 7.16.7 + "@babel/helper-simple-access": 7.16.7 + "@babel/helper-split-export-declaration": 7.16.7 + "@babel/helper-validator-identifier": 7.16.7 + "@babel/template": 7.16.7 + "@babel/traverse": 7.17.0 + "@babel/types": 7.17.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-plugin-utils/7.16.7: + resolution: + { + integrity: sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/helper-simple-access/7.16.7: + resolution: + { + integrity: sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-split-export-declaration/7.16.7: + resolution: + { + integrity: sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/types": 7.17.0 + dev: true + + /@babel/helper-validator-identifier/7.16.7: + resolution: + { + integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/helper-validator-option/7.16.7: + resolution: + { + integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==, + } + engines: {node: ">=6.9.0"} + dev: true + + /@babel/helpers/7.17.0: + resolution: + { + integrity: sha512-Xe/9NFxjPwELUvW2dsukcMZIp6XwPSbI4ojFBJuX5ramHuVE22SVcZIwqzdWo5uCgeTXW8qV97lMvSOjq+1+nQ==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/template": 7.16.7 + "@babel/traverse": 7.17.0 + "@babel/types": 7.17.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight/7.16.10: + resolution: + { + integrity: sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-validator-identifier": 7.16.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser/7.17.0: + resolution: + { + integrity: sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==, + } + engines: {node: ">=6.0.0"} + hasBin: true + dev: true + + /@babel/plugin-syntax-jsx/7.16.7_@babel+core@7.17.0: + resolution: + { + integrity: sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==, + } + engines: {node: ">=6.9.0"} + peerDependencies: + "@babel/core": ^7.0.0-0 + dependencies: + "@babel/core": 7.17.0 + "@babel/helper-plugin-utils": 7.16.7 + dev: true + + /@babel/plugin-transform-react-jsx/7.16.7_@babel+core@7.17.0: + resolution: + { + integrity: sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag==, + } + engines: {node: ">=6.9.0"} + peerDependencies: + "@babel/core": ^7.0.0-0 + dependencies: + "@babel/core": 7.17.0 + "@babel/helper-annotate-as-pure": 7.16.7 + "@babel/helper-module-imports": 7.16.7 + "@babel/helper-plugin-utils": 7.16.7 + "@babel/plugin-syntax-jsx": 7.16.7_@babel+core@7.17.0 + "@babel/types": 7.17.0 + dev: true + + /@babel/template/7.16.7: + resolution: + { + integrity: sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/code-frame": 7.16.7 + "@babel/parser": 7.17.0 + "@babel/types": 7.17.0 + dev: true + + /@babel/traverse/7.17.0: + resolution: + { + integrity: sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/code-frame": 7.16.7 + "@babel/generator": 7.17.0 + "@babel/helper-environment-visitor": 7.16.7 + "@babel/helper-function-name": 7.16.7 + "@babel/helper-hoist-variables": 7.16.7 + "@babel/helper-split-export-declaration": 7.16.7 + "@babel/parser": 7.17.0 + "@babel/types": 7.17.0 + debug: 4.3.3 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types/7.17.0: + resolution: + { + integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==, + } + engines: {node: ">=6.9.0"} + dependencies: + "@babel/helper-validator-identifier": 7.16.7 + to-fast-properties: 2.0.0 + dev: true + + /@jridgewell/resolve-uri/3.0.4: + resolution: + { + integrity: sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg==, + } + engines: {node: ">=6.0.0"} + dev: true + + /@jridgewell/trace-mapping/0.2.5: + resolution: + { + integrity: sha512-K+Eths78fXDFOvQ2hgJhCiI5s+g81r2yXmACBpbn+f2+Qt94PNoTgUcAXPT8DZkhXCsZRsHVWVtY5KIBMcpDqQ==, + } + dependencies: + "@jridgewell/resolve-uri": 3.0.4 + sourcemap-codec: 1.4.8 + dev: true + + /@mdi/js/6.5.95: + resolution: + { + integrity: sha512-x/bwEoAGP+Mo10Dfk5audNIPi7Yz8ZBrILcbXLW3ShOI/njpgodzpgpC2WYK3D2ZSC392peRRemIFb/JsyzzYQ==, + } + dev: false + + /@preact/preset-vite/2.1.7_8bad03c14e079d36b71a4a5e3cd3eb6f: + resolution: + { + integrity: sha512-/Ii+aN1Jm8TK5YzDVrj5UOVRZ91F2Nik1P3FbmZyQ5VJAg1ZCkOBBkmLrz9YhiMbbGl+U35yaZNUDMcO8Xlp2g==, + } + peerDependencies: + "@babel/core": 7.x + vite: 2.x + dependencies: + "@babel/core": 7.17.0 + "@babel/plugin-transform-react-jsx": 7.16.7_@babel+core@7.17.0 + "@prefresh/vite": 2.2.6_preact@10.6.5+vite@2.7.13 + "@rollup/pluginutils": 4.1.2 + babel-plugin-transform-hook-names: 1.0.2_@babel+core@7.17.0 + debug: 4.3.3 + kolorist: 1.5.1 + resolve: 1.22.0 + vite: 2.7.13_sass@1.49.7 + 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.6.5: + resolution: + { + integrity: sha512-Iv+uI698KDgWsrKpLvOgN3hmAMyvhVgn09mcnhZ98BUNdg/qrxE7tcUf5yFCImkgqED5/Dcn8G5hFy4IikEDvg==, + } + peerDependencies: + preact: ^10.0.0 + dependencies: + preact: 10.6.5 + dev: true + + /@prefresh/utils/1.1.1: + resolution: + { + integrity: sha512-MUhT5m2XNN5NsZl4GnpuvlzLo6VSTa/+wBfBd3fiWUvHGhv0GF9hnA1pd//v0uJaKwUnVRQ1hYElxCV7DtYsCQ==, + } + dev: true + + /@prefresh/vite/2.2.6_preact@10.6.5+vite@2.7.13: + resolution: + { + integrity: sha512-Q5i15C27NanIPzimkvBuzImebCxeQS6p8OyyH2QpNKS/HGsm7WpnyCzgMkIsidnmMHY59qllKwF01TWpq9IeGA==, + } + peerDependencies: + preact: ^10.4.0 + vite: ">=2.0.0-beta.3" + dependencies: + "@babel/core": 7.17.0 + "@prefresh/babel-plugin": 0.4.1 + "@prefresh/core": 1.3.2_preact@10.6.5 + "@prefresh/utils": 1.1.1 + "@rollup/pluginutils": 4.1.2 + preact: 10.6.5 + vite: 2.7.13_sass@1.49.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@rollup/pluginutils/4.1.2: + resolution: + { + integrity: sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==, + } + engines: {node: ">= 8.0.0"} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + 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.1 + dev: true + + /axios/0.24.0: + resolution: + { + integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==, + } + dependencies: + follow-redirects: 1.14.7 + transitivePeerDependencies: + - debug + dev: false + + /babel-plugin-transform-hook-names/1.0.2_@babel+core@7.17.0: + resolution: + { + integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==, + } + peerDependencies: + "@babel/core": ^7.12.10 + dependencies: + "@babel/core": 7.17.0 + 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.19.1: + resolution: + { + integrity: sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==, + } + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001306 + electron-to-chromium: 1.4.63 + escalade: 3.1.1 + node-releases: 2.0.1 + picocolors: 1.0.0 + dev: true + + /caniuse-lite/1.0.30001306: + resolution: + { + integrity: sha512-Wd1OuggRzg1rbnM5hv1wXs2VkxJH/AA+LuudlIqvZiCvivF+wJJe2mgBZC8gPMgI7D76PP5CTx8Luvaqc1V6OQ==, + } + 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.3: + resolution: + { + integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==, + } + 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.3: + resolution: + { + integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==, + } + engines: {node: ">=6.0"} + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /electron-to-chromium/1.4.63: + resolution: + { + integrity: sha512-e0PX/LRJPFRU4kzJKLvTobxyFdnANCvcoDCe8XcyTqP58nTWIwdsHvXLIl1RkB39X5yaosLaroMASWB0oIsgCA==, + } + dev: true + + /esbuild-android-arm64/0.13.15: + resolution: + { + integrity: sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==, + } + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.13.15: + resolution: + { + integrity: sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==, + } + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.13.15: + resolution: + { + integrity: sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==, + } + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.13.15: + resolution: + { + integrity: sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==, + } + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.13.15: + resolution: + { + integrity: sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==, + } + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.13.15: + resolution: + { + integrity: sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==, + } + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.13.15: + resolution: + { + integrity: sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==, + } + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.13.15: + resolution: + { + integrity: sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==, + } + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.13.15: + resolution: + { + integrity: sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==, + } + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.13.15: + resolution: + { + integrity: sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==, + } + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.13.15: + resolution: + { + integrity: sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==, + } + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.13.15: + resolution: + { + integrity: sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==, + } + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.13.15: + resolution: + { + integrity: sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==, + } + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.13.15: + resolution: + { + integrity: sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==, + } + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.13.15: + resolution: + { + integrity: sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==, + } + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.13.15: + resolution: + { + integrity: sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==, + } + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.13.15: + resolution: + { + integrity: sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==, + } + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.13.15: + resolution: + { + integrity: sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==, + } + hasBin: true + requiresBuild: true + optionalDependencies: + esbuild-android-arm64: 0.13.15 + esbuild-darwin-64: 0.13.15 + esbuild-darwin-arm64: 0.13.15 + esbuild-freebsd-64: 0.13.15 + esbuild-freebsd-arm64: 0.13.15 + esbuild-linux-32: 0.13.15 + esbuild-linux-64: 0.13.15 + esbuild-linux-arm: 0.13.15 + esbuild-linux-arm64: 0.13.15 + esbuild-linux-mips64le: 0.13.15 + esbuild-linux-ppc64le: 0.13.15 + esbuild-netbsd-64: 0.13.15 + esbuild-openbsd-64: 0.13.15 + esbuild-sunos-64: 0.13.15 + esbuild-windows-32: 0.13.15 + esbuild-windows-64: 0.13.15 + esbuild-windows-arm64: 0.13.15 + dev: true + + /escalade/3.1.1: + resolution: + { + integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==, + } + engines: {node: ">=6"} + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + engines: {node: ">=0.8.0"} + dev: true + + /estree-walker/2.0.2: + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, + } + dev: true + + /fill-range/7.0.1: + resolution: + { + integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==, + } + engines: {node: ">=8"} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /follow-redirects/1.14.7: + resolution: + { + integrity: sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==, + } + engines: {node: ">=4.0"} + peerDependencies: + debug: "*" + peerDependenciesMeta: + debug: + optional: true + dev: false + + /fsevents/2.3.2: + resolution: + { + integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, + } + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: + { + integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==, + } + dev: true + + /gensync/1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: {node: ">=6.9.0"} + dev: true + + /glob-parent/5.1.2: + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: {node: ">= 6"} + dependencies: + is-glob: 4.0.3 + dev: true + + /globals/11.12.0: + resolution: + { + integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, + } + engines: {node: ">=4"} + dev: true + + /has-flag/3.0.0: + resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + engines: {node: ">=4"} + dev: true + + /has/1.0.3: + resolution: + { + integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==, + } + engines: {node: ">= 0.4.0"} + dependencies: + function-bind: 1.1.1 + dev: true + + /immutable/4.0.0: + resolution: + { + integrity: sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==, + } + 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.1: + resolution: + { + integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==, + } + 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.1: + resolution: + { + integrity: sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==, + } + 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.2.0: + resolution: + { + integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==, + } + 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.1: + resolution: + { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, + } + engines: {node: ">=8.6"} + dev: true + + /postcss/8.4.6: + resolution: + { + integrity: sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==, + } + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.2.0 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /preact/10.6.5: + resolution: + { + integrity: sha512-i+LXM6JiVjQXSt2jG2vZZFapGpCuk1fl8o6ii3G84MA3xgj686FKjs4JFDkmUVhtxyq21+4ay74zqPykz9hU6w==, + } + dev: false + + /prettier/2.5.1: + resolution: + { + integrity: sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==, + } + 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.1 + dev: true + + /resolve/1.22.0: + resolution: + { + integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==, + } + hasBin: true + dependencies: + is-core-module: 2.8.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /rollup/2.67.0: + resolution: + { + integrity: sha512-W83AaERwvDiHwHEF/dfAfS3z1Be5wf7n+pO3ZAO5IQadCT2lBTr7WQ2MwZZe+nodbD+n3HtC4OCOAdsOPPcKZQ==, + } + 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.49.7: + resolution: + { + integrity: sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==, + } + engines: {node: ">=12.0.0"} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.0.0 + source-map-js: 1.0.2 + dev: true + + /semver/6.3.0: + resolution: + { + integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==, + } + hasBin: true + dev: true + + /source-map-js/1.0.2: + resolution: + { + integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, + } + engines: {node: ">=0.10.0"} + dev: true + + /source-map/0.5.7: + resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} + engines: {node: ">=0.10.0"} + dev: true + + /sourcemap-codec/1.4.8: + resolution: + { + integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==, + } + dev: true + + /supports-color/5.5.0: + resolution: + { + integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==, + } + engines: {node: ">=4"} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: + { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, + } + engines: {node: ">= 0.4"} + 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.5: + resolution: + { + integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==, + } + engines: {node: ">=4.2.0"} + hasBin: true + dev: true + + /vite/2.7.13_sass@1.49.7: + resolution: + { + integrity: sha512-Mq8et7f3aK0SgSxjDNfOAimZGW9XryfHRa/uV0jseQSilg+KhYDSoNb9h1rknOy6SuMkvNDLKCYAYYUMCE+IgQ==, + } + 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.15 + postcss: 8.4.6 + resolve: 1.22.0 + rollup: 2.67.0 + sass: 1.49.7 + optionalDependencies: + fsevents: 2.3.2 + dev: true diff --git a/ui/src/components/app.tsx b/ui/src/components/app.tsx new file mode 100644 index 0000000..3e0efc5 --- /dev/null +++ b/ui/src/components/app.tsx @@ -0,0 +1,10 @@ +import {Component} from "preact" +// import UpdaterView from "./Updater/UpdaterView" +// import logo from "../assets/logo.svg" +import {getConfig} from "../util/config" + +export default class App extends Component { + render() { + return
{getConfig().version}
+ } +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 0000000..66f7e6b --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,5 @@ +import {render} from "preact" +import App from "./components/app" +import "./style/index.scss" + +render(, document.getElementById("app")!) diff --git a/ui/src/preact.d.ts b/ui/src/preact.d.ts new file mode 100644 index 0000000..ac79d62 --- /dev/null +++ b/ui/src/preact.d.ts @@ -0,0 +1 @@ +import JSX = preact.JSX diff --git a/ui/src/style/index.scss b/ui/src/style/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/ui/src/tsgrain-client/.gitignore b/ui/src/tsgrain-client/.gitignore new file mode 100644 index 0000000..149b576 --- /dev/null +++ b/ui/src/tsgrain-client/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/ui/src/tsgrain-client/.npmignore b/ui/src/tsgrain-client/.npmignore new file mode 100644 index 0000000..999d88d --- /dev/null +++ b/ui/src/tsgrain-client/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/ui/src/tsgrain-client/.openapi-generator-ignore b/ui/src/tsgrain-client/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/ui/src/tsgrain-client/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# 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 diff --git a/ui/src/tsgrain-client/.openapi-generator/FILES b/ui/src/tsgrain-client/.openapi-generator/FILES new file mode 100644 index 0000000..16b445e --- /dev/null +++ b/ui/src/tsgrain-client/.openapi-generator/FILES @@ -0,0 +1,9 @@ +.gitignore +.npmignore +.openapi-generator-ignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/ui/src/tsgrain-client/.openapi-generator/VERSION b/ui/src/tsgrain-client/.openapi-generator/VERSION new file mode 100644 index 0000000..7d3cdbf --- /dev/null +++ b/ui/src/tsgrain-client/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.3.1 \ No newline at end of file diff --git a/ui/src/tsgrain-client/api.ts b/ui/src/tsgrain-client/api.ts new file mode 100644 index 0000000..75fba97 --- /dev/null +++ b/ui/src/tsgrain-client/api.ts @@ -0,0 +1,1455 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * TSGRain WebUI + * REST API for the TSGRain WebUI + * + * 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" + +/** + * Zustand des Automatikmodus + * @export + * @interface AutoMode + */ +export interface AutoMode { + /** + * + * @type {boolean} + * @memberof AutoMode + */ + state: boolean +} +/** + * Aktuelle Systemzeit/Zeitzone + * @export + * @interface ConfigTime + */ +export interface ConfigTime { + /** + * Aktuelle Systemzeit + * @type {number} + * @memberof ConfigTime + */ + time: number + /** + * Aktuelle Zeitzone + * @type {string} + * @memberof ConfigTime + */ + timezone: string +} +/** + * Manuelle Bewässerungszeit in Sekunden + * @export + * @interface DefaultIrrigationTime + */ +export interface DefaultIrrigationTime { + /** + * + * @type {number} + * @memberof DefaultIrrigationTime + */ + time: number +} +/** + * + * @export + * @interface InlineObject + */ +export interface InlineObject { + /** + * + * @type {boolean} + * @memberof InlineObject + */ + state: boolean +} +/** + * + * @export + * @interface InlineObject1 + */ +export interface InlineObject1 { + /** + * Aufgabe stoppen/aus der Warteschlange entfernen, wenn sie bereits läuft oder sich in der Warteschlange befindet. + * @type {boolean} + * @memberof InlineObject1 + */ + cancelling: boolean + /** + * Bewässerungsdauer in Sekunden (0: Standarddauer) + * @type {number} + * @memberof InlineObject1 + */ + duration: number + /** + * Aufgabe in die Warteschlange einreihen, wenn sie nicht sofort ausgeführt werden kann. + * @type {boolean} + * @memberof InlineObject1 + */ + queuing: boolean + /** + * Nummer der Bewässerungszone + * @type {number} + * @memberof InlineObject1 + */ + zone_id: number +} +/** + * Job stellt einen Bewässerungszeitplan dar. + * @export + * @interface Job + */ +export interface Job { + /** + * Zritstempel des Bewässerungsjobs + * @type {number} + * @memberof Job + */ + date: number + /** + * Bewässerungsdauer in Sekunden + * @type {number} + * @memberof Job + */ + duration: number + /** + * Job aktiviert? + * @type {boolean} + * @memberof Job + */ + enable: boolean + /** + * ID des Jobs + * @type {number} + * @memberof Job + */ + id: number + /** + * Job täglich wiederholen + * @type {boolean} + * @memberof Job + */ + repeat: boolean + /** + * Zu bewässernde Zonen + * @type {Array} + * @memberof Job + */ + zones: Array +} +/** + * + * @export + * @interface JobID + */ +export interface JobID { + /** + * + * @type {number} + * @memberof JobID + */ + id?: number +} +/** + * The Error contains error relevant information. + * @export + * @interface ModelError + */ +export interface ModelError { + /** + * The general error message according to HTTP specification. + * @type {string} + * @memberof ModelError + */ + error: string + /** + * Concrete error message. + * @type {string} + * @memberof ModelError + */ + msg: string + /** + * The http error code. + * @type {number} + * @memberof ModelError + */ + status_code: number +} +/** + * NewJob stellt einen zu erstellenden Bewässerungszeitplan dar. + * @export + * @interface NewJob + */ +export interface NewJob { + /** + * Zritstempel des Bewässerungsjobs + * @type {number} + * @memberof NewJob + */ + date: number + /** + * Bewässerungsdauer in Sekunden + * @type {number} + * @memberof NewJob + */ + duration: number + /** + * Job aktiviert? + * @type {boolean} + * @memberof NewJob + */ + enable: boolean + /** + * Job täglich wiederholen + * @type {boolean} + * @memberof NewJob + */ + repeat: boolean + /** + * Zu bewässernde Zonen + * @type {Array} + * @memberof NewJob + */ + zones: Array +} +/** + * StatusMessage contains the status of an operation. + * @export + * @interface StatusMessage + */ +export interface StatusMessage { + /** + * Status message text + * @type {string} + * @memberof StatusMessage + */ + msg: string + /** + * Is operation successful? + * @type {boolean} + * @memberof StatusMessage + */ + success: boolean +} +/** + * Task stellt eine Bewässerungsaufgabe dar. + * @export + * @interface Task + */ +export interface Task { + /** + * Zeitstempel, wann die Bewässerung beendet sein wird + * @type {number} + * @memberof Task + */ + datetime_finished: number + /** + * Zeitstempel, wann die Bewässerung gestartet wurde + * @type {number} + * @memberof Task + */ + datetime_started: number + /** + * Bewässerungsdauer in Sekunden + * @type {number} + * @memberof Task + */ + duration: number + /** + * Quelle der Bewässerungsaufgabe (0: MANUAL, 1: SCHEDULE) + * @type {number} + * @memberof Task + */ + source: number + /** + * Nummer der Bewässerungszone + * @type {number} + * @memberof Task + */ + zone_id: number +} +/** + * TaskList ist eine Liste der aktuell laufenden Bewässerungsaufgaben. + * @export + * @interface TaskList + */ +export interface TaskList { + /** + * Automatikmodus aktiv + * @type {boolean} + * @memberof TaskList + */ + auto_mode: boolean + /** + * Aktueller Zeitstempel + * @type {number} + * @memberof TaskList + */ + now: number + /** + * Liste der laufenden Tasks + * @type {Array} + * @memberof TaskList + */ + tasks: Array +} +/** + * TaskRequestResult wird beim Starten eines Tasks zurückgegeben + * @export + * @interface TaskRequestResult + */ +export interface TaskRequestResult { + /** + * + * @type {boolean} + * @memberof TaskRequestResult + */ + Started?: boolean + /** + * + * @type {boolean} + * @memberof TaskRequestResult + */ + Stopped?: boolean +} + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Erstelle einen neuen Zeitplan. + * @param {NewJob} job Neuer Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createJob: async ( + job: NewJob, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'job' is not null or undefined + assertParamExists("createJob", "job", job) + const localVarPath = `/job` + // 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 + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + job, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * + * @summary Lösche einen gespeicherten Zeitplan. + * @param {number} [id] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteJob: async ( + id?: number, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/job` + // 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: "DELETE", + ...baseOptions, + ...options, + } + const localVarHeaderParameter = {} as any + const localVarQueryParameter = {} as any + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + id, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * + * @summary Rufe ab, ob der Automatikmodus aktiviert ist. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAutoMode: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/config/auto` + // 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, + } + }, + /** + * Rufe die aktuelle Systemzeit/Zeitzone ab + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getConfigTime: async ( + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/config/time` + // 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, + } + }, + /** + * + * @summary Rufe die Standardzeit bei manueller Bewässerung ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDefaultIrrigationTime: async ( + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/config/defaultIrrigationTime` + // 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, + } + }, + /** + * + * @summary Rufe einen gespeicherten Zeitplan ab. + * @param {number} id Job-ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getJob: async ( + id: number, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists("getJob", "id", id) + const localVarPath = `/job` + // 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 + + if (id !== undefined) { + localVarQueryParameter["id"] = id + } + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * + * @summary Rufe alle gespeicherten Zeitpläne ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getJobs: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/jobs` + // 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, + } + }, + /** + * Automatikmodus aktivieren/deaktivieren + * @param {InlineObject} [state] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + setAutoMode: async ( + state?: InlineObject, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/config/auto` + // 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 + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + state, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * Automatikmodus aktivieren/deaktivieren + * @param {ConfigTime} [configTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + setConfigTime: async ( + configTime?: ConfigTime, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/config/time` + // 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 + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + configTime, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * + * @summary Setze die die Standardzeit bei manueller Bewässerung. + * @param {DefaultIrrigationTime} [defaultIrrigationTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + setDefaultIrrigationTime: async ( + defaultIrrigationTime?: DefaultIrrigationTime, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/config/defaultIrrigationTime` + // 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 + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + defaultIrrigationTime, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * Starte/stoppe manuell eine neue Bewässerungsaufgabe + * @param {InlineObject1} [taskRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + startManualTask: async ( + taskRequest?: InlineObject1, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/task/manual` + // 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 + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + taskRequest, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + /** + * + * @summary Aktualisiere einen gespeicherten Zeitplan. + * @param {Job} job Aktualisierter Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateJob: async ( + job: Job, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'job' is not null or undefined + assertParamExists("updateJob", "job", job) + const localVarPath = `/job` + // 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: "PUT", ...baseOptions, ...options} + const localVarHeaderParameter = {} as any + const localVarQueryParameter = {} as any + + localVarHeaderParameter["Content-Type"] = "application/json" + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + localVarRequestOptions.data = serializeDataIfNeeded( + job, + localVarRequestOptions, + configuration + ) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + } +} + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Erstelle einen neuen Zeitplan. + * @param {NewJob} job Neuer Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createJob( + job: NewJob, + options?: AxiosRequestConfig + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createJob( + job, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Lösche einen gespeicherten Zeitplan. + * @param {number} [id] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteJob( + id?: number, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteJob( + id, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Rufe ab, ob der Automatikmodus aktiviert ist. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAutoMode( + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAutoMode( + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * Rufe die aktuelle Systemzeit/Zeitzone ab + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getConfigTime( + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigTime( + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Rufe die Standardzeit bei manueller Bewässerung ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getDefaultIrrigationTime( + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.getDefaultIrrigationTime(options) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Rufe einen gespeicherten Zeitplan ab. + * @param {number} id Job-ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getJob( + id: number, + options?: AxiosRequestConfig + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getJob( + id, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Rufe alle gespeicherten Zeitpläne ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getJobs( + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise> + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.getJobs(options) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * Automatikmodus aktivieren/deaktivieren + * @param {InlineObject} [state] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async setAutoMode( + state?: InlineObject, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.setAutoMode( + state, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * Automatikmodus aktivieren/deaktivieren + * @param {ConfigTime} [configTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async setConfigTime( + configTime?: ConfigTime, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.setConfigTime( + configTime, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Setze die die Standardzeit bei manueller Bewässerung. + * @param {DefaultIrrigationTime} [defaultIrrigationTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async setDefaultIrrigationTime( + defaultIrrigationTime?: DefaultIrrigationTime, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.setDefaultIrrigationTime( + defaultIrrigationTime, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * Starte/stoppe manuell eine neue Bewässerungsaufgabe + * @param {InlineObject1} [taskRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async startManualTask( + taskRequest?: InlineObject1, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.startManualTask( + taskRequest, + options + ) + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ) + }, + /** + * + * @summary Aktualisiere einen gespeicherten Zeitplan. + * @param {Job} job Aktualisierter Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateJob( + job: Job, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateJob( + job, + 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 { + /** + * + * @summary Erstelle einen neuen Zeitplan. + * @param {NewJob} job Neuer Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createJob(job: NewJob, options?: any): AxiosPromise { + return localVarFp + .createJob(job, options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Lösche einen gespeicherten Zeitplan. + * @param {number} [id] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteJob(id?: number, options?: any): AxiosPromise { + return localVarFp + .deleteJob(id, options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Rufe ab, ob der Automatikmodus aktiviert ist. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAutoMode(options?: any): AxiosPromise { + return localVarFp + .getAutoMode(options) + .then((request) => request(axios, basePath)) + }, + /** + * Rufe die aktuelle Systemzeit/Zeitzone ab + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getConfigTime(options?: any): AxiosPromise { + return localVarFp + .getConfigTime(options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Rufe die Standardzeit bei manueller Bewässerung ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDefaultIrrigationTime(options?: any): AxiosPromise { + return localVarFp + .getDefaultIrrigationTime(options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Rufe einen gespeicherten Zeitplan ab. + * @param {number} id Job-ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getJob(id: number, options?: any): AxiosPromise { + return localVarFp + .getJob(id, options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Rufe alle gespeicherten Zeitpläne ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getJobs(options?: any): AxiosPromise> { + return localVarFp + .getJobs(options) + .then((request) => request(axios, basePath)) + }, + /** + * Automatikmodus aktivieren/deaktivieren + * @param {InlineObject} [state] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + setAutoMode(state?: InlineObject, options?: any): AxiosPromise { + return localVarFp + .setAutoMode(state, options) + .then((request) => request(axios, basePath)) + }, + /** + * Automatikmodus aktivieren/deaktivieren + * @param {ConfigTime} [configTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + setConfigTime( + configTime?: ConfigTime, + options?: any + ): AxiosPromise { + return localVarFp + .setConfigTime(configTime, options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Setze die die Standardzeit bei manueller Bewässerung. + * @param {DefaultIrrigationTime} [defaultIrrigationTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + setDefaultIrrigationTime( + defaultIrrigationTime?: DefaultIrrigationTime, + options?: any + ): AxiosPromise { + return localVarFp + .setDefaultIrrigationTime(defaultIrrigationTime, options) + .then((request) => request(axios, basePath)) + }, + /** + * Starte/stoppe manuell eine neue Bewässerungsaufgabe + * @param {InlineObject1} [taskRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + startManualTask( + taskRequest?: InlineObject1, + options?: any + ): AxiosPromise { + return localVarFp + .startManualTask(taskRequest, options) + .then((request) => request(axios, basePath)) + }, + /** + * + * @summary Aktualisiere einen gespeicherten Zeitplan. + * @param {Job} job Aktualisierter Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateJob(job: Job, options?: any): AxiosPromise { + return localVarFp + .updateJob(job, options) + .then((request) => request(axios, basePath)) + }, + } +} + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI { + /** + * + * @summary Erstelle einen neuen Zeitplan. + * @param {NewJob} job Neuer Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public createJob(job: NewJob, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .createJob(job, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Lösche einen gespeicherten Zeitplan. + * @param {number} [id] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public deleteJob(id?: number, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .deleteJob(id, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Rufe ab, ob der Automatikmodus aktiviert ist. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getAutoMode(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .getAutoMode(options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * Rufe die aktuelle Systemzeit/Zeitzone ab + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getConfigTime(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .getConfigTime(options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Rufe die Standardzeit bei manueller Bewässerung ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getDefaultIrrigationTime(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .getDefaultIrrigationTime(options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Rufe einen gespeicherten Zeitplan ab. + * @param {number} id Job-ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getJob(id: number, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .getJob(id, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Rufe alle gespeicherten Zeitpläne ab. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getJobs(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .getJobs(options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * Automatikmodus aktivieren/deaktivieren + * @param {InlineObject} [state] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public setAutoMode(state?: InlineObject, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .setAutoMode(state, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * Automatikmodus aktivieren/deaktivieren + * @param {ConfigTime} [configTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public setConfigTime(configTime?: ConfigTime, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .setConfigTime(configTime, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Setze die die Standardzeit bei manueller Bewässerung. + * @param {DefaultIrrigationTime} [defaultIrrigationTime] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public setDefaultIrrigationTime( + defaultIrrigationTime?: DefaultIrrigationTime, + options?: AxiosRequestConfig + ) { + return DefaultApiFp(this.configuration) + .setDefaultIrrigationTime(defaultIrrigationTime, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * Starte/stoppe manuell eine neue Bewässerungsaufgabe + * @param {InlineObject1} [taskRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public startManualTask(taskRequest?: InlineObject1, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .startManualTask(taskRequest, options) + .then((request) => request(this.axios, this.basePath)) + } + + /** + * + * @summary Aktualisiere einen gespeicherten Zeitplan. + * @param {Job} job Aktualisierter Job + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public updateJob(job: Job, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration) + .updateJob(job, options) + .then((request) => request(this.axios, this.basePath)) + } +} diff --git a/ui/src/tsgrain-client/base.ts b/ui/src/tsgrain-client/base.ts new file mode 100644 index 0000000..d5101da --- /dev/null +++ b/ui/src/tsgrain-client/base.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * TSGRain WebUI + * REST API for the TSGRain WebUI + * + * 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".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) + } +} diff --git a/ui/src/tsgrain-client/common.ts b/ui/src/tsgrain-client/common.ts new file mode 100644 index 0000000..74161a8 --- /dev/null +++ b/ui/src/tsgrain-client/common.ts @@ -0,0 +1,181 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * TSGRain WebUI + * REST API for the TSGRain WebUI + * + * 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 >( + axios: AxiosInstance = globalAxios, + basePath: string = BASE_PATH + ) => { + const axiosRequestArgs = { + ...axiosArgs.options, + url: (configuration?.basePath || basePath) + axiosArgs.url, + } + return axios.request(axiosRequestArgs) + } +} diff --git a/ui/src/tsgrain-client/configuration.ts b/ui/src/tsgrain-client/configuration.ts new file mode 100644 index 0000000..fdf126c --- /dev/null +++ b/ui/src/tsgrain-client/configuration.ts @@ -0,0 +1,123 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * TSGRain WebUI + * REST API for the TSGRain WebUI + * + * 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 + | ((name: string) => string) + | ((name: string) => Promise) + username?: string + password?: string + accessToken?: + | string + | Promise + | ((name?: string, scopes?: string[]) => string) + | ((name?: string, scopes?: string[]) => Promise) + basePath?: string + baseOptions?: any + formDataCtor?: new () => any +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: + | string + | Promise + | ((name: string) => string) + | ((name: string) => Promise) + /** + * 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 + | ((name?: string, scopes?: string[]) => string) + | ((name?: string, scopes?: string[]) => Promise) + /** + * 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") + ) + } +} diff --git a/ui/src/tsgrain-client/git_push.sh b/ui/src/tsgrain-client/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/ui/src/tsgrain-client/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/ui/src/tsgrain-client/index.ts b/ui/src/tsgrain-client/index.ts new file mode 100644 index 0000000..2812ea5 --- /dev/null +++ b/ui/src/tsgrain-client/index.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * TSGRain WebUI + * REST API for the TSGRain WebUI + * + * 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" diff --git a/ui/src/util/apiUrls.ts b/ui/src/util/apiUrls.ts new file mode 100644 index 0000000..cceed04 --- /dev/null +++ b/ui/src/util/apiUrls.ts @@ -0,0 +1,20 @@ +import {Configuration, DefaultApi} from "../tsgrain-client" + +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` + +let apicfg = new Configuration({ + basePath: apiUrl, +}) + +const sebraucApi = new DefaultApi(apicfg) + +export {apiUrl, wsUrl, sebraucApi} diff --git a/ui/src/util/config.ts b/ui/src/util/config.ts new file mode 100644 index 0000000..f5950c2 --- /dev/null +++ b/ui/src/util/config.ts @@ -0,0 +1,27 @@ +export interface Config { + version: string + n_zones: number +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +declare global { + interface Window { + config?: any + } +} + +function isConfig(object: any): object is Config { + return typeof object === "object" && "version" in object +} + +export function getConfig(): Config { + if (isConfig(window.config)) { + return window.config + } + + console.error("App config not found") + return { + version: "dev", + n_zones: 0, + } +} diff --git a/ui/src/util/functions.ts b/ui/src/util/functions.ts new file mode 100644 index 0000000..18179e2 --- /dev/null +++ b/ui/src/util/functions.ts @@ -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} diff --git a/ui/src/util/websocket.ts b/ui/src/util/websocket.ts new file mode 100644 index 0000000..8d75673 --- /dev/null +++ b/ui/src/util/websocket.ts @@ -0,0 +1,92 @@ +import {wsUrl} from "./apiUrls" + +class WebsocketAPI { + private static ws: WebsocketAPI | undefined + + private conn: WebSocket | undefined + private wsConnected: boolean + + private clients: Set + + private constructor() { + this.clients = new Set() + this.wsConnected = false + + if (window.WebSocket) { + this.connect() + } else { + console.log("Your browser does not support WebSockets") + } + } + + private setStatus(wsConnected: boolean) { + if (wsConnected !== this.wsConnected) { + this.wsConnected = wsConnected + this.clients.forEach((client) => { + client.statusCallback(this.wsConnected) + }) + } + } + + private connect() { + this.conn = new WebSocket(wsUrl) + this.conn.onopen = () => { + this.setStatus(true) + console.log("WS connected") + } + this.conn.onclose = () => { + this.setStatus(false) + console.log("WS connection closed") + window.setTimeout(() => this.connect(), 3000) + } + this.conn.onmessage = (evt) => { + this.clients.forEach((client) => { + client.msgCallback(evt) + }) + } + } + + static Get(): WebsocketAPI { + if (this.ws === undefined) { + this.ws = new WebsocketAPI() + } + return this.ws + } + + isConnected(): boolean { + return this.wsConnected + } + + addClient(client: WebsocketClient) { + console.log("added client", client) + this.clients.add(client) + } + + removeClient(client: WebsocketClient) { + console.log("removed client", client) + this.clients.delete(client) + } +} + +export default class WebsocketClient { + statusCallback: (wsConnected: boolean) => void + msgCallback: (evt: MessageEvent) => void + + constructor( + statusCallback: (wsConnected: boolean) => void, + msgCallback: (evt: MessageEvent) => void + ) { + this.statusCallback = statusCallback + this.msgCallback = msgCallback + + this.api().addClient(this) + } + + api(): WebsocketAPI { + return WebsocketAPI.Get() + } + + destroy() { + this.api().removeClient(this) + } +} 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..3556ea3 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,66 @@ +package ui + +import ( + "bytes" + "embed" + "encoding/json" + "io/fs" + "net/http" + + "code.thetadev.de/TSGRain/WebUI/src/server/middleware" + "code.thetadev.de/TSGRain/WebUI/src/util" + "github.com/gin-gonic/gin" +) + +const distDir = "dist" + +//go:embed dist/** +var assets embed.FS + +type uiConfig struct { + Version string `json:"version"` + NZones int32 `json:"n_zones"` +} + +func subFS(fsys fs.FS, dir string) fs.FS { + sub, err := fs.Sub(fsys, dir) + if err != nil { + panic(err) + } + return sub +} + +func distFS() fs.FS { + return subFS(assets, distDir) +} + +func Register(r gin.IRouter, nZones int32) { + indexHandler := getIndexHandler(nZones) + + uiAssets := r.Group("/assets", middleware.Cache) + + r.GET("/", indexHandler) + r.GET("/index.html", indexHandler) + + uiAssets.StaticFS("/", http.FS(subFS(distFS(), "assets"))) +} + +func getIndexHandler(nZones int32) gin.HandlerFunc { + content, err := fs.ReadFile(distFS(), "index.html") + if err != nil { + panic(err) + } + + uiConfigBytes, err := json.Marshal(uiConfig{ + Version: util.Version(), + NZones: nZones, + }) + if err != nil { + panic(err) + } + content = bytes.ReplaceAll(content, []byte("\"%CONFIG%\""), uiConfigBytes) + + return func(c *gin.Context) { + c.Data(200, "text/html", content) + } +} diff --git a/ui/ui_test.go b/ui/ui_test.go new file mode 100644 index 0000000..53cc6a7 --- /dev/null +++ b/ui/ui_test.go @@ -0,0 +1,87 @@ +package ui + +import ( + "net/http" + "net/http/httptest" + "os" + "path" + "regexp" + "testing" + + "code.thetadev.de/TSGRain/WebUI/src/fixtures" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestUI(t *testing.T) { + tests := []struct { + name string + path string + contains string + cached bool + }{ + { + name: "index_html", + path: "/", + contains: "TSGRain", + cached: false, + }, + { + name: "index_html2", + path: "/index.html", + contains: "TSGRain", + cached: false, + }, + { + name: "index_js", + path: path.Join("/assets", getIndexJS()), + contains: "app", + cached: true, + }, + } + + router := gin.New() + Register(router, 3) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", tt.path, nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), tt.contains) + + ccHeader := w.Header().Get("Cache-Control") + + if tt.cached { + assert.Equal(t, "public, max-age=604800, immutable", ccHeader) + } else { + assert.Equal(t, "", ccHeader) + } + }) + } +} + +func getIndexJS() string { + baseDir := "ui/dist/assets" + indexExp := regexp.MustCompile(`index\.[0-9a-f]{8}\.js`) + + fixtures.CdProjectRoot() + distDir, err := os.Open(baseDir) + if err != nil { + panic(err) + } + + list, err := distDir.Readdir(-1) + if err != nil { + panic(err) + } + + for _, f := range list { + if indexExp.MatchString(f.Name()) { + return f.Name() + } + } + panic("no index.js found") +} 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()], +})