diff --git a/node.go b/node.go index 85738d6..bac98bf 100644 --- a/node.go +++ b/node.go @@ -98,9 +98,6 @@ 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 6f54d03..96af03b 100644 --- a/script.go +++ b/script.go @@ -26,7 +26,7 @@ type Hook struct { SSH string } -func GetScripts() (scripts []Script, err error) { // {{{ +func GetScripts() (scripts []Script, err error) { scripts = []Script{} var rows *sqlx.Rows @@ -54,8 +54,8 @@ func GetScripts() (scripts []Script, err error) { // {{{ } return -} // }}} -func UpdateScript(scriptID int, data []byte) (script Script, err error) { // {{{ +} +func UpdateScript(scriptID int, data []byte) (script Script, err error) { err = json.Unmarshal(data, &script) if err != nil { err = werr.Wrap(err) @@ -106,64 +106,12 @@ func UpdateScript(scriptID int, data []byte) (script Script, err error) { // {{{ } return -} // }}} -func DeleteScript(scriptID int) (err error) { // {{{ +} +func DeleteScript(scriptID int) (err error) { _, err = db.Exec(`DELETE FROM script WHERE id = $1`, scriptID) if err != nil { err = werr.Wrap(err) return } return -} // }}} -func SearchScripts(search string) (scripts []Script, err error) { // {{{ - scripts = []Script{} - - row := db.QueryRow(` - SELECT - json_agg(script) AS scripts - FROM public.script - WHERE - name ILIKE $1 - ORDER BY - "group" ASC, - name ASC - `, - search, - ) - - var jsonBody []byte - err = row.Scan(&jsonBody) - if err != nil { - err = werr.Wrap(err) - return - } - - err = json.Unmarshal(jsonBody, &scripts) - if err != nil { - err = werr.Wrap(err) - return - } - - 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)) - if err != nil { - err = werr.Wrap(err) - return - } - return -} // }}} -func DeleteHook(hookID int) (err error) { // {{{ - _, err = db.Exec(`DELETE FROM hook WHERE id=$1`, hookID) - if err != nil { - err = werr.Wrap(err) - return - } - return -} // }}} +} diff --git a/sql/0012.sql b/sql/0012.sql deleted file mode 100644 index 1297758..0000000 --- a/sql/0012.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.hook ADD CONSTRAINT hook_unique UNIQUE (node_id,script_id); diff --git a/static/css/main.css b/static/css/main.css index 191b218..b87e152 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -210,18 +210,17 @@ 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: 16px; + margin-bottom: 8px; } -#connected-nodes > .label > img.add { +#connected-nodes > .add { + margin-bottom: 8px; +} +#connected-nodes > .add img { height: 24px; cursor: pointer; - margin-left: 8px; } #connected-nodes .connected-nodes { display: flex; @@ -249,9 +248,9 @@ select:focus { } #script-hooks .scripts-grid { display: grid; - grid-template-columns: repeat(3, min-content); + grid-template-columns: repeat(4, min-content); align-items: center; - grid-gap: 2px 0px; + grid-gap: 4px 0px; } #script-hooks .scripts-grid .header { font-weight: bold; @@ -260,11 +259,10 @@ select:focus { #script-hooks .scripts-grid div { white-space: nowrap; } -#script-hooks .scripts-grid .script-group { - grid-column: 1 / -1; - font-weight: bold; - margin-top: 8px; +#script-hooks .scripts-grid .script-icon { + margin-right: 4px; } +#script-hooks .scripts-grid .script-icon img, #script-hooks .scripts-grid .script-unhook img { display: block; height: 24px; @@ -276,20 +274,19 @@ 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; @@ -301,10 +298,6 @@ select:focus { min-width: 300px; width: 100%; } -#select-node .label { - font-weight: bold; - color: var(--section-color); -} #select-node button { width: 100px !important; } @@ -463,21 +456,3 @@ 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 446dd4c..1107d10 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 { @@ -949,10 +921,8 @@ class ConnectedNodes { render() {// {{{ const div = document.createElement('template') div.innerHTML = ` -
-
Connected nodes
- -
+
Connected nodes
+
` @@ -1003,133 +973,56 @@ class ConnectedNode { } class ScriptHooks extends Component { - constructor(hooks) {// {{{ + constructor(hooks) { super() this.hooks = hooks - this.scriptGrid = null - }// }}} - renderComponent() {// {{{ + } + renderComponent() { const div = document.createElement('div') div.innerHTML = ` -
-
Script hooks
- -
+
Script hooks
+
Script
SSH
` - div.querySelector('.add').addEventListener('click', () => { - const dlg = new ScriptSelectDialog(s => { - this.hookScript(s) - }) - dlg.render() + div.querySelector('.add').addEventListener('click', ()=>{ + alert('FIXME') }) - this.scriptGrid = div.querySelector('.scripts-grid') - this.renderHooks() + const scriptsGrid = div.querySelector('.scripts-grid') + for(const hook of this.hooks) { + const h = new ScriptHook(hook) + scriptsGrid.append(h.render()) + } 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) { - 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, parentList) {// {{{ + constructor(hook) {// {{{ 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()) + tmpl.content.querySelector('.script-ssh').addEventListener('click', () => { + prompt('SSH', this.hook.SSH) + //new ConnectionDataDialog(this.hook, () => _app.edit(_app.currentNode.ID)).render() + }) return tmpl.content }// }}} - update() {// {{{ - const ssh = prompt('SSH', this.hook.SSH) - if (ssh === null) - return - if (ssh.trim() === '') { - alert(`SSH can't be empty.`) - return - } - - const request = { - ID: this.hook.ID, - SSH: ssh, - } - fetch('/hooks/update', { - method: 'POST', - body: JSON.stringify(request), - - }) - .then(data => data.json()) - .then(json => { - if (!json.OK) { - showError(json.Error) - return - } - this.hook.SSH = ssh - this.element_ssh.innerText = this.hook.SSH - }) - .catch(err => showError(err)) - }// }}} - delete() {// {{{ - if (!confirm(`Unhook the '${this.hook.Script.Name}' script?`)) - return - - fetch(`/hooks/delete/${this.hook.ID}`) - .then(data => data.json()) - .then(json => { - if (!json.OK) { - showError(json.Error) - return - } - this.parentList.hookDeleted(this.hook.ID) - }) - .catch(err => showError(err)) - }// }}} } class ScriptsList extends Component { @@ -1345,80 +1238,4 @@ 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 709684c..c0a303b 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -284,19 +284,17 @@ 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: 16px; + margin-bottom: 8px; + } - & > img.add { + & > .add { + margin-bottom: 8px; + img { height: 24px; cursor: pointer; - margin-left: 8px; } } @@ -339,21 +337,19 @@ select:focus { } display: grid; - grid-template-columns: repeat(3, min-content); + grid-template-columns: repeat(4, min-content); align-items: center; - grid-gap: 2px 0px; + grid-gap: 4px 0px; div { white-space: nowrap; } - .script-group { - grid-column: 1 / -1; - font-weight: bold; - margin-top: 8px; + .script-icon { + margin-right: 4px; } - .script-unhook { + .script-icon, .script-unhook { img { display: block; height: 24px; @@ -369,21 +365,19 @@ select:focus { } } - & > .label { - display: grid; - grid-template-columns: min-content min-content; - align-items: center; + & > .add { + margin-bottom: 8px; + img { + height: 24px; + cursor: pointer; + } + } + & > .label { color: var(--section-color); font-weight: bold; font-size: 1.25em; margin-bottom: 8px; - - img.add { - height: 24px; - cursor: pointer; - margin-left: 8px; - } } } @@ -400,11 +394,6 @@ select:focus { width: 100%; } - .label { - font-weight: bold; - color: var(--section-color); - } - button { width: 100px !important; } @@ -600,26 +589,3 @@ 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 ba2d293..cd8d9b3 100644 --- a/webserver.go +++ b/webserver.go @@ -41,7 +41,6 @@ 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) @@ -51,9 +50,7 @@ func initWebserver() (err error) { http.HandleFunc("/scripts/", actionScripts) http.HandleFunc("/scripts/update/{scriptID}", actionScriptUpdate) http.HandleFunc("/scripts/delete/{scriptID}", actionScriptDelete) - http.HandleFunc("/hooks/search", actionScriptsSearch) - http.HandleFunc("/hooks/update", actionHookUpdate) - http.HandleFunc("/hooks/delete/{hookID}", actionHookDelete) + http.HandleFunc("/hooks/update/{hookID}", actionHookUpdate) err = http.ListenAndServe(address, nil) return @@ -375,37 +372,6 @@ 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) { // {{{ @@ -655,67 +621,15 @@ func actionScriptDelete(w http.ResponseWriter, r *http.Request) { // {{{ j, _ := json.Marshal(out) w.Write(j) } // }}} -func actionScriptsSearch(w http.ResponseWriter, r *http.Request) { // {{{ - var search struct { - Search string - } - body, _ := io.ReadAll(r.Body) - err := json.Unmarshal(body, &search) - if err != nil { - err = werr.Wrap(err) - httpError(w, err) - return - } - - scripts, err := SearchScripts(search.Search) - if err != nil { - err = werr.Wrap(err) - httpError(w, err) - return - } - - out := struct { - OK bool - Scripts []Script - }{ - true, - scripts, - } - j, _ := json.Marshal(out) - w.Write(j) -} // }}} func actionHookUpdate(w http.ResponseWriter, r *http.Request) { // {{{ - var hook Hook - body, _ := io.ReadAll(r.Body) - err := json.Unmarshal(body, &hook) - if err != nil { - err = werr.Wrap(err) - httpError(w, err) - return - } - - err = UpdateHook(hook) - if err != nil { - err = werr.Wrap(err) - httpError(w, err) - return - } - - out := struct { - OK bool - }{ - true, - } - j, _ := json.Marshal(out) - w.Write(j) -} // }}} -func actionHookDelete(w http.ResponseWriter, r *http.Request) { // {{{ hookID := 0 hookIDStr := r.PathValue("hookID") hookID, _ = strconv.Atoi(hookIDStr) - err := DeleteHook(hookID) + // XXX - here + + err := UpdateHook(hook) if err != nil { err = werr.Wrap(err) httpError(w, err)