Show connected nodes
This commit is contained in:
parent
c3f8bedea1
commit
dff17cad5b
7 changed files with 212 additions and 6 deletions
48
node.go
48
node.go
|
|
@ -211,7 +211,7 @@ func DeleteNode(nodeID int) (err error) { // {{{
|
|||
}
|
||||
return
|
||||
} // }}}
|
||||
func MoveNodes(newParentID int, nodeIDs []int) (err error) {
|
||||
func MoveNodes(newParentID int, nodeIDs []int) (err error) { // {{{
|
||||
// TODO - implement a method to verify that a node isn't moved underneath itself.
|
||||
// Preferably using a stored procedure?
|
||||
|
||||
|
|
@ -230,6 +230,50 @@ func MoveNodes(newParentID int, nodeIDs []int) (err error) {
|
|||
|
||||
_, err = db.Exec(sql, newParentID)
|
||||
return
|
||||
}
|
||||
} // }}}
|
||||
func GetNodeConnections(nodeID int) (connections []Node, err error) { // {{{
|
||||
connections = []Node{}
|
||||
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
SELECT
|
||||
n.*,
|
||||
t.id AS type_id,
|
||||
t.name AS type_name,
|
||||
t.schema AS type_schema_raw,
|
||||
t.schema->>'icon' AS type_icon
|
||||
FROM public.connection c
|
||||
INNER JOIN public.node n ON c.to = n.id
|
||||
INNER JOIN public.type t ON n.type_id = t.id
|
||||
WHERE
|
||||
c.from = $1
|
||||
`,
|
||||
nodeID,
|
||||
)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var node Node
|
||||
err = rows.StructScan(&node)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(node.TypeSchemaRaw, &node.TypeSchema)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
connections = append(connections, node)
|
||||
}
|
||||
|
||||
return
|
||||
} // }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
11
sql/0005.sql
Normal file
11
sql/0005.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
DROP TABLE public."connection";
|
||||
|
||||
CREATE TABLE public."connection" (
|
||||
id serial NOT NULL,
|
||||
"from" int4 NOT NULL,
|
||||
"to" int4 NOT NULL,
|
||||
updated timestamptz DEFAULT NOW() NOT NULL,
|
||||
CONSTRAINT newtable_pk PRIMARY KEY (id),
|
||||
CONSTRAINT connection_node_fk FOREIGN KEY ("from") REFERENCES public.node(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT connection_node_fk_1 FOREIGN KEY ("to") REFERENCES public.node(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
:root {
|
||||
--textsize: 12pt;
|
||||
--section-color: #73a44d;
|
||||
--je-color: #73a44d;
|
||||
--border-radius: 5px;
|
||||
}
|
||||
|
|
@ -197,3 +198,29 @@ select:focus {
|
|||
outline: 2px solid #888;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
#connected-nodes > .label {
|
||||
margin-bottom: 16px;
|
||||
color: var(--section-color);
|
||||
font-weight: bold;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
#connected-nodes .connected-nodes {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-flow: row wrap;
|
||||
gap: 32px;
|
||||
}
|
||||
#connected-nodes .connected-nodes .type-group {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr;
|
||||
grid-gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
#connected-nodes .connected-nodes .type-group .type-name {
|
||||
font-weight: bold;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
#connected-nodes .type-icon img {
|
||||
display: block;
|
||||
height: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,22 @@ export class App {
|
|||
// doesn't make any sense before a node is selected.
|
||||
document.getElementById('editor-node').style.display = 'grid'
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
|
||||
fetch(`/nodes/connections/${nodeID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(err)
|
||||
return
|
||||
}
|
||||
|
||||
const connectedNodes = new ConnectedNodes(json.Nodes)
|
||||
document.getElementById('connected-nodes').replaceChildren(connectedNodes.render())
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
|
||||
}// }}}
|
||||
async nodeRename(nodeID, name) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -380,7 +396,6 @@ class NodeCreateDialog {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
|
||||
class SelectType {
|
||||
constructor(types) {// {{{
|
||||
this.types = types
|
||||
|
|
@ -549,7 +564,7 @@ export class TreeNode {
|
|||
|
||||
if (this.expanded)
|
||||
this.tree.fetchNodes(this.node.ID)
|
||||
.then(()=>this.render())
|
||||
.then(() => this.render())
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.element === null) {
|
||||
|
|
@ -727,4 +742,55 @@ function typeSort(a, b) {// {{{
|
|||
return 0
|
||||
}// }}}
|
||||
|
||||
class ConnectedNodes {
|
||||
constructor(nodes) {
|
||||
this.nodes = nodes
|
||||
}
|
||||
render() {
|
||||
const div = document.createElement('template')
|
||||
div.innerHTML = `
|
||||
<div class="label">Connected nodes</div>
|
||||
<div class="connected-nodes"></div>
|
||||
`
|
||||
|
||||
const types = new Map()
|
||||
for (const n of this.nodes) {
|
||||
let typeGroup = types.get(n.TypeSchema.title)
|
||||
if (typeGroup === undefined) {
|
||||
typeGroup = document.createElement('div')
|
||||
typeGroup.classList.add('type-group')
|
||||
typeGroup.innerHTML = `<div class="type-name">${n.TypeSchema.title}</div>`
|
||||
types.set(n.TypeSchema.title, typeGroup)
|
||||
}
|
||||
|
||||
typeGroup.appendChild(
|
||||
new ConnectedNode(n).render()
|
||||
)
|
||||
}
|
||||
|
||||
const connectedNodes = div.content.querySelector('.connected-nodes')
|
||||
for (const t of Array.from(types.keys()).sort()) {
|
||||
connectedNodes.append(types.get(t))
|
||||
}
|
||||
|
||||
return div.content
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ConnectedNode {
|
||||
constructor(node) {
|
||||
this.node = node
|
||||
}
|
||||
render() {
|
||||
const tmpl = document.createElement('template')
|
||||
tmpl.innerHTML = `
|
||||
<div class="type-icon"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.node.TypeIcon}.svg" /></div>
|
||||
<div class="node-name">${this.node.Name}</div>
|
||||
`
|
||||
return tmpl.content
|
||||
}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
:root {
|
||||
--textsize: 12pt;
|
||||
--section-color: #73a44d;
|
||||
--je-color: #73a44d;
|
||||
--border-radius: 5px;
|
||||
}
|
||||
|
|
@ -266,3 +267,38 @@ select:focus {
|
|||
outline: 2px solid #888;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
#connected-nodes {
|
||||
& > .label {
|
||||
margin-bottom: 16px;
|
||||
color: var(--section-color);
|
||||
font-weight: bold;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.connected-nodes {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-flow: row wrap;
|
||||
gap: 32px;
|
||||
|
||||
.type-group {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr;
|
||||
grid-gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.type-name {
|
||||
font-weight: bold;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
img {
|
||||
display: block;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<b>References</b>
|
||||
<div class="section" id="connected-nodes">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
23
webserver.go
23
webserver.go
|
|
@ -38,6 +38,7 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/nodes/update/{nodeID}", actionNodeUpdate)
|
||||
http.HandleFunc("/nodes/rename/{nodeID}", actionNodeRename)
|
||||
http.HandleFunc("/nodes/delete/{nodeID}", actionNodeDelete)
|
||||
http.HandleFunc("/nodes/connections/{nodeID}", actionNodeConnections)
|
||||
http.HandleFunc("/nodes/create", actionNodeCreate)
|
||||
http.HandleFunc("/nodes/move", actionNodeMove)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
|
|
@ -235,6 +236,28 @@ func actionNodeDelete(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeConnections(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
nodeID := 0
|
||||
nodeIDStr := r.PathValue("nodeID")
|
||||
nodeID, _ = strconv.Atoi(nodeIDStr)
|
||||
|
||||
nodes, err := GetNodeConnections(nodeID)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
Nodes []Node
|
||||
}{
|
||||
true,
|
||||
nodes,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeMove(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
var req struct {
|
||||
NewParentID int
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue