diff --git a/node.go b/node.go index bac98bf..85738d6 100644 --- a/node.go +++ b/node.go @@ -98,6 +98,9 @@ func GetNode(nodeID int) (node Node, err error) { // {{{ INNER JOIN public.script s ON h.script_id = s.id WHERE h.node_id = n.id + ORDER BY + s.group ASC, + s.name ASC ) AS res ) , '[]'::jsonb diff --git a/script.go b/script.go index 5dc8bf3..6f54d03 100644 --- a/script.go +++ b/script.go @@ -124,6 +124,9 @@ func SearchScripts(search string) (scripts []Script, err error) { // {{{ FROM public.script WHERE name ILIKE $1 + ORDER BY + "group" ASC, + name ASC `, search, ) @@ -143,6 +146,10 @@ func SearchScripts(search string) (scripts []Script, err error) { // {{{ return } // }}} +func HookScript(nodeID, scriptID int) (err error) {// {{{ + _, err = db.Exec(`INSERT INTO hook(node_id, script_id, ssh) VALUES($1, $2, '')`, nodeID, scriptID) + return +}// }}} func UpdateHook(hook Hook) (err error) { // {{{ _, err = db.Exec(`UPDATE hook SET ssh=$2 WHERE id=$1`, hook.ID, strings.TrimSpace(hook.SSH)) diff --git a/static/css/main.css b/static/css/main.css index b87e152..191b218 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -210,17 +210,18 @@ select:focus { outline-offset: -2px; } #connected-nodes > .label { + display: grid; + grid-template-columns: min-content min-content; + align-items: center; color: var(--section-color); font-weight: bold; font-size: 1.25em; - margin-bottom: 8px; + margin-bottom: 16px; } -#connected-nodes > .add { - margin-bottom: 8px; -} -#connected-nodes > .add img { +#connected-nodes > .label > img.add { height: 24px; cursor: pointer; + margin-left: 8px; } #connected-nodes .connected-nodes { display: flex; @@ -248,9 +249,9 @@ select:focus { } #script-hooks .scripts-grid { display: grid; - grid-template-columns: repeat(4, min-content); + grid-template-columns: repeat(3, min-content); align-items: center; - grid-gap: 4px 0px; + grid-gap: 2px 0px; } #script-hooks .scripts-grid .header { font-weight: bold; @@ -259,10 +260,11 @@ select:focus { #script-hooks .scripts-grid div { white-space: nowrap; } -#script-hooks .scripts-grid .script-icon { - margin-right: 4px; +#script-hooks .scripts-grid .script-group { + grid-column: 1 / -1; + font-weight: bold; + margin-top: 8px; } -#script-hooks .scripts-grid .script-icon img, #script-hooks .scripts-grid .script-unhook img { display: block; height: 24px; @@ -274,19 +276,20 @@ select:focus { #script-hooks .scripts-grid .script-ssh { cursor: pointer; } -#script-hooks > .add { - margin-bottom: 8px; -} -#script-hooks > .add img { - height: 24px; - cursor: pointer; -} #script-hooks > .label { + display: grid; + grid-template-columns: min-content min-content; + align-items: center; color: var(--section-color); font-weight: bold; font-size: 1.25em; margin-bottom: 8px; } +#script-hooks > .label img.add { + height: 24px; + cursor: pointer; + margin-left: 8px; +} #select-node { padding: 32px; display: grid; @@ -298,6 +301,10 @@ select:focus { min-width: 300px; width: 100%; } +#select-node .label { + font-weight: bold; + color: var(--section-color); +} #select-node button { width: 100px !important; } @@ -456,3 +463,21 @@ dialog#connection-data div.button { #editor-script button { margin-top: 8px; } +#script-select-dialog { + display: grid; + grid-gap: 8px; + padding: 32px; +} +#script-select-dialog > .header { + font-weight: bold; + color: var(--section-color); +} +#script-select-dialog .scripts .group { + font-weight: bold; + color: var(--section-color); + margin-top: 16px; +} +#script-select-dialog .scripts .script { + cursor: pointer; + margin-top: 4px; +} diff --git a/static/images/logo.svg b/static/images/logo.svg index 1107d10..446dd4c 100644 --- a/static/images/logo.svg +++ b/static/images/logo.svg @@ -3,12 +3,12 @@ JSON { + let request = {} + + if (data !== undefined) { + request.method = 'POST' + request.body = JSON.stringify(data) + } + + fetch(path, request) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + reject(json.Error) + return + } + resolve(json) + }) + .catch(err => { + reject(err) + }) + }) + }// }}} } class NodeCreateDialog { @@ -921,8 +949,10 @@ class ConnectedNodes { render() {// {{{ const div = document.createElement('template') div.innerHTML = ` -
Connected nodes
-
+
+
Connected nodes
+ +
` @@ -977,18 +1007,14 @@ class ScriptHooks extends Component { super() this.hooks = hooks this.scriptGrid = null - - mbus.subscribe('hook_deleted', event => { - const deletedHook = event.detail - this.hooks = this.hooks.filter(h => h.ID !== deletedHook.ID) - this.renderHooks() - }) }// }}} renderComponent() {// {{{ const div = document.createElement('div') div.innerHTML = ` -
Script hooks
-
+
+
Script hooks
+ +
Script
SSH
@@ -996,7 +1022,10 @@ class ScriptHooks extends Component { ` div.querySelector('.add').addEventListener('click', () => { - alert('FIXME') + const dlg = new ScriptSelectDialog(s => { + this.hookScript(s) + }) + dlg.render() }) this.scriptGrid = div.querySelector('.scripts-grid') @@ -1004,31 +1033,53 @@ class ScriptHooks extends Component { return div.children }// }}} + hookDeleted(deletedHookID) {// {{{ + this.hooks = this.hooks.filter(h => h.ID !== deletedHookID) + this.renderHooks() + }// }}} renderHooks() {// {{{ this.scriptGrid.innerHTML = '' + let prevGroup = null for (const hook of this.hooks) { - const h = new ScriptHook(hook) + if (hook.Script.Group !== prevGroup) { + const g = document.createElement('div') + g.classList.add('script-group') + g.innerText = hook.Script.Group + this.scriptGrid.append(g) + prevGroup = hook.Script.Group + } + + const h = new ScriptHook(hook, this) this.scriptGrid.append(h.render()) } }// }}} + hookScript(script) {// {{{ + _app.query(`/nodes/hook`, { + NodeID: _app.currentNode.ID, + ScriptID: script.ID, + }) + .then(()=>mbus.dispatch('NODE_HOOKED', _app.currentNode.ID)) + .catch(err => showError(err)) + }// }}} } class ScriptHook extends Component { - constructor(hook) {// {{{ + constructor(hook, parentList) {// {{{ super() this.hook = hook + this.parentList = parentList this.element_ssh = null }// }}} renderComponent() {// {{{ const tmpl = document.createElement('template') tmpl.innerHTML = ` -
${this.hook.Script.Name}
-
${this.hook.SSH}
+
` this.element_ssh = tmpl.content.querySelector('.script-ssh') + this.element_ssh.innerText = this.hook.SSH tmpl.content.querySelector('.script-ssh').addEventListener('click', () => this.update()) tmpl.content.querySelector('.script-unhook').addEventListener('click', () => this.delete()) @@ -1075,7 +1126,7 @@ class ScriptHook extends Component { showError(json.Error) return } - mbus.dispatch('hook_deleted', this.hook) + this.parentList.hookDeleted(this.hook.ID) }) .catch(err => showError(err)) }// }}} @@ -1294,4 +1345,80 @@ class ScriptEditor extends Component { }// }}} } +class ScriptSelectDialog extends Component { + constructor(callback) {// {{{ + super() + this.dlg = document.createElement('dialog') + this.dlg.id = 'script-select-dialog' + this.dlg.addEventListener('close', () => this.dlg.remove()) + this.searchFor = null + this.scripts = null + this.callback = callback + }// }}} + renderComponent() {// {{{ + const div = document.createElement('div') + div.innerHTML = ` +
Search for script
+
+
+
+ ` + + this.searchFor = div.querySelector('.search-for') + this.scripts = div.querySelector('.scripts') + const button = div.querySelector('button') + this.searchFor.addEventListener('keydown', event => { + if (event.key == 'Enter') + this.searchScripts() + }) + button.addEventListener('click', () => this.searchScripts()) + + this.dlg.append(...div.children) + document.body.append(this.dlg) + this.dlg.showModal() + + return [] + }// }}} + searchScripts() {// {{{ + fetch('/scripts/search', { + method: 'POST', + body: JSON.stringify({ + Search: this.searchFor.value, + }), + }) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + showError(json.Error) + return + } + this.populateScripts(json.Scripts) + }) + .catch(err => showError(err)) + }// }}} + populateScripts(scripts) {// {{{ + this.scripts.innerHTML = '' + + let prevGroup = null + for (const s of scripts) { + if (s.Group !== prevGroup) { + const group = document.createElement('div') + group.classList.add('group') + group.innerText = s.Group + this.scripts.append(group) + prevGroup = s.Group + } + + const div = document.createElement('div') + div.innerText = s.Name + div.classList.add('script') + div.addEventListener('click', () => { + this.dlg.close() + this.callback(s) + }) + this.scripts.append(div) + } + }// }}} +} + // vim: foldmethod=marker diff --git a/static/less/main.less b/static/less/main.less index c0a303b..709684c 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -284,17 +284,19 @@ select:focus { #connected-nodes { & > .label { + display: grid; + grid-template-columns: min-content min-content; + align-items: center; + color: var(--section-color); font-weight: bold; font-size: 1.25em; - margin-bottom: 8px; - } + margin-bottom: 16px; - & > .add { - margin-bottom: 8px; - img { + & > img.add { height: 24px; cursor: pointer; + margin-left: 8px; } } @@ -337,19 +339,21 @@ select:focus { } display: grid; - grid-template-columns: repeat(4, min-content); + grid-template-columns: repeat(3, min-content); align-items: center; - grid-gap: 4px 0px; + grid-gap: 2px 0px; div { white-space: nowrap; } - .script-icon { - margin-right: 4px; + .script-group { + grid-column: 1 / -1; + font-weight: bold; + margin-top: 8px; } - .script-icon, .script-unhook { + .script-unhook { img { display: block; height: 24px; @@ -365,19 +369,21 @@ select:focus { } } - & > .add { - margin-bottom: 8px; - img { - height: 24px; - cursor: pointer; - } - } - & > .label { + display: grid; + grid-template-columns: min-content min-content; + align-items: center; + color: var(--section-color); font-weight: bold; font-size: 1.25em; margin-bottom: 8px; + + img.add { + height: 24px; + cursor: pointer; + margin-left: 8px; + } } } @@ -394,6 +400,11 @@ select:focus { width: 100%; } + .label { + font-weight: bold; + color: var(--section-color); + } + button { width: 100px !important; } @@ -589,3 +600,26 @@ dialog#connection-data { margin-top: 8px; } } + +#script-select-dialog { + display: grid; + grid-gap: 8px; + padding: 32px; + + & > .header { + font-weight: bold; + color: var(--section-color); + } + + .scripts { + .group { + font-weight: bold; + color: var(--section-color); + margin-top: 16px; + } + .script { + cursor: pointer; + margin-top: 4px; + } + } +} diff --git a/webserver.go b/webserver.go index f707802..ba2d293 100644 --- a/webserver.go +++ b/webserver.go @@ -41,6 +41,7 @@ func initWebserver() (err error) { http.HandleFunc("/nodes/move", actionNodeMove) http.HandleFunc("/nodes/search", actionNodeSearch) http.HandleFunc("/nodes/connect", actionNodeConnect) + http.HandleFunc("/nodes/hook", actionNodeHook) http.HandleFunc("/types/{typeID}", actionType) http.HandleFunc("/types/", actionTypesAll) http.HandleFunc("/types/create", actionTypeCreate) @@ -374,6 +375,37 @@ func actionNodeConnect(w http.ResponseWriter, r *http.Request) { // {{{ j, _ := json.Marshal(res) w.Write(j) +} // }}} +func actionNodeHook(w http.ResponseWriter, r *http.Request) { // {{{ + var req struct { + NodeID int + ScriptID int + } + + body, _ := io.ReadAll(r.Body) + err := json.Unmarshal(body, &req) + if err != nil { + err = werr.Wrap(err) + httpError(w, err) + return + } + + err = HookScript(req.NodeID, req.ScriptID) + if err != nil { + pqErr, ok := err.(*pq.Error) + if ok && pqErr.Code == "23505" { + err = errors.New("This script is already hooked.") + } else { + err = werr.Wrap(err) + } + httpError(w, err) + return + } + + res := struct{ OK bool }{true} + j, _ := json.Marshal(res) + w.Write(j) + } // }}} func actionType(w http.ResponseWriter, r *http.Request) { // {{{