Saving of nodes
This commit is contained in:
parent
c5bec0afa6
commit
08fd2cf4e9
16 changed files with 852 additions and 42 deletions
BIN
datagraph
BIN
datagraph
Binary file not shown.
55
node.go
55
node.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Standard
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -14,19 +15,56 @@ type Node struct {
|
|||
|
||||
ParentID int `db:"parent_id"`
|
||||
|
||||
TypeID int `db:"type_id"`
|
||||
TypeName string `db:"type_name"`
|
||||
TypeIcon string `db:"type_icon"`
|
||||
TypeID int `db:"type_id"`
|
||||
TypeName string `db:"type_name"`
|
||||
TypeSchema any `db:"type_schema"`
|
||||
TypeSchemaRaw []byte `db:"type_schema_raw" json:"-"`
|
||||
TypeIcon string `db:"type_icon"`
|
||||
|
||||
Updated time.Time
|
||||
Data any
|
||||
RawData []byte `db:"raw_data"`
|
||||
DataRaw []byte `db:"data_raw" json:"-"`
|
||||
|
||||
NumChildren int `db:"num_children"`
|
||||
Children []*Node
|
||||
}
|
||||
|
||||
func GetNode(startNodeID, maxDepth int) (topNode *Node, err error) {
|
||||
func GetNode(nodeID int) (node Node, err error) {
|
||||
row := db.QueryRowx(`
|
||||
SELECT
|
||||
n.id,
|
||||
n.name,
|
||||
n.updated,
|
||||
n.data AS data_raw,
|
||||
|
||||
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)
|
||||
|
||||
err = row.StructScan(&node)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(node.TypeSchemaRaw, &node.TypeSchema)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(node.DataRaw, &node.Data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) {
|
||||
nodes := make(map[int]*Node)
|
||||
var rows *sqlx.Rows
|
||||
rows, err = GetNodeRows(startNodeID, maxDepth)
|
||||
|
|
@ -85,7 +123,7 @@ func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) {
|
|||
t.name AS type_name,
|
||||
COALESCE(t.schema->>'icon', '') AS type_icon,
|
||||
n.updated,
|
||||
n.data AS raw_data,
|
||||
n.data AS data_raw,
|
||||
COUNT(c.child) AS num_children
|
||||
FROM nodes ns
|
||||
INNER JOIN public.node n ON ns.id = n.id
|
||||
|
|
@ -122,3 +160,8 @@ func ComposeTree(nodes map[int]*Node, node *Node) {
|
|||
|
||||
nodes[node.ID] = node
|
||||
}
|
||||
|
||||
func UpdateNode(nodeID int, data []byte) (err error) {
|
||||
_, err = db.Exec(`UPDATE public.node SET data=$2 WHERE id=$1`, nodeID, data)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ html {
|
|||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background-color: #333;
|
||||
background-color: #444 !important;
|
||||
}
|
||||
*,
|
||||
*:before,
|
||||
|
|
@ -21,13 +21,80 @@ body {
|
|||
[onClick] {
|
||||
cursor: pointer;
|
||||
}
|
||||
#nodes {
|
||||
.section {
|
||||
background-color: #fff;
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
margin: 32px;
|
||||
display: none;
|
||||
}
|
||||
.section.show {
|
||||
display: block;
|
||||
}
|
||||
#layout {
|
||||
display: grid;
|
||||
grid-template-areas: "menu menu" "navigation details";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-gap: 32px;
|
||||
padding: 32px;
|
||||
}
|
||||
#menu {
|
||||
grid-area: menu;
|
||||
grid-template-columns: repeat(100, min-content);
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
#menu.section {
|
||||
display: grid;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
#menu .item {
|
||||
cursor: pointer;
|
||||
}
|
||||
#menu .item.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
#logo img {
|
||||
height: 96px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
#nodes {
|
||||
grid-area: navigation;
|
||||
width: min-content;
|
||||
}
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
}
|
||||
#types {
|
||||
grid-area: navigation;
|
||||
}
|
||||
#types .group {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#types .group:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
#types .type {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-gap: 8px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#types .type .img img {
|
||||
height: 24px;
|
||||
display: inline;
|
||||
}
|
||||
#types .type .title {
|
||||
white-space: nowrap;
|
||||
line-height: 24px;
|
||||
}
|
||||
#editor-type-schema {
|
||||
grid-area: details;
|
||||
}
|
||||
.node {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content 100%;
|
||||
|
|
@ -44,7 +111,7 @@ body {
|
|||
padding-right: 8px;
|
||||
}
|
||||
.node .expand-status.leaf {
|
||||
width: 36px;
|
||||
width: 40px;
|
||||
}
|
||||
.node .expand-status.leaf img {
|
||||
display: none;
|
||||
|
|
@ -52,15 +119,16 @@ body {
|
|||
.node .expand-status img {
|
||||
cursor: pointer;
|
||||
}
|
||||
.node .icon {
|
||||
padding-right: 8px;
|
||||
.node .type-icon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
.node .icon img {
|
||||
filter: invert(0.7) sepia(0.5) hue-rotate(50deg) saturate(300%) brightness(0.85);
|
||||
.node .type-icon img {
|
||||
filter: invert(0.7) sepia(0.5) hue-rotate(50deg) saturate(300%) brightness(0.85) !important;
|
||||
}
|
||||
.node .name {
|
||||
margin-bottom: 8px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.node .children {
|
||||
display: none;
|
||||
|
|
|
|||
1
static/css/spectre-exp.min.css
vendored
Normal file
1
static/css/spectre-exp.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/spectre-icons.min.css
vendored
Normal file
1
static/css/spectre-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/spectre.min.css
vendored
Normal file
1
static/css/spectre.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
142
static/images/logo.svg
Normal file
142
static/images/logo.svg
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="79.624962mm"
|
||||
height="78.647499mm"
|
||||
viewBox="0 0 79.624962 78.647499"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||
sodipodi:docname="logo.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="0.60258984"
|
||||
inkscape:cx="66.380144"
|
||||
inkscape:cy="193.33217"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1161"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-38.185371,-67.683764)"><path
|
||||
d="m 51.45301,114.65074 v -1.98318 h 4.210436 q 1.55603,0 2.379811,-0.82378 0.823781,-0.82378 0.823781,-2.50185 V 98.632775 h -3.081551 v -1.983178 h 4.881664 v 12.051613 q 0,5.94953 -5.247789,5.94953 z M 72.535691,98.632775 h -4.942685 q -1.311947,0 -2.074707,0.67123 -0.76276,0.67123 -0.76276,2.135735 0,1.58654 0.610208,2.3493 0.610208,0.73225 1.708582,0.73225 h 1.861134 q 2.379812,0 3.417165,1.34245 1.037354,1.34246 1.037354,3.90533 0,2.41033 -1.342458,3.66125 -1.311947,1.22042 -3.874821,1.22042 h -5.308809 v -1.98318 h 5.827486 q 1.281437,0 1.983176,-0.70174 0.73225,-0.70174 0.73225,-2.16624 0,-1.55603 -0.579698,-2.41032 -0.579697,-0.8848 -1.861134,-0.8848 h -2.013687 q -2.166238,0 -3.142571,-1.25092 -0.976333,-1.28144 -0.976333,-3.84432 0,-2.349305 1.281437,-3.539205 1.281437,-1.220418 3.966352,-1.220418 h 4.454519 z m 10.922694,-1.983178 q 1.372968,0 2.074708,0.671228 0.732249,0.67123 0.732249,2.22726 v 12.021105 q 0,3.08155 -2.959509,3.08155 H 78.45468 q -2.867978,0 -2.867978,-2.86798 V 99.548085 q 0,-2.898488 2.837467,-2.898488 z m -4.362987,1.983178 q -1.617051,0 -1.617051,1.464505 v 10.83119 q 0,0.91531 0.427145,1.34246 0.427146,0.39663 1.006844,0.39663 h 4.088393 q 0.549187,0 0.945823,-0.51867 0.427145,-0.51868 0.427145,-1.22042 v -10.83119 q 0,-0.793275 -0.427145,-1.128895 -0.427146,-0.33561 -1.159396,-0.33561 z M 98.835602,114.65074 H 96.089666 L 90.628304,100.4634 v 14.18734 H 88.858701 V 96.649597 h 2.257769 l 5.919018,15.377243 V 96.649597 h 1.800114 z"
|
||||
id="text1"
|
||||
style="font-size:30.5104px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Normal';text-align:center;text-anchor:middle;fill:#262626;stroke-width:1.71737"
|
||||
aria-label="JSON" /><circle
|
||||
style="fill:#27a698;fill-opacity:1;stroke-width:0.477313;-inkscape-stroke:none"
|
||||
id="path1"
|
||||
cx="46.627323"
|
||||
cy="90.933174"
|
||||
r="4.6399903" /><circle
|
||||
style="fill:#1e6380;fill-opacity:1;stroke-width:0.452723;-inkscape-stroke:none"
|
||||
id="circle1"
|
||||
cx="96.612923"
|
||||
cy="72.084717"
|
||||
r="4.4009533" /><circle
|
||||
style="fill:#21818c;fill-opacity:1;stroke-width:0.529167;-inkscape-stroke:none"
|
||||
id="circle2"
|
||||
cx="43.329437"
|
||||
cy="121.67689"
|
||||
r="5.1440659" /><circle
|
||||
style="fill:#2988af;fill-opacity:1;stroke-width:0.355205;-inkscape-stroke:none"
|
||||
id="circle3"
|
||||
cx="78.708733"
|
||||
cy="125.57096"
|
||||
r="3.4529741" /><circle
|
||||
style="fill:#1e4c80;fill-opacity:1;stroke-width:0.70227;-inkscape-stroke:none"
|
||||
id="circle4"
|
||||
cx="102.07027"
|
||||
cy="124.97508"
|
||||
r="6.8268108" /><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:30.5104px;line-height:normal;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-decoration-color:#000000;text-anchor:middle;fill:#262626;stroke-width:1.71737;-inkscape-stroke:none"
|
||||
x="73.981911"
|
||||
y="186.11093"
|
||||
id="text4"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:30.5104px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.71737"
|
||||
x="73.981911"
|
||||
y="186.11093">JSON</tspan></text><circle
|
||||
style="fill:#1e8071;fill-opacity:1;stroke-width:0.529167;-inkscape-stroke:none"
|
||||
id="circle5"
|
||||
cx="80.797234"
|
||||
cy="85.280693"
|
||||
r="5.1440659" /><circle
|
||||
style="fill:#248d9a;fill-opacity:1;stroke-width:0.529167;-inkscape-stroke:none"
|
||||
id="circle6"
|
||||
cx="112.66627"
|
||||
cy="94.749619"
|
||||
r="5.1440659" /><path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 51.205022,90.175919 75.722226,86.120215"
|
||||
id="path6"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-start="#path1"
|
||||
inkscape:connection-end="#circle5" /><path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 84.746953,81.985206 93.23378,74.904138"
|
||||
id="path7"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-start="#circle5"
|
||||
inkscape:connection-end="#circle1" /><path
|
||||
style="fill:#1e6380;fill-opacity:1;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 85.728154,86.745768 22.007196,6.538776"
|
||||
id="path8"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-start="#circle5"
|
||||
inkscape:connection-end="#circle6" /><path
|
||||
style="display:inline;fill:#1e6380;fill-opacity:1;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 48.4426,122.23968 26.833902,2.95351"
|
||||
id="path9"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-start="#circle2"
|
||||
inkscape:connection-end="#circle3" /><path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 82.160565,125.48292 13.085148,-0.33376"
|
||||
id="path10"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-end="#circle4"
|
||||
inkscape:connection-start="#circle3" /><circle
|
||||
style="fill:#267ea2;fill-opacity:1;stroke-width:0.653982;-inkscape-stroke:none"
|
||||
id="circle10"
|
||||
cx="62.795177"
|
||||
cy="139.97386"
|
||||
r="6.3574047" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#1e6380;fill-opacity:1;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="m 67.508676,135.70781 8.639957,-7.81978"
|
||||
id="path11"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-start="#circle10"
|
||||
inkscape:connection-end="#circle3" /><path
|
||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#1e6380;fill-opacity:1;fill-rule:evenodd;stroke:#205d7b;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1"
|
||||
d="M 99.156616,75.676028 109.69306,90.551905"
|
||||
id="path12"
|
||||
inkscape:connector-type="polyline"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:connection-start="#circle1"
|
||||
inkscape:connection-end="#circle6" /></g></svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
|
|
@ -1,3 +1,124 @@
|
|||
import { Editor } from '@editor'
|
||||
import { MessageBus } from '@mbus'
|
||||
|
||||
export class App {
|
||||
constructor() {// {{{
|
||||
window.mbus = new MessageBus()
|
||||
this.editor = null
|
||||
this.typesList = null
|
||||
this.currentNodeID = null
|
||||
|
||||
const events = [
|
||||
'MENU_ITEM_SELECTED',
|
||||
'NODE_SELECTED',
|
||||
'EDITOR_NODE_SAVE',
|
||||
'TYPES_LIST_FETCHED',
|
||||
]
|
||||
for (const eventName of events)
|
||||
mbus.subscribe(eventName, event => this.eventHandler(event))
|
||||
|
||||
mbus.dispatch('MENU_ITEM_SELECTED', 'node')
|
||||
}// }}}
|
||||
|
||||
eventHandler(event) {// {{{
|
||||
switch (event.type) {
|
||||
case 'MENU_ITEM_SELECTED':
|
||||
const item = document.querySelector(`#menu [data-section="${event.detail}"]`)
|
||||
this.section(item, event.detail)
|
||||
break
|
||||
|
||||
case 'NODE_SELECTED':
|
||||
this.currentNodeID = event.detail
|
||||
this.edit(this.currentNodeID)
|
||||
break
|
||||
|
||||
case 'EDITOR_NODE_SAVE':
|
||||
this.nodeUpdate()
|
||||
break
|
||||
|
||||
case 'TYPES_LIST_FETCHED':
|
||||
const types = document.getElementById('types')
|
||||
types.replaceChildren(this.typesList.render())
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
console.log(event)
|
||||
}
|
||||
}// }}}
|
||||
section(item, name) {// {{{
|
||||
for (const el of document.querySelectorAll('#menu .item'))
|
||||
el.classList.remove('selected')
|
||||
item.classList.add('selected')
|
||||
|
||||
for (const el of document.querySelectorAll('.section.show'))
|
||||
el.classList.remove('show')
|
||||
|
||||
switch (name) {
|
||||
case 'node':
|
||||
document.getElementById('nodes').classList.add('show')
|
||||
document.getElementById('editor-node').classList.add('show')
|
||||
break
|
||||
|
||||
case 'type':
|
||||
document.getElementById('types').classList.add('show')
|
||||
document.getElementById('editor-type-schema').classList.add('show')
|
||||
|
||||
if (this.typesList === null)
|
||||
this.typesList = new TypesList()
|
||||
this.typesList.fetchTypes().then(() => {
|
||||
mbus.dispatch('TYPES_LIST_FETCHED')
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}// }}}
|
||||
edit(nodeID) {// {{{
|
||||
console.log(nodeID)
|
||||
fetch(`/nodes/${nodeID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
const editorEl = document.querySelector('#editor-node .editor')
|
||||
this.editor = new Editor(json.Node.TypeSchema)
|
||||
editorEl.replaceChildren(this.editor.render(json.Node.Data))
|
||||
|
||||
})
|
||||
}// }}}
|
||||
nodeUpdate() {// {{{
|
||||
if (this.editor === null)
|
||||
return
|
||||
|
||||
const btn = document.querySelector('#editor-node .controls button')
|
||||
btn.disabled = true
|
||||
const buttonPressed = Date.now()
|
||||
|
||||
const nodeData = this.editor.data()
|
||||
|
||||
fetch(`/nodes/update/${this.currentNodeID}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(nodeData),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
const timePassed = Date.now() - buttonPressed
|
||||
if (timePassed < 250)
|
||||
setTimeout(()=>btn.disabled = false, 250 - timePassed)
|
||||
else
|
||||
btn.disabled = false
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
constructor(parent, data) {// {{{
|
||||
this.data = data
|
||||
|
|
@ -12,16 +133,18 @@ export class TreeNode {
|
|||
const nodeHTML = `
|
||||
<div class="node">
|
||||
<div class="expand-status"><img /></div>
|
||||
<div class="icon"><img /></div>
|
||||
<div class="type-icon"><img /></div>
|
||||
<div class="name">${this.name()}</div>
|
||||
<div class="children"></div>
|
||||
</div>
|
||||
`
|
||||
this.name()
|
||||
|
||||
const tmpl = document.createElement('template')
|
||||
tmpl.innerHTML = nodeHTML
|
||||
this.children = tmpl.content.querySelector('.children')
|
||||
|
||||
tmpl.content.querySelector('.name').addEventListener('click', () => mbus.dispatch('NODE_SELECTED', this.data.ID))
|
||||
|
||||
// data.NumChildren is set regardless of having fetched the children or not.
|
||||
if (this.hasChildren()) {
|
||||
const img = tmpl.content.querySelector('.expand-status img')
|
||||
|
|
@ -31,7 +154,7 @@ export class TreeNode {
|
|||
tmpl.content.querySelector('.expand-status').classList.add('leaf')
|
||||
|
||||
if (this.data.TypeIcon) {
|
||||
const img = tmpl.content.querySelector('.icon img')
|
||||
const img = tmpl.content.querySelector('.type-icon img')
|
||||
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.data.TypeIcon}.svg`)
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +174,6 @@ export class TreeNode {
|
|||
}// }}}
|
||||
sortChildren() {// {{{
|
||||
this.data.Children.sort((a, b) => {
|
||||
console.log(a.Name, b.Name)
|
||||
if (a.TypeName < b.TypeName) return -1
|
||||
if (a.TypeName > b.TypeName) return 1
|
||||
|
||||
|
|
@ -91,10 +213,81 @@ export class TreeNode {
|
|||
return new Promise((resolve, reject) => {
|
||||
fetch(`/nodes/tree/${this.data.ID}?depth=2`)
|
||||
.then(data => data.json())
|
||||
.then(json => resolve(json))
|
||||
.then(json => {
|
||||
if (json.OK)
|
||||
resolve(json.Nodes)
|
||||
else
|
||||
reject(json.Error)
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class TypesList {
|
||||
constructur() {// {{{
|
||||
this.types = []
|
||||
}// }}}
|
||||
async fetchTypes() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/types/')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
this.types = json.Types
|
||||
resolve()
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const div = document.createElement('div')
|
||||
|
||||
this.types.sort((a,b)=> {
|
||||
if (a.Schema['x-group'] === undefined)
|
||||
a.Schema['x-group'] = 'No group'
|
||||
|
||||
if (b.Schema['x-group'] === undefined)
|
||||
b.Schema['x-group'] = 'No group'
|
||||
|
||||
if (a.Schema['x-group'] < b.Schema['x-group']) return -1
|
||||
if (a.Schema['x-group'] > b.Schema['x-group']) return 1
|
||||
|
||||
if ((a.Schema.title || a.Name) < (b.Schema.title || b.Name)) return -1
|
||||
if ((a.Schema.title || a.Name) > (b.Schema.title || b.Name)) return 1
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
let prevGroup = null
|
||||
|
||||
for (const t of this.types) {
|
||||
if (t.Name == 'root_node')
|
||||
continue
|
||||
|
||||
if (t.Schema['x-group'] != prevGroup) {
|
||||
prevGroup = t.Schema['x-group']
|
||||
const group = document.createElement('div')
|
||||
group.classList.add('group')
|
||||
group.innerText = t.Schema['x-group']
|
||||
div.appendChild(group)
|
||||
}
|
||||
|
||||
const tDiv = document.createElement('div')
|
||||
tDiv.classList.add('type')
|
||||
tDiv.innerHTML = `
|
||||
<div class="img"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/file-document-check-outline.svg" /></div>
|
||||
<div class="title">${t.Schema.title || t.Name}</div>
|
||||
`
|
||||
|
||||
div.appendChild(tDiv)
|
||||
}
|
||||
|
||||
return div
|
||||
}// }}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
27
static/js/editor.mjs
Normal file
27
static/js/editor.mjs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export class Editor {
|
||||
constructor(schema) {
|
||||
this.schema = schema
|
||||
this.editor = null
|
||||
}
|
||||
|
||||
render(data) {
|
||||
const div = document.createElement('div')
|
||||
this.editor = new JSONEditor(div, {
|
||||
theme: 'spectre',
|
||||
iconlib: 'spectre',
|
||||
disable_collapse: true,
|
||||
disable_properties: true,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
this.editor.on('ready', () => {
|
||||
this.editor.setValue(data)
|
||||
})
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
data() {
|
||||
return this.editor.getValue()
|
||||
}
|
||||
}
|
||||
2
static/js/lib/jsoneditor.js
Normal file
2
static/js/lib/jsoneditor.js
Normal file
File diff suppressed because one or more lines are too long
17
static/js/mbus.mjs
Normal file
17
static/js/mbus.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export class MessageBus {
|
||||
constructor() {
|
||||
this.bus = new EventTarget()
|
||||
}
|
||||
|
||||
subscribe(eventName, fn) {
|
||||
this.bus.addEventListener(eventName, fn)
|
||||
}
|
||||
|
||||
unsubscribe(eventName, fn) {
|
||||
this.bus.removeEventListener(eventName, fn)
|
||||
}
|
||||
|
||||
dispatch(eventName, data) {
|
||||
this.bus.dispatchEvent(new CustomEvent(eventName, { detail: data }))
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ html {
|
|||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background-color: #333;
|
||||
background-color: #444 !important;
|
||||
}
|
||||
|
||||
*,
|
||||
|
|
@ -26,14 +26,105 @@ body {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#nodes {
|
||||
.section {
|
||||
background-color: #fff;
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
margin: 32px;
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"menu menu"
|
||||
"navigation details"
|
||||
;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-gap: 32px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
#menu {
|
||||
grid-area: menu;
|
||||
grid-template-columns: repeat(100, min-content);
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
&.section {
|
||||
display: grid;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#logo {
|
||||
img {
|
||||
height: 96px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#nodes {
|
||||
grid-area: navigation;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
}
|
||||
|
||||
#types {
|
||||
grid-area: navigation;
|
||||
|
||||
.group {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-gap: 8px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.img {
|
||||
img {
|
||||
height: 24px;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#editor-type-schema {
|
||||
grid-area: details;
|
||||
}
|
||||
|
||||
.node {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content 100%;
|
||||
|
|
@ -54,7 +145,7 @@ body {
|
|||
padding-right: 8px;
|
||||
|
||||
&.leaf {
|
||||
width: 36px;
|
||||
width: 40px;
|
||||
img {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -65,17 +156,18 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-right: 8px;
|
||||
.type-icon {
|
||||
padding-right: 4px;
|
||||
|
||||
img {
|
||||
filter: invert(.7) sepia(.5) hue-rotate(50deg) saturate(300%) brightness(0.85);
|
||||
filter: invert(.7) sepia(.5) hue-rotate(50deg) saturate(300%) brightness(0.85) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-bottom: 8px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.children {
|
||||
|
|
|
|||
76
type.go
Normal file
76
type.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
// External
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Standard
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NodeType struct {
|
||||
ID int
|
||||
Name string
|
||||
SchemaRaw []byte `db:"schema_raw" json:"-"`
|
||||
Schema any
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
func GetType(typeID int) (typ NodeType, err error) {
|
||||
row := db.QueryRowx(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
schema AS schema_raw,
|
||||
updated
|
||||
FROM public.type
|
||||
WHERE
|
||||
id = $1
|
||||
`, typeID)
|
||||
|
||||
err = row.StructScan(&typ)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(typ.SchemaRaw, &typ.Schema)
|
||||
return
|
||||
}
|
||||
|
||||
func GetTypes() (types []NodeType, err error) {
|
||||
types = []NodeType{}
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
updated,
|
||||
schema AS schema_raw
|
||||
FROM
|
||||
public.type
|
||||
ORDER BY
|
||||
name ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var typ NodeType
|
||||
err = rows.StructScan(&typ)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(typ.SchemaRaw, &typ.Schema)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
types = append(types, typ)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -4,6 +4,12 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
|
||||
<script type="text/javascript">
|
||||
function showError(err) {
|
||||
console.error(err)
|
||||
alert(err)
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">{{ block "page" . }}{{ end }}</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,56 @@
|
|||
{{ define "page" }}
|
||||
<script type="module" defer>
|
||||
import { TreeNode } from '/js/{{ .VERSION }}/app.mjs'
|
||||
window._VERSION = '{{ .VERSION }}'
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@editor": "/js/{{ .VERSION }}/editor.mjs",
|
||||
"@mbus": "/js/{{ .VERSION }}/mbus.mjs"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
fetch('/nodes/tree/0?depth=2')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
const top = document.getElementById('nodes')
|
||||
const topNode = new TreeNode(top, json)
|
||||
topNode.render()
|
||||
})
|
||||
<script src="/js/{{ .VERSION }}/lib/jsoneditor.js"></script>
|
||||
<!--<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script>-->
|
||||
<script type="module" defer>
|
||||
import {App, TreeNode} from '/js/{{ .VERSION }}/app.mjs'
|
||||
window._VERSION = '{{ .VERSION }}'
|
||||
window._app = new App()
|
||||
|
||||
fetch('/nodes/tree/0?depth=2')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
const top = document.getElementById('nodes')
|
||||
const topNode = new TreeNode(top, json.Nodes)
|
||||
topNode.render()
|
||||
})
|
||||
|
||||
</script>
|
||||
<div id="nodes"></div>
|
||||
|
||||
<div id="layout">
|
||||
<div class="section" id="menu">
|
||||
<div class="item" id="logo"><img src="/images/{{ .VERSION }}/logo.svg" /></div>
|
||||
<div class="item" data-section='node' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'node')">Nodes</div>
|
||||
<div class="item" data-section='type' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'type')">Types</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="nodes"></div>
|
||||
<div class="section" id="editor-node">
|
||||
<div class="editor"></div>
|
||||
<div class="controls">
|
||||
<button onclick="mbus.dispatch('EDITOR_NODE_SAVE')">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="types"></div>
|
||||
<div class="section" id="editor-type-schema"></div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="/css/{{ .VERSION }}/spectre.min.css">
|
||||
<link rel="stylesheet" href="/css/{{ .VERSION }}/spectre-exp.min.css">
|
||||
<link rel="stylesheet" href="/css/{{ .VERSION }}/spectre-icons.min.css">
|
||||
|
||||
{{ end }}
|
||||
|
|
|
|||
111
webserver.go
111
webserver.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
// Standard
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
|
@ -31,11 +32,27 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/", pageIndex)
|
||||
http.HandleFunc("/app", pageApp)
|
||||
http.HandleFunc("/nodes/tree/{startNode}", actionNodesTree)
|
||||
http.HandleFunc("/nodes/{nodeID}", actionNode)
|
||||
http.HandleFunc("/nodes/update/{nodeID}", actionNodeUpdate)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
http.HandleFunc("/types/", actionTypesAll)
|
||||
|
||||
err = http.ListenAndServe(address, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, err error) { // {{{
|
||||
out := struct {
|
||||
OK bool
|
||||
Error string
|
||||
}{
|
||||
false,
|
||||
err.Error(),
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/app", http.StatusSeeOther)
|
||||
|
|
@ -43,7 +60,6 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
engine.StaticResource(w, r)
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func pageApp(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
page := NewPage("app")
|
||||
err := engine.Render(page, w, r)
|
||||
|
|
@ -64,11 +80,96 @@ func actionNodesTree(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
maxDepth = 3
|
||||
}
|
||||
|
||||
topNode, err := GetNode(startNode, maxDepth)
|
||||
topNode, err := GetNodeTree(startNode, maxDepth)
|
||||
if err != nil {
|
||||
logger.Error("test", "error", err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(topNode)
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
Nodes *Node
|
||||
}{
|
||||
true,
|
||||
topNode,
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNode(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
nodeID := 0
|
||||
nodeIDStr := r.PathValue("nodeID")
|
||||
nodeID, _ = strconv.Atoi(nodeIDStr)
|
||||
|
||||
node, err := GetNode(nodeID)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
Node Node
|
||||
}{
|
||||
true,
|
||||
node,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeUpdate(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
nodeID := 0
|
||||
nodeIDStr := r.PathValue("nodeID")
|
||||
nodeID, _ = strconv.Atoi(nodeIDStr)
|
||||
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
|
||||
err := UpdateNode(nodeID, data)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionType(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
typeID := 0
|
||||
typeIDStr := r.PathValue("typeID")
|
||||
typeID, _ = strconv.Atoi(typeIDStr)
|
||||
|
||||
typ, err := GetType(typeID)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(typ)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionTypesAll(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
types, err := GetTypes()
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
Types []NodeType
|
||||
}{
|
||||
true,
|
||||
types,
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue