From b4e677209a19e8eb47c6f8f3ea5cbc55a79ef347 Mon Sep 17 00:00:00 2001 From: Livio Brunner Date: Tue, 17 Oct 2023 19:08:40 +0200 Subject: [PATCH 1/2] refactor: combine label / command endpoint --- api/api.go | 3 +- api/commandController.go | 35 ------------- api/confController.go | 25 +++++++++ api/labelController.go | 87 ------------------------------- api/taskSpoolerController.go | 4 +- internal/user-conf/command.go | 8 --- internal/user-conf/label.go | 39 -------------- internal/user-conf/user-conf.go | 12 +++++ web/api.js | 92 +++++++++++++++++++++++++++++++++ web/command/command-list.js | 32 ++++-------- web/label/label-badge.js | 3 +- web/label/label-filter.js | 2 +- web/label/label.js | 7 --- web/pages/home.js | 40 +++++++------- web/task/task-item.js | 8 ++- web/task/task-list.js | 3 +- web/task/task.js | 25 --------- 17 files changed, 171 insertions(+), 254 deletions(-) delete mode 100644 api/commandController.go create mode 100644 api/confController.go delete mode 100644 api/labelController.go delete mode 100644 internal/user-conf/command.go delete mode 100644 internal/user-conf/label.go create mode 100644 web/api.js delete mode 100644 web/label/label.js delete mode 100644 web/task/task.js diff --git a/api/api.go b/api/api.go index 9837b02..e76735d 100644 --- a/api/api.go +++ b/api/api.go @@ -51,9 +51,8 @@ func Run(args args.TspWebArgs) error { }) }) - LabelController(args, api.PathPrefix("/label").Subrouter()) + ConfigController(args, api.PathPrefix("/config").Subrouter()) TaskSpoolerController(args, api.PathPrefix("/task-spooler").Subrouter()) - CommandController(args, api.PathPrefix("/command").Subrouter()) // SPA spa := util.SpaHandler{StaticFS: Static, StaticPath: "web", IndexPath: "index.html"} diff --git a/api/commandController.go b/api/commandController.go deleted file mode 100644 index 998f2d1..0000000 --- a/api/commandController.go +++ /dev/null @@ -1,35 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "tsp-web/internal/args" - userconf "tsp-web/internal/user-conf" - - "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" -) - -func CommandController(args args.TspWebArgs, r *mux.Router) { - commands := userconf.GetUserConf(args).Commands - if commands == nil || len(commands) == 0 { - return - } - - r.HandleFunc("", func(w http.ResponseWriter, r *http.Request) { - GetCommands(args, w, r) - }).Methods("GET") -} - -func GetCommands(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - commands := userconf.GetCommands(args) - res, err := json.Marshal(commands) - - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(res) -} diff --git a/api/confController.go b/api/confController.go new file mode 100644 index 0000000..d6fb94e --- /dev/null +++ b/api/confController.go @@ -0,0 +1,25 @@ +package api + +import ( + "encoding/json" + "net/http" + "tsp-web/internal/args" + userconf "tsp-web/internal/user-conf" + + "github.com/gorilla/mux" +) + +func ConfigController(args args.TspWebArgs, r *mux.Router) { + r.HandleFunc("", func(w http.ResponseWriter, r *http.Request) { + conf := userconf.GetUserConf(args) + res, err := json.Marshal(conf) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(res) + + }).Methods("GET") +} diff --git a/api/labelController.go b/api/labelController.go deleted file mode 100644 index 3d8771f..0000000 --- a/api/labelController.go +++ /dev/null @@ -1,87 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "tsp-web/internal/args" - userconf "tsp-web/internal/user-conf" - - "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" -) - -func LabelController(args args.TspWebArgs, r *mux.Router) { - r.HandleFunc("", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - GetLabels(args, w, r) - } else if r.Method == "POST" { - PostLabel(args, w, r) - } else if r.Method == "PUT" { - PutLabel(args, w, r) - } else if r.Method == "DELETE" { - DeleteLabel(args, w, r) - } else { - w.WriteHeader(http.StatusMethodNotAllowed) - } - }).Methods("GET", "POST", "PUT", "DELETE") -} - -func GetLabels(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - labels, err := userconf.GetLabels(args) - res, err := json.Marshal(labels) - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(res) -} - -func PostLabel(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - var newLabel userconf.Label - err := json.NewDecoder(r.Body).Decode(&newLabel) - - conf, err := userconf.AddLabel(args, newLabel) - res, err := json.Marshal(conf.Labels) - - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(res) -} - -func PutLabel(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - var newLabel userconf.Label - err := json.NewDecoder(r.Body).Decode(&newLabel) - - conf, err := userconf.UpdateLabel(args, newLabel) - res, err := json.Marshal(conf.Labels) - - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(res) -} - -func DeleteLabel(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - var newLabel userconf.Label - err := json.NewDecoder(r.Body).Decode(&newLabel) - - conf, err := userconf.RemoveLabel(args, newLabel) - res, err := json.Marshal(conf.Labels) - - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(res) -} diff --git a/api/taskSpoolerController.go b/api/taskSpoolerController.go index 512b9d7..9295b16 100644 --- a/api/taskSpoolerController.go +++ b/api/taskSpoolerController.go @@ -49,7 +49,7 @@ func TaskSpoolerController(args args.TspWebArgs, r *mux.Router) { } func GetList(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - labels, _ := userconf.GetLabels(args) + labels := userconf.GetUserConf(args).Labels currentTasks, err := taskspooler.List(args) @@ -118,7 +118,7 @@ func PostExec(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { return } - allCommands := userconf.GetCommands(args) + allCommands := userconf.GetUserConf(args).Commands var foundCommand *userconf.Command for _, c := range allCommands { diff --git a/internal/user-conf/command.go b/internal/user-conf/command.go deleted file mode 100644 index 97ef019..0000000 --- a/internal/user-conf/command.go +++ /dev/null @@ -1,8 +0,0 @@ -package userconf - -import "tsp-web/internal/args" - -func GetCommands(args args.TspWebArgs) []Command { - conf := GetUserConf(args) - return conf.Commands -} diff --git a/internal/user-conf/label.go b/internal/user-conf/label.go deleted file mode 100644 index 5a6baaf..0000000 --- a/internal/user-conf/label.go +++ /dev/null @@ -1,39 +0,0 @@ -package userconf - -import "tsp-web/internal/args" - -func AddLabel(args args.TspWebArgs, label Label) (UserConf, error) { - conf := GetUserConf(args) - conf.Labels = append(conf.Labels, label) - return writeUserConf(args, conf) -} - -func RemoveLabel(args args.TspWebArgs, label Label) (UserConf, error) { - conf := GetUserConf(args) - for i, l := range conf.Labels { - if l.Name == label.Name { - conf.Labels = append(conf.Labels[:i], conf.Labels[i+1:]...) - return writeUserConf(args, conf) - } - } - - return UserConf{}, nil -} - -func UpdateLabel(args args.TspWebArgs, label Label) (UserConf, error) { - conf := GetUserConf(args) - - for i, l := range conf.Labels { - if l.Name == label.Name { - conf.Labels[i] = label - return writeUserConf(args, conf) - } - } - - return UserConf{}, nil -} - -func GetLabels(args args.TspWebArgs) ([]Label, error) { - conf := GetUserConf(args) - return conf.Labels, nil -} diff --git a/internal/user-conf/user-conf.go b/internal/user-conf/user-conf.go index 7ed29ce..16f888b 100644 --- a/internal/user-conf/user-conf.go +++ b/internal/user-conf/user-conf.go @@ -21,6 +21,12 @@ labels: bgColor: '#0C2880' fgColor: 'black' icon: 💤 + +# To use a custom sockets, uncomment the "sockets" block +#sockets: +# - name: "Default" +# - name: "Other" +# path: "/tmp/other.sock" ` type Label struct { @@ -35,9 +41,15 @@ type Command struct { Args []string `yaml:"args"` } +type Socket struct { + Name string `yaml:"name"` + Path string `yaml:"path"` +} + type UserConf struct { Labels []Label `yaml:"labels"` Commands []Command `yaml:"commands"` + Sockets []Socket `yaml:"sockets"` } var cachedConf UserConf = UserConf{} diff --git a/web/api.js b/web/api.js new file mode 100644 index 0000000..8823f2d --- /dev/null +++ b/web/api.js @@ -0,0 +1,92 @@ +// @ts-check +/** + * @typedef {object} Label + * @property {string} Name + * @property {string} BgColor + * @property {string} FgColor + * @property {string} Icon + */ +/** + * @typedef {object} Command + * @property {string} Name + * @property {string} Args + */ +/** + * @typedef {object} Socket + * @property {string} Name + * @property {string} Path + */ +/** + * @typedef {object} Config + * @property {Label[]} Labels + * @property {Command[]} Commands + * @property {Socket[]} Sockets + */ + +/** + * @typedef {'running' | 'queued' | 'finished'} TaskState + */ +/** + * @typedef {object} TaskDetail + * @property {string} ExitStatus + * @property {string} Command + * @property {number} SlotsRequired + * @property {string} EnqueueTime + * @property {string} StartTime + * @property {string} EndTime + * @property {string} TimeRun + */ +/** + * @typedef {object} Task + * @property {string} ID + * @property {TaskState} State + * @property {string} Output + * @property {string} ELevel + * @property {string} Times + * @property {string} Command + * @property {Label} Label + * @property {string} LabelName + * @property {TaskDetail?} Detail + */ + +const taskSpooler = { + /** + * @param {string} id + */ + kill: async (id) => { + await fetch(`/api/v1/task-spooler/kill/${id}`, { method: 'POST' }) + }, + + /** + * + * @returns {Promise} + */ + list: async () => { + return await fetch('/api/v1/task-spooler/list').then(response => response.json()) + }, + + /** + * @param {string} name + * @returns {Promise} + */ + exec: async (name) => { + return await fetch(`/api/v1/task-spooler/exec`, { + method: 'POST', body: JSON.stringify({ Name: name }) + }) + .then(response => response.json()) + } +} + +const config = { + /** + * @returns {Promise} + */ + get: async () => { + return await fetch('/api/v1/config').then(response => response.json()) + } +} + +export const api = { + taskSpooler, + config, +} diff --git a/web/command/command-list.js b/web/command/command-list.js index e6f0daa..4d4a59d 100644 --- a/web/command/command-list.js +++ b/web/command/command-list.js @@ -1,17 +1,19 @@ // @ts-check -import { LitElement, html, css } from 'lit'; - - +import { LitElement, html, css, nothing } from 'lit'; +import { api } from '../api.js'; export class CommandList extends LitElement { constructor() { super(); + /** @type {import('../api.js').Command[]} */ this.commands = []; + this.isLoading = true; } static get properties() { return { - commands: { type: Array, attribute: false } + commands: { type: Array }, + isLoading: { type: Boolean }, } } @@ -24,29 +26,17 @@ export class CommandList extends LitElement { } `; - async #loadCommands() { - this.commands = await fetch('/api/v1/command') - .then(response => response.json()) - } - - firstUpdated(_changedProperties) { - super.firstUpdated(_changedProperties); - this.#loadCommands(); - } #exec(command) { - fetch(`/api/v1/task-spooler/exec`, { - method: 'POST', body: JSON.stringify({ Name: command.Name }) - }) - .then(response => response.json()) - .then(data => { - console.log(data); - window.dispatchEvent(new CustomEvent('task-list-updated')); - }); + api.taskSpooler.exec(command.Name) + .then(() => window.dispatchEvent(new CustomEvent('task-list-updated'))); } render() { + if (this.isLoading) { + return nothing; + } return html`
${this.commands.map((command) => { diff --git a/web/label/label-badge.js b/web/label/label-badge.js index 46ee30d..6e9a971 100644 --- a/web/label/label-badge.js +++ b/web/label/label-badge.js @@ -1,11 +1,10 @@ // @ts-check import { LitElement, html, css } from "lit"; -import './label.js'; export class LabelBadge extends LitElement { constructor() { super(); - /** @type {Label | null} */ + /** @type {import('../api.js').Label | null} */ this.label = null; this.clickable = false; } diff --git a/web/label/label-filter.js b/web/label/label-filter.js index 84490d6..c67a71e 100644 --- a/web/label/label-filter.js +++ b/web/label/label-filter.js @@ -9,7 +9,7 @@ export class LabelFilter extends LitElement { constructor() { super(); - /** @type {Label[]} */ + /** @type {import('../api.js').Label[]} */ this.labels = []; this.isLoading = true; } diff --git a/web/label/label.js b/web/label/label.js deleted file mode 100644 index efa2d5c..0000000 --- a/web/label/label.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @typedef {object} Label - * @property {string} Name - * @property {string} BgColor - * @property {string} FgColor - * @property {string} Icon - */ \ No newline at end of file diff --git a/web/pages/home.js b/web/pages/home.js index e94917b..ecd1209 100644 --- a/web/pages/home.js +++ b/web/pages/home.js @@ -1,24 +1,24 @@ // @ts-check -import { LitElement, html, css } from "lit"; +import { LitElement, html, css, nothing } from "lit"; import '../task/task-list.js' import '../task/task-item.js' import '../label/label-badge.js' import '../label/label-filter.js' -import '../label/label.js' import '../command/command-list.js' import '../card.js' +import { api } from "../api.js"; export class Home extends LitElement { constructor() { super(); - /** @type {Task[]} */ + /** @type {import("../api.js").Task[]} */ this.tasks = []; - /** @type {Task[]} */ + /** @type {import("../api.js").Task[]} */ this.filteredTasks = []; - /** @type {Label[]} */ - this.labels = []; + /** @type {Partial} */ + this.config = {}; this.isLoadingTasks = true; - this.isLoadingLabels = true; + this.isLoadingConfig = true; /** * @type {{ label: string }} */ @@ -52,16 +52,14 @@ export class Home extends LitElement { } async #loadTasks() { - this.tasks = await fetch('/api/v1/task-spooler/list') - .then(response => response.json()) + this.tasks = await api.taskSpooler.list(); this.#filterTasks(); this.isLoadingTasks = false; } - async #loadLabels() { - this.labels = await fetch('/api/v1/label') - .then(response => response.json()) - this.isLoadingLabels = false; + async #loadConfig() { + this.config = await api.config.get(); + this.isLoadingConfig = false; } #filterTasks() { @@ -80,7 +78,7 @@ export class Home extends LitElement { window.dispatchEvent(new CustomEvent('task-list-updated')); }, 2000); window.addEventListener('task-list-updated', async () => await this.#loadTasks()) - await Promise.all([this.#loadTasks(), this.#loadLabels()]) + await Promise.all([this.#loadTasks(), this.#loadConfig()]) } /** @@ -93,13 +91,19 @@ export class Home extends LitElement { render() { + const hasCommands = (this.config.Commands || []).length > 0; + + const commandsCard = html` + + + + ` + return html`
- - - + ${hasCommands ? commandsCard : nothing} - +
diff --git a/web/task/task-item.js b/web/task/task-item.js index 22aa9ce..bb9c3b7 100644 --- a/web/task/task-item.js +++ b/web/task/task-item.js @@ -1,9 +1,9 @@ // @ts-check import { LitElement, css, html, nothing } from "lit"; import { intervalToDuration } from "date-fns"; -import './task.js' import { formatDuration } from "../utils.js"; import './task-log.js' +import { api } from "../api.js"; export class TaskItem extends LitElement { static styles = css` @@ -92,7 +92,7 @@ export class TaskItem extends LitElement { constructor() { super(); - /** @type {Task | null} */ + /** @type {import("../api.js").Task | null} */ this.task = null; this.isOpen = false; } @@ -111,9 +111,7 @@ export class TaskItem extends LitElement { return } - fetch(`/api/v1/task-spooler/kill/${this.task.ID}`, { - method: 'POST' - }) + api.taskSpooler.kill(this.task.ID) .then(() => window.dispatchEvent(new CustomEvent('task-list-updated'))); } diff --git a/web/task/task-list.js b/web/task/task-list.js index 272692f..f7e4554 100644 --- a/web/task/task-list.js +++ b/web/task/task-list.js @@ -1,12 +1,11 @@ // @ts-check import { LitElement, css, html } from 'lit'; import { repeat } from "lit-html/directives/repeat.js"; -import './task.js'; export class TaskList extends LitElement { constructor() { super(); - /** @type {Task[]} */ + /** @type {import('../api').Task[]} */ this.tasks = []; this.isLoading = true; } diff --git a/web/task/task.js b/web/task/task.js deleted file mode 100644 index aa7885d..0000000 --- a/web/task/task.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @typedef {'running' | 'queued' | 'finished'} TaskState - */ -/** - * @typedef {object} TaskDetail - * @property {string} ExitStatus - * @property {string} Command - * @property {number} SlotsRequired - * @property {string} EnqueueTime - * @property {string} StartTime - * @property {string} EndTime - * @property {string} TimeRun - */ -/** - * @typedef {object} Task - * @property {string} ID - * @property {TaskState} State - * @property {string} Output - * @property {string} ELevel - * @property {string} Times - * @property {string} Command - * @property {Label} Label - * @property {string} LabelName - * @property {TaskDetail?} Detail - */ \ No newline at end of file From 3e0cd52a8df049256d82beca0639625c80cf45a9 Mon Sep 17 00:00:00 2001 From: Livio Brunner Date: Mon, 13 Nov 2023 16:03:41 +0100 Subject: [PATCH 2/2] feat: add socket dropdown --- api/taskSpoolerController.go | 100 +++++++++++++++--------------- internal/task-spooler/commands.go | 37 +++++++---- web/api.js | 22 ++++++- web/card.js | 4 +- web/command/command-list.js | 4 +- web/index.html | 12 ++++ web/pages/home.js | 21 ++++++- web/socket/socket-dropdown.js | 49 +++++++++++++++ web/task/task-list.js | 2 +- 9 files changed, 180 insertions(+), 71 deletions(-) create mode 100644 web/socket/socket-dropdown.js diff --git a/api/taskSpoolerController.go b/api/taskSpoolerController.go index 9295b16..8b60673 100644 --- a/api/taskSpoolerController.go +++ b/api/taskSpoolerController.go @@ -2,8 +2,8 @@ package api import ( "encoding/json" + "errors" "net/http" - "os/exec" "tsp-web/internal/args" taskspooler "tsp-web/internal/task-spooler" userconf "tsp-web/internal/user-conf" @@ -24,22 +24,40 @@ type ExecRes struct { func TaskSpoolerController(args args.TspWebArgs, r *mux.Router) { r.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) { - GetList(args, w, r) + res, err := GetList(args, getEnv(r), w, r) + + if err != nil { + log.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(res) }).Methods("GET") r.HandleFunc("/clear", func(w http.ResponseWriter, r *http.Request) { - PostClear(args, w, r) + err := taskspooler.ClearFinishedTasks(args, getEnv(r)) + if err != nil { + log.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } }).Methods("POST") r.HandleFunc("/exec", func(w http.ResponseWriter, r *http.Request) { - PostExec(args, w, r) + res, err := PostExec(args, w, r, getEnv(r)) + if err != nil { + log.Error(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(res) }).Methods("POST") r.HandleFunc("/kill/{id}", func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] - - err := taskspooler.Kill(args, id) + err := taskspooler.Kill(args, id, getEnv(r)) if err != nil { log.Error(err) w.WriteHeader(http.StatusInternalServerError) @@ -48,15 +66,12 @@ func TaskSpoolerController(args args.TspWebArgs, r *mux.Router) { }).Methods("POST") } -func GetList(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { +func GetList(args args.TspWebArgs, env map[string]string, w http.ResponseWriter, r *http.Request) (res []byte, err error) { labels := userconf.GetUserConf(args).Labels - - currentTasks, err := taskspooler.List(args) + currentTasks, err := taskspooler.List(args, env) if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return + return nil, err } for i, task := range currentTasks { @@ -79,43 +94,23 @@ func GetList(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { } if !hasFoundCachedDetail { - currentTasks[i].Detail, err = taskspooler.Detail(args, task.ID) + currentTasks[i].Detail, err = taskspooler.Detail(args, task.ID, env) if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return + return nil, err } } } cachedTasks = currentTasks - res, err := json.Marshal(currentTasks) - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(res) -} - -func PostClear(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { - err := taskspooler.ClearFinishedTasks(args) - if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return - } + return json.Marshal(currentTasks) } -func PostExec(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { +func PostExec(args args.TspWebArgs, w http.ResponseWriter, r *http.Request, env map[string]string) (res []byte, erro error) { var command ExecArg err := json.NewDecoder(r.Body).Decode(&command) if err != nil { - log.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return + return nil, err } allCommands := userconf.GetUserConf(args).Commands @@ -129,22 +124,27 @@ func PostExec(args args.TspWebArgs, w http.ResponseWriter, r *http.Request) { } if foundCommand == nil { - log.Error("Command not found: ", command.Name) - w.WriteHeader(http.StatusNotFound) - return + return nil, errors.New("Command not found") } log.Info("Executing command from user request with args: ", args.TsBin, foundCommand.Args) - cmd := exec.Command(args.TsBin, foundCommand.Args...) - out, err := cmd.Output() - if err != nil { - log.Error("Error executing command: ", err) - w.WriteHeader(http.StatusInternalServerError) - } + arguments := append([]string{args.TsBin}, foundCommand.Args...) + out, err := taskspooler.Execute(env, arguments...) + + var result ExecRes + result.ID = int(out[0]) + resJson, _ := json.Marshal(result) + return resJson, nil +} + +func getEnv(r *http.Request) map[string]string { + socket := r.URL.Query().Get("socket") - var res ExecRes - res.ID = int(out[0]) + env := map[string]string{} + + if socket != "default" && socket != "" { + env["TS_SOCKET"] = socket + } - resJson, _ := json.Marshal(res) - w.Write(resJson) + return env } diff --git a/internal/task-spooler/commands.go b/internal/task-spooler/commands.go index 37e1112..393380c 100644 --- a/internal/task-spooler/commands.go +++ b/internal/task-spooler/commands.go @@ -1,9 +1,12 @@ package taskspooler import ( + "os" "os/exec" "tsp-web/internal/args" userconf "tsp-web/internal/user-conf" + + log "github.com/sirupsen/logrus" ) type Task struct { @@ -29,26 +32,36 @@ type TaskDetail struct { TimeRun string } -func List(args args.TspWebArgs) ([]Task, error) { - cmd := exec.Command(args.TsBin) - out, err := cmd.Output() +func List(args args.TspWebArgs, envVars map[string]string) ([]Task, error) { + out, err := Execute(envVars, args.TsBin) return parseListOutput(string(out)), err } -func Detail(args args.TspWebArgs, id string) (TaskDetail, error) { - cmd := exec.Command(args.TsBin, "-i", id) - out, err := cmd.Output() +func Detail(args args.TspWebArgs, id string, envVars map[string]string) (TaskDetail, error) { + out, err := Execute(envVars, args.TsBin, "-i", id) return parseDetailOutput(string(out)), err } -func ClearFinishedTasks(args args.TspWebArgs) error { - cmd := exec.Command(args.TsBin, "-C") - err := cmd.Run() +func ClearFinishedTasks(args args.TspWebArgs, envVars map[string]string) error { + _, err := Execute(envVars, args.TsBin, "-C") return err } -func Kill(args args.TspWebArgs, id string) error { - cmd := exec.Command(args.TsBin, "-k", id) - err := cmd.Run() +func Kill(args args.TspWebArgs, id string, envVars map[string]string) error { + _, err := Execute(envVars, args.TsBin, "-k", id) return err } + +func cmdEnv(cmd *exec.Cmd, envVars map[string]string) { + cmd.Env = os.Environ() + for k, v := range envVars { + cmd.Env = append(cmd.Env, k+"="+v) + } +} + +func Execute(envVars map[string]string, args ...string) ([]byte, error) { + cmd := exec.Command(args[0], args[1:]...) + log.Debug(cmd.Args) + cmdEnv(cmd, envVars) + return cmd.Output() +} diff --git a/web/api.js b/web/api.js index 8823f2d..e1fada3 100644 --- a/web/api.js +++ b/web/api.js @@ -50,11 +50,24 @@ */ const taskSpooler = { + /** + * @type {string} + */ + socket: 'default', + + /** + * @param {string} socket + */ + setSocket: (socket) => { + taskSpooler.socket = socket + }, + /** * @param {string} id */ kill: async (id) => { - await fetch(`/api/v1/task-spooler/kill/${id}`, { method: 'POST' }) + const query = new URLSearchParams({ socket: taskSpooler.socket }) + await fetch(`/api/v1/task-spooler/kill/${id}?${query}`, { method: 'POST' }) }, /** @@ -62,7 +75,9 @@ const taskSpooler = { * @returns {Promise} */ list: async () => { - return await fetch('/api/v1/task-spooler/list').then(response => response.json()) + const query = new URLSearchParams({ socket: taskSpooler.socket }) + return await fetch(`/api/v1/task-spooler/list?${query}`) + .then(response => response.json()) }, /** @@ -70,7 +85,8 @@ const taskSpooler = { * @returns {Promise} */ exec: async (name) => { - return await fetch(`/api/v1/task-spooler/exec`, { + const query = new URLSearchParams({ socket: taskSpooler.socket }) + return await fetch(`/api/v1/task-spooler/exec?${query}`, { method: 'POST', body: JSON.stringify({ Name: name }) }) .then(response => response.json()) diff --git a/web/card.js b/web/card.js index 37164da..02d6724 100644 --- a/web/card.js +++ b/web/card.js @@ -31,7 +31,7 @@ export class Card extends LitElement { } - :host h2 { + :host h2, :host slot[name="title"]::slotted(*) { margin: 0 0 var(--sl-spacing-large) 0; font-weight: 500; font-size: 16px; @@ -49,7 +49,7 @@ export class Card extends LitElement { render() { return html`
-

${this.title}

+ ${this.title ? html`

${this.title}

` : html``}
`; diff --git a/web/command/command-list.js b/web/command/command-list.js index 4d4a59d..eeaae7b 100644 --- a/web/command/command-list.js +++ b/web/command/command-list.js @@ -26,7 +26,9 @@ export class CommandList extends LitElement { } `; - + /** + * @param {import('../api.js').Command} command + */ #exec(command) { api.taskSpooler.exec(command.Name) .then(() => window.dispatchEvent(new CustomEvent('task-list-updated'))); diff --git a/web/index.html b/web/index.html index 6f2eaad..850f469 100644 --- a/web/index.html +++ b/web/index.html @@ -51,6 +51,18 @@ type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.9.0/cdn/components/dialog/dialog.js" > + + + diff --git a/web/pages/home.js b/web/pages/home.js index ecd1209..883ee3b 100644 --- a/web/pages/home.js +++ b/web/pages/home.js @@ -1,12 +1,13 @@ // @ts-check import { LitElement, html, css, nothing } from "lit"; +import { api } from "../api.js"; import '../task/task-list.js' import '../task/task-item.js' import '../label/label-badge.js' import '../label/label-filter.js' import '../command/command-list.js' import '../card.js' -import { api } from "../api.js"; +import '../socket/socket-dropdown.js' export class Home extends LitElement { constructor() { @@ -33,6 +34,17 @@ export class Home extends LitElement { gap: var(--sl-spacing-large); } + :host .card-title { + display: flex; + justify-content: space-between; + align-items: center; + } + + :host .card-title h2 { + font-size: 16px; + font-weight: 500; + } + @media (min-width: 768px) { :host .container { padding: var(--sl-spacing-2x-large); @@ -92,6 +104,7 @@ export class Home extends LitElement { render() { const hasCommands = (this.config.Commands || []).length > 0; + const hasSockets = (this.config.Sockets || []).length > 0; const commandsCard = html` @@ -102,7 +115,11 @@ export class Home extends LitElement { return html`
${hasCommands ? commandsCard : nothing} - + +
+

Tasks

+ ${hasSockets ? html`` : nothing} +
diff --git a/web/socket/socket-dropdown.js b/web/socket/socket-dropdown.js new file mode 100644 index 0000000..85c7bb6 --- /dev/null +++ b/web/socket/socket-dropdown.js @@ -0,0 +1,49 @@ +// @ts-check +import { LitElement, css, html } from "lit"; +import { api } from "../api.js"; + +class SocketDropdown extends LitElement { + constructor() { + super(); + /** @type {import("../api.js").Socket[]} */ + this.sockets = []; + + } + + static styles = css` + :host { + position: relative; + } + `; + + static get properties() { + return { + sockets: { type: Array }, + currentSocket: { type: Object } + } + } + + /** + * @param {import("@shoelace-style/shoelace").SlSelectEvent} event + */ + setSocket(event) { + api.taskSpooler.setSocket(event.detail.item.value); + this.currentSocket = this.sockets.find((socket) => socket.Path === event.detail.item.value); + window.dispatchEvent(new CustomEvent('task-list-updated')); + } + + render() { + return html` + + ${this.currentSocket ? `Socket: ${this.currentSocket.Name}` : `Socket: ${this.sockets[0].Name}`} + this.setSocket(e)}> + ${this.sockets.map((socket) => { + return html`${socket.Name}` + })} + + + `; + } +} + +customElements.define('socket-dropdown', SocketDropdown); \ No newline at end of file diff --git a/web/task/task-list.js b/web/task/task-list.js index f7e4554..e547dfa 100644 --- a/web/task/task-list.js +++ b/web/task/task-list.js @@ -5,7 +5,7 @@ import { repeat } from "lit-html/directives/repeat.js"; export class TaskList extends LitElement { constructor() { super(); - /** @type {import('../api').Task[]} */ + /** @type {import('../api.js').Task[]} */ this.tasks = []; this.isLoading = true; }