Connected nodes
This commit is contained in:
parent
2b8472bcd1
commit
ca0659a368
9 changed files with 581 additions and 89 deletions
213
node.go
213
node.go
|
|
@ -27,50 +27,86 @@ type Node struct {
|
|||
TypeSchemaRaw []byte `db:"type_schema_raw" json:"-"`
|
||||
TypeIcon string `db:"type_icon"`
|
||||
|
||||
ConnectionID int
|
||||
ConnectionData any
|
||||
|
||||
Updated time.Time
|
||||
Data any
|
||||
DataRaw []byte `db:"data_raw" json:"-"`
|
||||
|
||||
NumChildren int `db:"num_children"`
|
||||
Children []*Node
|
||||
|
||||
ConnectedNodes []Node
|
||||
}
|
||||
|
||||
func GetNode(nodeID int) (node Node, err error) { // {{{
|
||||
row := db.QueryRowx(`
|
||||
SELECT
|
||||
n.id,
|
||||
COALESCE(n.parent_id, -1) AS parent_id,
|
||||
n.name,
|
||||
n.updated,
|
||||
n.data AS data_raw,
|
||||
to_json(res) AS node
|
||||
FROM (
|
||||
SELECT
|
||||
n.id,
|
||||
COALESCE(n.parent_id, -1) AS ParentID,
|
||||
n.name,
|
||||
n.updated,
|
||||
n.data AS data,
|
||||
|
||||
t.id AS type_id,
|
||||
t.name AS type_name,
|
||||
t.schema AS type_schema_raw,
|
||||
t.schema->>'icon' AS type_icon
|
||||
FROM public.node n
|
||||
INNER JOIN public.type t ON n.type_id = t.id
|
||||
WHERE
|
||||
n.id = $1
|
||||
`, nodeID)
|
||||
t.id AS TypeID,
|
||||
t.name AS TypeName,
|
||||
t.schema AS TypeSchema,
|
||||
t.schema->>'icon' AS TypeIcon,
|
||||
|
||||
err = row.StructScan(&node)
|
||||
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 public.node n
|
||||
INNER JOIN public.type t ON n.type_id = t.id
|
||||
WHERE
|
||||
n.id = $1
|
||||
) res
|
||||
`,
|
||||
nodeID,
|
||||
)
|
||||
|
||||
var body []byte
|
||||
err = row.Scan(&body)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(node.TypeSchemaRaw, &node.TypeSchema)
|
||||
err = json.Unmarshal(body, &node)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(node.DataRaw, &node.Data)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
} // }}}
|
||||
|
||||
|
|
@ -243,10 +279,10 @@ func GetNodeConnections(nodeID int) (connections []Node, err error) { // {{{
|
|||
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.node n ON c.child_node_id = n.id
|
||||
INNER JOIN public.type t ON n.type_id = t.id
|
||||
WHERE
|
||||
c.from = $1
|
||||
c.parent_node_id = $1
|
||||
`,
|
||||
nodeID,
|
||||
)
|
||||
|
|
@ -275,66 +311,115 @@ func GetNodeConnections(nodeID int) (connections []Node, err error) { // {{{
|
|||
|
||||
return
|
||||
} // }}}
|
||||
func ConnectNode(parentID, childID int) (err error) { // {{{
|
||||
_, err = db.Exec(`INSERT INTO public.connection(parent_node_id, child_node_id) VALUES($1, $2)`, parentID, childID)
|
||||
return
|
||||
} // }}}
|
||||
func SearchNodes(typeID int, search string, maxResults int) (nodes []Node, err error) { // {{{
|
||||
nodes = []Node{}
|
||||
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
row := db.QueryRowx(`
|
||||
SELECT
|
||||
n.id,
|
||||
n.name,
|
||||
n.updated,
|
||||
n.data,
|
||||
COALESCE(n.parent_id, 0) AS parent_id,
|
||||
json_agg(res) AS node
|
||||
FROM (
|
||||
SELECT
|
||||
n.id,
|
||||
n.name,
|
||||
n.updated,
|
||||
n.data,
|
||||
COALESCE(n.parent_id, 0) AS ParentID,
|
||||
|
||||
t.id AS type_id,
|
||||
t.name AS type_name,
|
||||
t.schema AS type_schema_raw,
|
||||
t.schema->>'icon' AS type_icon
|
||||
FROM public.node n
|
||||
INNER JOIN public.type t ON n.type_id = t.id
|
||||
WHERE
|
||||
n.id > 0 AND
|
||||
n.name ILIKE $2 AND
|
||||
(CASE
|
||||
WHEN $1 = -1 THEN true
|
||||
ELSE
|
||||
type_id = $1
|
||||
END)
|
||||
t.id AS TypeID,
|
||||
t.name AS TypeName,
|
||||
t.schema AS TypeSchema,
|
||||
t.schema->>'icon' AS TypeIcon,
|
||||
|
||||
ORDER BY
|
||||
t.schema->>'title' ASC,
|
||||
UPPER(n.name) ASC
|
||||
LIMIT $3
|
||||
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 public.node n
|
||||
INNER JOIN public.type t ON n.type_id = t.id
|
||||
WHERE
|
||||
n.id > 0 AND
|
||||
n.name ILIKE $2 AND
|
||||
(CASE
|
||||
WHEN $1 = -1 THEN true
|
||||
ELSE
|
||||
type_id = $1
|
||||
END)
|
||||
|
||||
ORDER BY
|
||||
t.schema->>'title' ASC,
|
||||
UPPER(n.name) ASC
|
||||
LIMIT $3
|
||||
) AS res
|
||||
`,
|
||||
typeID,
|
||||
search,
|
||||
maxResults+1,
|
||||
)
|
||||
|
||||
var body []byte
|
||||
err = row.Scan(&body)
|
||||
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
|
||||
}
|
||||
|
||||
nodes = append(nodes, node)
|
||||
err = json.Unmarshal(body, &nodes)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
} // }}}
|
||||
|
||||
func UpdateConnection(connID int, data []byte) (err error) { // {{{
|
||||
_, err = db.Exec(`UPDATE public.connection SET data=$2 WHERE id=$1`, connID, data)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok && pqErr.Code == "22P02" {
|
||||
err = errors.New("Invalid JSON")
|
||||
} else {
|
||||
err = werr.Wrap(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
} // }}}
|
||||
func DeleteConnection(connID int) (err error) {// {{{
|
||||
_, err = db.Exec(`DELETE FROM public.connection WHERE id=$1`, connID)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}// }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
2
sql/0006.sql
Normal file
2
sql/0006.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE public."connection" RENAME COLUMN "from" TO parent_node_id;
|
||||
ALTER TABLE public."connection" RENAME COLUMN "to" TO child_node_id;
|
||||
1
sql/0007.sql
Normal file
1
sql/0007.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE public."connection" ADD CONSTRAINT connection_unique UNIQUE (parent_node_id,child_node_id);
|
||||
1
sql/0008.sql
Normal file
1
sql/0008.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE public."connection" ADD "data" jsonb DEFAULT '{}' NOT NULL;
|
||||
|
|
@ -229,6 +229,10 @@ select:focus {
|
|||
font-weight: bold;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
#connected-nodes .connected-nodes .type-group .type-icon,
|
||||
#connected-nodes .connected-nodes .type-group .node-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
#connected-nodes .type-icon img {
|
||||
display: block;
|
||||
height: 24px;
|
||||
|
|
@ -276,3 +280,23 @@ select:focus {
|
|||
cursor: pointer;
|
||||
height: 24px;
|
||||
}
|
||||
dialog#connection-data {
|
||||
padding: 24px;
|
||||
}
|
||||
dialog#connection-data .label {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: var(--section-color);
|
||||
}
|
||||
dialog#connection-data img {
|
||||
height: 32px;
|
||||
}
|
||||
dialog#connection-data textarea {
|
||||
margin-top: 16px;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
dialog#connection-data div.button {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Editor } from '@editor'
|
||||
import { MessageBus } from '@mbus'
|
||||
import { SelectType, SelectNode } from '@select_node'
|
||||
import { SelectType, SelectNodeDialog, ConnectionDataDialog } from '@select_node'
|
||||
|
||||
export class App {
|
||||
constructor() {// {{{
|
||||
|
|
@ -41,9 +41,9 @@ export class App {
|
|||
break
|
||||
|
||||
case 'NODE_CONNECT':
|
||||
const selectnode = new SelectNode(selectedNode => {
|
||||
const selectnode = new SelectNodeDialog(selectedNode => {
|
||||
this.nodeConnect(this.currentNode, selectedNode)
|
||||
.then(() => this.edit(this.currentNode))
|
||||
.then(() => this.edit(this.currentNode.ID))
|
||||
})
|
||||
selectnode.render()
|
||||
break
|
||||
|
|
@ -202,23 +202,11 @@ export class App {
|
|||
// The editor-node div is hidden from the start as a lot of the elements
|
||||
// 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)
|
||||
const connectedNodes = new ConnectedNodes(json.Node.ConnectedNodes)
|
||||
document.getElementById('connected-nodes').replaceChildren(connectedNodes.render())
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
|
||||
}// }}}
|
||||
async nodeRename(nodeID, name) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -318,12 +306,28 @@ export class App {
|
|||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
async nodeConnect(parentNode, nodeToConnect) {
|
||||
return new Promise((resolve, reject)=>{
|
||||
// XXX - here
|
||||
//fetch('/nodes/)
|
||||
async nodeConnect(parentNode, nodeToConnect) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = {
|
||||
ParentNodeID: parentNode.ID,
|
||||
ChildNodeID: nodeToConnect.ID,
|
||||
}
|
||||
|
||||
fetch(`/nodes/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}
|
||||
}// }}}
|
||||
typeSort(a, b) {// {{{
|
||||
if (a.Schema['x-group'] === undefined)
|
||||
a.Schema['x-group'] = 'No group'
|
||||
|
|
@ -763,6 +767,12 @@ class ConnectedNode {
|
|||
<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>
|
||||
`
|
||||
|
||||
for (const el of tmpl.content.children) {
|
||||
el.addEventListener('click', () => {
|
||||
new ConnectionDataDialog(this.node, () => _app.edit(_app.currentNode.ID)).render()
|
||||
})
|
||||
}
|
||||
return tmpl.content
|
||||
}// }}}
|
||||
}
|
||||
|
|
|
|||
256
static/js/select_node.mjs
Normal file
256
static/js/select_node.mjs
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
export class SelectNodeDialog {
|
||||
constructor(callback) {// {{{
|
||||
this.selectType = new SelectType
|
||||
this.searchResults = null
|
||||
this.searchText = null
|
||||
this.selectType = null
|
||||
this.nodeTable = null
|
||||
this.moreExist = null
|
||||
|
||||
if (callback !== undefined)
|
||||
this.callback = callback
|
||||
else
|
||||
this.callback = () => { }
|
||||
}// }}}
|
||||
async render() {// {{{
|
||||
const dlg = document.createElement('dialog')
|
||||
dlg.id = 'select-node'
|
||||
dlg.addEventListener('close', () => dlg.remove())
|
||||
|
||||
dlg.innerHTML = `
|
||||
<div class="label">Search for node</div>
|
||||
<input class="search-text" type="text" placeholder="Search" />
|
||||
<select></select>
|
||||
<div style="display: grid; grid-template-columns: min-content 1fr; align-items: center; grid-gap: 16px;">
|
||||
<button>Search</button>
|
||||
<div class="more-exist"></div>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
`
|
||||
|
||||
this.nodeTable = new NodeTable((_node, node) => {
|
||||
this.callback(node)
|
||||
dlg.close()
|
||||
})
|
||||
|
||||
this.searchText = dlg.querySelector('.search-text')
|
||||
this.searchResults = dlg.querySelector('.search-results')
|
||||
this.moreExist = dlg.querySelector('.more-exist')
|
||||
const button = dlg.querySelector('button')
|
||||
button.addEventListener('click', () => this.search())
|
||||
|
||||
this.searchText.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter')
|
||||
this.search()
|
||||
})
|
||||
this.searchText.focus()
|
||||
this.searchText.value = '%'
|
||||
|
||||
new SelectType(true).render()
|
||||
.then(select => {
|
||||
this.selectType = select
|
||||
dlg.querySelector('select').replaceWith(this.selectType)
|
||||
})
|
||||
|
||||
document.body.appendChild(dlg)
|
||||
dlg.showModal()
|
||||
}// }}}
|
||||
search() {// {{{
|
||||
const type_id = this.selectType.value
|
||||
const search = this.searchText.value
|
||||
this.moreExist.innerText = ''
|
||||
|
||||
fetch(`/nodes/search?` + new URLSearchParams({ type_id, search }))
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
this.nodeTable.clearNodes()
|
||||
this.nodeTable.addNodes(json.Nodes)
|
||||
this.searchResults.replaceChildren(this.nodeTable.render())
|
||||
|
||||
if (json.MoreExistThan > 0)
|
||||
this.moreExist.innerText = `Only displaying ${json.MoreExistThan} nodes. There are more matching the given criteria.`
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class SelectType {
|
||||
constructor(allowNoType) {// {{{
|
||||
this.allowNoType = allowNoType
|
||||
}// }}}
|
||||
async render() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fetchTypes()
|
||||
.then(types => {
|
||||
const select = document.createElement('select')
|
||||
|
||||
if (this.allowNoType) {
|
||||
const option = document.createElement('option')
|
||||
option.setAttribute('value', -1)
|
||||
option.innerText = '[ No specific type ]'
|
||||
select.appendChild(option)
|
||||
}
|
||||
|
||||
types.sort(_app.typeSort)
|
||||
let prevGroup = null
|
||||
for (const t of types) {
|
||||
if (t.Name == 'root_node')
|
||||
continue
|
||||
|
||||
if (t.Schema['x-group'] != prevGroup) {
|
||||
prevGroup = t.Schema['x-group']
|
||||
const group = document.createElement('optgroup')
|
||||
group.setAttribute('label', t.Schema['x-group'])
|
||||
select.appendChild(group)
|
||||
}
|
||||
|
||||
const opt = document.createElement('option')
|
||||
opt.setAttribute('value', t.ID)
|
||||
opt.innerHTML = ' ' + (t.Schema.title || t.Name)
|
||||
select.appendChild(opt)
|
||||
}
|
||||
|
||||
resolve(select)
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
async fetchTypes() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/types/')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
resolve(json.Types)
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class NodeTable {
|
||||
constructor(callback) {// {{{
|
||||
this.nodes = new Map()
|
||||
|
||||
if (callback !== undefined)
|
||||
this.callback = callback
|
||||
else
|
||||
this.callback = () => { }
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('node-table')
|
||||
|
||||
for (const k of Array.from(this.nodes.keys())) {
|
||||
const group = document.createElement('div')
|
||||
group.classList.add('group')
|
||||
group.innerHTML = `
|
||||
<div class="label">${k}</div>
|
||||
<div class="children"></div>
|
||||
`
|
||||
|
||||
const groupChildren = group.querySelector('.children')
|
||||
for (const n of this.nodes.get(k)) {
|
||||
const icon = document.createElement('img')
|
||||
icon.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${n.TypeIcon}.svg`)
|
||||
icon.addEventListener('click', event => this.callback(event, n))
|
||||
|
||||
const node = document.createElement('div')
|
||||
node.classList.add('node')
|
||||
node.innerText = n.Name
|
||||
node.addEventListener('click', event => this.callback(event, n))
|
||||
|
||||
groupChildren.appendChild(icon)
|
||||
groupChildren.appendChild(node)
|
||||
}
|
||||
|
||||
div.appendChild(group)
|
||||
}
|
||||
|
||||
return div
|
||||
}// }}}
|
||||
clearNodes() {// {{{
|
||||
this.nodes = new Map()
|
||||
}// }}}
|
||||
addNodes(nodes) {// {{{
|
||||
for (const n of nodes) {
|
||||
let tableNodes = this.nodes.get(n.TypeSchema.title)
|
||||
if (tableNodes === undefined) {
|
||||
tableNodes = []
|
||||
this.nodes.set(n.TypeSchema.title, tableNodes)
|
||||
}
|
||||
tableNodes.push(n)
|
||||
}
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class ConnectionDataDialog {
|
||||
constructor(node, callback) {// {{{
|
||||
this.node = node
|
||||
this.callback = callback
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const dlg = document.createElement('dialog')
|
||||
dlg.id = 'connection-data'
|
||||
dlg.addEventListener('close', () => dlg.remove())
|
||||
|
||||
dlg.innerHTML = `
|
||||
<div>
|
||||
<div style="float: left;" class="label">Connection data</div>
|
||||
<div style="float: right;"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/trash-can.svg" /></div>
|
||||
</div>
|
||||
<div style="clear: both;"><b>${this.node.Name}</b></div>
|
||||
<div><textarea></textarea></div>
|
||||
<div class="button"><button>Update</button></div>
|
||||
`
|
||||
dlg.querySelector('textarea').value = JSON.stringify(this.node.ConnectionData, null, 4)
|
||||
|
||||
dlg.querySelector('img').addEventListener('click', ()=>{
|
||||
if(!confirm('Do you want to delete the connection?'))
|
||||
return
|
||||
|
||||
fetch(`/connection/delete/${this.node.ConnectionID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
dlg.close()
|
||||
this.callback()
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
})
|
||||
|
||||
dlg.querySelector('button').addEventListener('click', () => {
|
||||
// Connection data is updated.
|
||||
fetch(`/connection/update/${this.node.ConnectionID}`, {
|
||||
method: 'POST',
|
||||
body: dlg.querySelector('textarea').value,
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
dlg.close()
|
||||
this.callback()
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
})
|
||||
|
||||
document.body.appendChild(dlg)
|
||||
dlg.showModal()
|
||||
}// }}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
@ -304,6 +304,10 @@ select:focus {
|
|||
font-weight: bold;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.type-icon, .node-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,3 +372,28 @@ select:focus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog#connection-data {
|
||||
padding: 24px;
|
||||
|
||||
.label {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: var(--section-color);
|
||||
}
|
||||
|
||||
img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 16px;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
div.button {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
webserver.go
92
webserver.go
|
|
@ -4,9 +4,11 @@ import (
|
|||
// External
|
||||
"git.ahall.se/go/html_template"
|
||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||
"github.com/lib/pq"
|
||||
|
||||
// Standard
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
|
@ -42,8 +44,11 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/nodes/create", actionNodeCreate)
|
||||
http.HandleFunc("/nodes/move", actionNodeMove)
|
||||
http.HandleFunc("/nodes/search", actionNodeSearch)
|
||||
http.HandleFunc("/nodes/connect", actionNodeConnect)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
http.HandleFunc("/types/", actionTypesAll)
|
||||
http.HandleFunc("/connection/update/{connID}", actionConnectionUpdate)
|
||||
http.HandleFunc("/connection/delete/{connID}", actionConnectionDelete)
|
||||
|
||||
err = http.ListenAndServe(address, nil)
|
||||
return
|
||||
|
|
@ -250,7 +255,7 @@ func actionNodeConnections(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
OK bool
|
||||
Nodes []Node
|
||||
}{
|
||||
true,
|
||||
|
|
@ -280,7 +285,7 @@ func actionNodeMove(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
|
|
@ -312,8 +317,8 @@ func actionNodeSearch(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
Nodes []Node
|
||||
OK bool
|
||||
Nodes []Node
|
||||
MoreExistThan int
|
||||
}{
|
||||
true,
|
||||
|
|
@ -328,6 +333,37 @@ func actionNodeSearch(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeConnect(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
var req struct {
|
||||
ParentNodeID int
|
||||
ChildNodeID int
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(body, &req)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ConnectNode(req.ParentNodeID, req.ChildNodeID)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok && pqErr.Code == "23505" {
|
||||
err = errors.New("This node is already connected.")
|
||||
} 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) { // {{{
|
||||
typeID := 0
|
||||
typeIDStr := r.PathValue("typeID")
|
||||
|
|
@ -362,5 +398,53 @@ func actionTypesAll(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionConnectionUpdate(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
connIDStr := r.PathValue("connID")
|
||||
connID, err := strconv.Atoi(connIDStr)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
|
||||
err = UpdateConnection(connID, data)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionConnectionDelete(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
connIDStr := r.PathValue("connID")
|
||||
connID, err := strconv.Atoi(connIDStr)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = DeleteConnection(connID)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue