diff --git a/node.go b/node.go index 9738ea0..bac98bf 100644 --- a/node.go +++ b/node.go @@ -84,7 +84,24 @@ func GetNode(nodeID int) (node Node, err error) { // {{{ ) AS res ) , '[]'::jsonb - ) AS ConnectedNodes + ) AS ConnectedNodes, + + COALESCE( + ( + SELECT jsonb_agg(res) + FROM ( + SELECT + h.id, + to_jsonb(s) AS script, + ssh + FROM hook h + INNER JOIN public.script s ON h.script_id = s.id + WHERE + h.node_id = n.id + ) AS res + ) + , '[]'::jsonb + ) AS ScriptHooks FROM public.node n INNER JOIN public.type t ON n.type_id = t.id @@ -111,25 +128,98 @@ func GetNode(nodeID int) (node Node, err error) { // {{{ return } // }}} -func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) { // {{{ +func GetNodeTree(startNodeID, maxDepth int, withData bool) (topNode *Node, err error) { // {{{ nodes := make(map[int]*Node) - var rows *sqlx.Rows - rows, err = GetNodeRows(startNodeID, maxDepth) + + var nodesFromRow []Node + row := db.QueryRow(` + SELECT json_agg(res) FROM ( + WITH RECURSIVE nodes AS ( + SELECT + $1::int AS id, + 0 AS depth + UNION + + SELECT + n.id, + ns.depth+1 AS depth + FROM node n + INNER JOIN nodes ns ON ns.depth < $2 AND n.parent_id = ns.id + ) + + SEARCH DEPTH FIRST BY id SET ordercol + + SELECT + COALESCE(n.parent_id, -1) AS ParentID, + n.id, + n.name, + n.type_id AS TypeID, + t.name AS TypeName, + COALESCE(t.schema->>'icon', '') AS TypeIcon, + n.updated, + n.data AS data, + COUNT(node_children.id) AS NumChildren, + COALESCE( + ( + SELECT jsonb_agg(res) + FROM ( + SELECT + nn.ID, + COALESCE(nn.parent_id, -1) AS ParentID, + nn.Name, + nn.Updated, + nn.data AS Data, + + tt.id AS TypeID, + tt.name AS TypeName, + tt.schema AS TypeSchema, + tt.schema->>'icon' AS TypeIcon, + + c.id AS ConnectionID, + c.data AS ConnectionData + FROM connection c + INNER JOIN public.node nn ON c.child_node_id = nn.id + INNER JOIN public.type tt ON nn.type_id = tt.id + WHERE + c.parent_node_id = n.id + ) AS res + ) + , '[]'::jsonb + ) AS ConnectedNodes + FROM nodes ns + INNER JOIN public.node n ON ns.id = n.id + INNER JOIN public.type t ON n.type_id = t.id + LEFT JOIN node node_children ON node_children.parent_id = n.id + + GROUP BY + ns.depth, + n.parent_id, + n.id, + t.name, + t.schema, + ns.ordercol + ORDER BY ordercol + ) AS res + `, + startNodeID, + maxDepth, + ) + + var body []byte + err = row.Scan(&body) + if err != nil { + err = werr.Wrap(err) + return + } + + err = json.Unmarshal(body, &nodesFromRow) if err != nil { err = werr.Wrap(err) return } - defer rows.Close() first := true - for rows.Next() { - var node Node - err = rows.StructScan(&node) - if err != nil { - err = werr.Wrap(err) - return - } - + for _, node := range nodesFromRow { if first { topNode = &node first = false @@ -140,57 +230,6 @@ func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) { // {{{ return } // }}} -func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) { // {{{ - rows, err = db.Queryx(` - WITH RECURSIVE nodes AS ( - SELECT - $1::int AS id, - 0 AS depth - UNION - - SELECT - n.id, - ns.depth+1 AS depth - FROM node n - INNER JOIN nodes ns ON ns.depth < $2 AND n.parent_id = ns.id - ) - - SEARCH DEPTH FIRST BY id SET ordercol - - SELECT - COALESCE(n.parent_id, -1) AS parent_id, - n.id, - n.name, - n.type_id, - t.name AS type_name, - COALESCE(t.schema->>'icon', '') AS type_icon, - n.updated, - n.data AS data_raw, - COUNT(node_children.id) AS num_children - FROM nodes ns - INNER JOIN public.node n ON ns.id = n.id - INNER JOIN public.type t ON n.type_id = t.id - LEFT JOIN node node_children ON node_children.parent_id = n.id - - GROUP BY - ns.depth, - n.parent_id, - n.id, - t.name, - t.schema, - ns.ordercol - ORDER BY ordercol - `, - startNodeID, - maxDepth, - ) - - if err != nil { - err = werr.Wrap(err) - } - - return -} // }}} func ComposeTree(nodes map[int]*Node, node *Node) { // {{{ if node.Children == nil { node.Children = []*Node{} @@ -321,7 +360,7 @@ func SearchNodes(typeID int, search string, maxResults int) (nodes []Node, err e row := db.QueryRowx(` SELECT - json_agg(res) AS node + COALESCE(json_agg(res), '[]'::json) AS node FROM ( SELECT n.id, diff --git a/static/css/main.css b/static/css/main.css index 5e62d57..b87e152 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -220,6 +220,7 @@ select:focus { } #connected-nodes > .add img { height: 24px; + cursor: pointer; } #connected-nodes .connected-nodes { display: flex; @@ -245,6 +246,41 @@ select:focus { display: block; height: 24px; } +#script-hooks .scripts-grid { + display: grid; + grid-template-columns: repeat(4, min-content); + align-items: center; + grid-gap: 4px 0px; +} +#script-hooks .scripts-grid .header { + font-weight: bold; + margin-right: 8px; +} +#script-hooks .scripts-grid div { + white-space: nowrap; +} +#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; +} +#script-hooks .scripts-grid .script-name, +#script-hooks .scripts-grid .script-ssh { + margin-right: 16px; +} +#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 { color: var(--section-color); font-weight: bold; diff --git a/static/images/logo.svg b/static/images/logo.svg index 22fc538..1107d10 100644 --- a/static/images/logo.svg +++ b/static/images/logo.svg @@ -2,13 +2,13 @@ JSONJSON + inkscape:connection-end="#circle6" />DATAGRAPH diff --git a/static/js/app.mjs b/static/js/app.mjs index 06828a0..503ffc4 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -973,19 +973,58 @@ class ConnectedNode { } class ScriptHooks extends Component { - constructor() { + constructor(hooks) { super() + this.hooks = hooks } renderComponent() { const div = document.createElement('div') div.innerHTML = `
Script hooks
-
hum
+
+
+
Script
+
SSH
+
` + + div.querySelector('.add').addEventListener('click', ()=>{ + alert('FIXME') + }) + + const scriptsGrid = div.querySelector('.scripts-grid') + for(const hook of this.hooks) { + const h = new ScriptHook(hook) + scriptsGrid.append(h.render()) + } + return div.children } } +class ScriptHook extends Component { + constructor(hook) {// {{{ + super() + this.hook = hook + }// }}} + renderComponent() {// {{{ + const tmpl = document.createElement('template') + tmpl.innerHTML = ` +
+
${this.hook.Script.Name}
+
${this.hook.SSH}
+
+ ` + + 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 + }// }}} +} + class ScriptsList extends Component { constructor() {// {{{ super() diff --git a/static/less/main.less b/static/less/main.less index 2af627a..c0a303b 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -292,9 +292,9 @@ select:focus { & > .add { margin-bottom: 8px; - img { height: 24px; + cursor: pointer; } } @@ -330,12 +330,56 @@ select:focus { } #script-hooks { + .scripts-grid { + .header { + font-weight: bold; + margin-right: 8px; + } + + display: grid; + grid-template-columns: repeat(4, min-content); + align-items: center; + grid-gap: 4px 0px; + + div { + white-space: nowrap; + } + + .script-icon { + margin-right: 4px; + } + + .script-icon, .script-unhook { + img { + display: block; + height: 24px; + } + } + + .script-name, .script-ssh { + margin-right: 16px; + } + + .script-ssh { + cursor: pointer; + } + } + + & > .add { + margin-bottom: 8px; + img { + height: 24px; + cursor: pointer; + } + } + & > .label { color: var(--section-color); font-weight: bold; font-size: 1.25em; margin-bottom: 8px; } + } #select-node { diff --git a/webserver.go b/webserver.go index 68ed9cb..cd8d9b3 100644 --- a/webserver.go +++ b/webserver.go @@ -50,6 +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/update/{hookID}", actionHookUpdate) err = http.ListenAndServe(address, nil) return @@ -105,7 +106,12 @@ func actionNodesTree(w http.ResponseWriter, r *http.Request) { // {{{ maxDepth = 3 } - topNode, err := GetNodeTree(startNode, maxDepth) + var withData bool + if r.URL.Query().Get("data") == "true" { + withData = true + } + + topNode, err := GetNodeTree(startNode, maxDepth, withData) if err != nil { err = werr.Wrap(err) httpError(w, err) @@ -616,4 +622,27 @@ func actionScriptDelete(w http.ResponseWriter, r *http.Request) { // {{{ w.Write(j) } // }}} +func actionHookUpdate(w http.ResponseWriter, r *http.Request) { // {{{ + hookID := 0 + hookIDStr := r.PathValue("hookID") + hookID, _ = strconv.Atoi(hookIDStr) + + // XXX - here + + 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) +} // }}} + // vim: foldmethod=marker