Compare commits
3 commits
60034b4815
...
834092a101
Author | SHA1 | Date | |
---|---|---|---|
834092a101 | |||
207db1c7a8 | |||
73208b6f51 |
43 changed files with 5060 additions and 113 deletions
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
||||
|
|
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
|
@ -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": ["<node_internals>/**"],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"name": "Test program",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/tmp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
3
Makefile
3
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:
|
||||
|
|
|
@ -87,6 +87,7 @@ message Task {
|
|||
message TaskList {
|
||||
repeated Task tasks = 1;
|
||||
Timestamp now = 2;
|
||||
bool auto_mode = 3;
|
||||
}
|
||||
|
||||
message Job {
|
||||
|
|
74
src/model/job.go
Normal file
74
src/model/job.go
Normal file
|
@ -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"`
|
||||
}
|
38
src/model/options.go
Normal file
38
src/model/options.go
Normal file
|
@ -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"`
|
||||
}
|
65
src/model/task.go
Normal file
65
src/model/task.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SEBRAUC API documentation</title>
|
||||
<title>TSGRain WebUI API documentation</title>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
1
ui/.env.development
Normal file
1
ui/.env.development
Normal file
|
@ -0,0 +1 @@
|
|||
VITE_API_HOST=127.0.0.1:8001
|
5
ui/.gitignore
vendored
Normal file
5
ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
4
ui/.prettierignore
Normal file
4
ui/.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
tmp
|
||||
.tmp
|
17
ui/index.html
Normal file
17
ui/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TSGRain</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You have to enable JavaScript to use TSGRain.</noscript>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.config = "%CONFIG%"
|
||||
</script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
24
ui/package.json
Normal file
24
ui/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
1242
ui/pnpm-lock.yaml
Normal file
1242
ui/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
10
ui/src/components/app.tsx
Normal file
10
ui/src/components/app.tsx
Normal file
|
@ -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 <div>{getConfig().version}</div>
|
||||
}
|
||||
}
|
5
ui/src/main.tsx
Normal file
5
ui/src/main.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {render} from "preact"
|
||||
import App from "./components/app"
|
||||
import "./style/index.scss"
|
||||
|
||||
render(<App />, document.getElementById("app")!)
|
1
ui/src/preact.d.ts
vendored
Normal file
1
ui/src/preact.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
import JSX = preact.JSX
|
0
ui/src/style/index.scss
Normal file
0
ui/src/style/index.scss
Normal file
4
ui/src/tsgrain-client/.gitignore
vendored
Normal file
4
ui/src/tsgrain-client/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
1
ui/src/tsgrain-client/.npmignore
Normal file
1
ui/src/tsgrain-client/.npmignore
Normal file
|
@ -0,0 +1 @@
|
|||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
23
ui/src/tsgrain-client/.openapi-generator-ignore
Normal file
23
ui/src/tsgrain-client/.openapi-generator-ignore
Normal file
|
@ -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
|
9
ui/src/tsgrain-client/.openapi-generator/FILES
Normal file
9
ui/src/tsgrain-client/.openapi-generator/FILES
Normal file
|
@ -0,0 +1,9 @@
|
|||
.gitignore
|
||||
.npmignore
|
||||
.openapi-generator-ignore
|
||||
api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
git_push.sh
|
||||
index.ts
|
1
ui/src/tsgrain-client/.openapi-generator/VERSION
Normal file
1
ui/src/tsgrain-client/.openapi-generator/VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
5.3.1
|
1455
ui/src/tsgrain-client/api.ts
Normal file
1455
ui/src/tsgrain-client/api.ts
Normal file
File diff suppressed because it is too large
Load diff
74
ui/src/tsgrain-client/base.ts
Normal file
74
ui/src/tsgrain-client/base.ts
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
181
ui/src/tsgrain-client/common.ts
Normal file
181
ui/src/tsgrain-client/common.ts
Normal file
|
@ -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 <T = unknown, R = AxiosResponse<T>>(
|
||||
axios: AxiosInstance = globalAxios,
|
||||
basePath: string = BASE_PATH
|
||||
) => {
|
||||
const axiosRequestArgs = {
|
||||
...axiosArgs.options,
|
||||
url: (configuration?.basePath || basePath) + axiosArgs.url,
|
||||
}
|
||||
return axios.request<T, R>(axiosRequestArgs)
|
||||
}
|
||||
}
|
123
ui/src/tsgrain-client/configuration.ts
Normal file
123
ui/src/tsgrain-client/configuration.ts
Normal file
|
@ -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<string>
|
||||
| ((name: string) => string)
|
||||
| ((name: string) => Promise<string>)
|
||||
username?: string
|
||||
password?: string
|
||||
accessToken?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name?: string, scopes?: string[]) => string)
|
||||
| ((name?: string, scopes?: string[]) => Promise<string>)
|
||||
basePath?: string
|
||||
baseOptions?: any
|
||||
formDataCtor?: new () => any
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
/**
|
||||
* parameter for apiKey security
|
||||
* @param name security name
|
||||
* @memberof Configuration
|
||||
*/
|
||||
apiKey?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name: string) => string)
|
||||
| ((name: string) => Promise<string>)
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
username?: string
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
password?: string
|
||||
/**
|
||||
* parameter for oauth2 security
|
||||
* @param name security name
|
||||
* @param scopes oauth2 scope
|
||||
* @memberof Configuration
|
||||
*/
|
||||
accessToken?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name?: string, scopes?: string[]) => string)
|
||||
| ((name?: string, scopes?: string[]) => Promise<string>)
|
||||
/**
|
||||
* override base path
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
basePath?: string
|
||||
/**
|
||||
* base options for axios calls
|
||||
*
|
||||
* @type {any}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
baseOptions?: any
|
||||
/**
|
||||
* The FormData constructor that will be used to create multipart form data
|
||||
* requests. You can inject this here so that execution environments that
|
||||
* do not support the FormData class can still run the generated client.
|
||||
*
|
||||
* @type {new () => FormData}
|
||||
*/
|
||||
formDataCtor?: new () => any
|
||||
|
||||
constructor(param: ConfigurationParameters = {}) {
|
||||
this.apiKey = param.apiKey
|
||||
this.username = param.username
|
||||
this.password = param.password
|
||||
this.accessToken = param.accessToken
|
||||
this.basePath = param.basePath
|
||||
this.baseOptions = param.baseOptions
|
||||
this.formDataCtor = param.formDataCtor
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given MIME is a JSON MIME.
|
||||
* JSON MIME examples:
|
||||
* application/json
|
||||
* application/json; charset=UTF8
|
||||
* APPLICATION/JSON
|
||||
* application/vnd.company+json
|
||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||
* @return True if the given MIME is JSON, false otherwise.
|
||||
*/
|
||||
public isJsonMime(mime: string): boolean {
|
||||
const jsonMime: RegExp = new RegExp(
|
||||
"^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$",
|
||||
"i"
|
||||
)
|
||||
return (
|
||||
mime !== null &&
|
||||
(jsonMime.test(mime) ||
|
||||
mime.toLowerCase() === "application/json-patch+json")
|
||||
)
|
||||
}
|
||||
}
|
57
ui/src/tsgrain-client/git_push.sh
Normal file
57
ui/src/tsgrain-client/git_push.sh
Normal file
|
@ -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'
|
16
ui/src/tsgrain-client/index.ts
Normal file
16
ui/src/tsgrain-client/index.ts
Normal file
|
@ -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"
|
20
ui/src/util/apiUrls.ts
Normal file
20
ui/src/util/apiUrls.ts
Normal file
|
@ -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}
|
27
ui/src/util/config.ts
Normal file
27
ui/src/util/config.ts
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
18
ui/src/util/functions.ts
Normal file
18
ui/src/util/functions.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
function secondsToString(seconds: number): string {
|
||||
const numyears = Math.floor(seconds / 31536000)
|
||||
const numdays = Math.floor((seconds % 31536000) / 86400)
|
||||
const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600)
|
||||
const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60)
|
||||
const numseconds = (((seconds % 31536000) % 86400) % 3600) % 60
|
||||
|
||||
let res = []
|
||||
if (numyears > 0) res.push(numyears + "yr")
|
||||
if (numdays > 0) res.push(numdays + "d")
|
||||
if (numhours > 0) res.push(numhours + "h")
|
||||
if (numminutes > 0) res.push(numminutes + "m")
|
||||
if (seconds < 60) res.push(numseconds + "s")
|
||||
|
||||
return res.join(" ")
|
||||
}
|
||||
|
||||
export {secondsToString}
|
92
ui/src/util/websocket.ts
Normal file
92
ui/src/util/websocket.ts
Normal file
|
@ -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<WebsocketClient>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
1
ui/src/vite-env.d.ts
vendored
Normal file
1
ui/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
21
ui/tsconfig.json
Normal file
21
ui/tsconfig.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
66
ui/ui.go
Normal file
66
ui/ui.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
87
ui/ui_test.go
Normal file
87
ui/ui_test.go
Normal file
|
@ -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")
|
||||
}
|
7
ui/vite.config.ts
Normal file
7
ui/vite.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {defineConfig} from "vite"
|
||||
import preact from "@preact/preset-vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
})
|
Loading…
Reference in a new issue