Download history to client
This commit is contained in:
parent
65a0225d74
commit
9506b89453
5 changed files with 249 additions and 75 deletions
26
main.go
26
main.go
|
|
@ -25,7 +25,7 @@ import (
|
||||||
|
|
||||||
const VERSION = "v11"
|
const VERSION = "v11"
|
||||||
const CONTEXT_USER = 1
|
const CONTEXT_USER = 1
|
||||||
const SYNC_PAGINATION = 200
|
const SYNC_PAGINATION = 20
|
||||||
|
|
||||||
var (
|
var (
|
||||||
FlagGenerate bool
|
FlagGenerate bool
|
||||||
|
|
@ -140,6 +140,7 @@ func main() { // {{{
|
||||||
http.HandleFunc("/sync/to_server", authenticated(actionSyncToServer))
|
http.HandleFunc("/sync/to_server", authenticated(actionSyncToServer))
|
||||||
|
|
||||||
http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve))
|
http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve))
|
||||||
|
http.HandleFunc("/node/history/retrieve/{uuid}/{offset}", authenticated(actionNodeHistoryRetrieve))
|
||||||
|
|
||||||
http.HandleFunc("/service_worker.js", pageServiceWorker)
|
http.HandleFunc("/service_worker.js", pageServiceWorker)
|
||||||
|
|
||||||
|
|
@ -328,6 +329,29 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
"Node": node,
|
"Node": node,
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
user := getUser(r)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
uuid := r.PathValue("uuid")
|
||||||
|
offset, err := strconv.Atoi(r.PathValue("offset"))
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, hasMore, err := RetrieveNodeHistory(user.UserID, uuid, offset)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]any{
|
||||||
|
"OK": true,
|
||||||
|
"Nodes": nodes,
|
||||||
|
"HasMore": hasMore,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
user := getUser(r)
|
user := getUser(r)
|
||||||
|
|
||||||
|
|
|
||||||
47
node.go
47
node.go
|
|
@ -3,8 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
// External
|
// External
|
||||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/derektata/lorem/ipsum"
|
"github.com/derektata/lorem/ipsum"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
@ -248,6 +248,51 @@ func RetrieveNode(userID int, nodeUUID string) (node Node, err error) { // {{{
|
||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func RetrieveNodeHistory(userID int, nodeUUID string, offset int) (nodes []Node, hasMore bool, err error) { // {{{
|
||||||
|
nodes = []Node{}
|
||||||
|
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
rows, err = db.Queryx(`
|
||||||
|
SELECT
|
||||||
|
uuid,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
content,
|
||||||
|
content_encrypted
|
||||||
|
FROM node_history
|
||||||
|
WHERE
|
||||||
|
user_id = $1 AND
|
||||||
|
uuid = $2
|
||||||
|
LIMIT $3 OFFSET $4
|
||||||
|
`,
|
||||||
|
userID,
|
||||||
|
nodeUUID,
|
||||||
|
SYNC_PAGINATION+1,
|
||||||
|
offset,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = werr.Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
node := Node{}
|
||||||
|
if err = rows.StructScan(&node); err != nil {
|
||||||
|
err = werr.Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nodes) > SYNC_PAGINATION {
|
||||||
|
hasMore = true
|
||||||
|
nodes = nodes[0 : len(nodes)-1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
|
func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
|
||||||
var rows *sqlx.Rows
|
var rows *sqlx.Rows
|
||||||
rows, err = db.Queryx(`
|
rows, err = db.Queryx(`
|
||||||
|
|
|
||||||
|
|
@ -516,87 +516,115 @@ dialog.op {
|
||||||
|
|
||||||
|
|
||||||
n2-pagehistory {
|
n2-pagehistory {
|
||||||
.layout {
|
|
||||||
|
.back,
|
||||||
|
.node-name {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.el-back-image,
|
.group-label {
|
||||||
.el-back-text {
|
font-weight: bold;
|
||||||
cursor: pointer;
|
background-color: #444;
|
||||||
}
|
color: #fff;
|
||||||
|
padding: 8px 32px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 32px;
|
||||||
|
transform: translateY(14px);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.el-node-name {
|
.group {
|
||||||
margin-left: 8px;
|
border: 1px solid #ccc;
|
||||||
}
|
padding: 32px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
|
||||||
.el-nodes {
|
box-shadow:
|
||||||
grid-column: 1 / -1;
|
rgba(0, 0, 0, 0.4) 0px 2px 4px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 7px 13px -3px,
|
||||||
|
rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
|
||||||
|
}
|
||||||
|
|
||||||
display: grid;
|
.el-back-image,
|
||||||
grid-template-columns: min-content minmax(min-content, max-content) min-content 1fr;
|
.el-back-text {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
background-color: var(--line-color);
|
.el-node-name {
|
||||||
gap: 1px;
|
margin-left: 8px;
|
||||||
border: 1px solid var(--line-color);
|
}
|
||||||
|
|
||||||
&>div>div {
|
.el-nodes {
|
||||||
padding: 8px 12px;
|
grid-column: 1 / -1;
|
||||||
background-color: #fff;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.index {
|
display: grid;
|
||||||
text-align: right;
|
grid-template-columns: min-content minmax(min-content, max-content) min-content 1fr;
|
||||||
}
|
|
||||||
|
|
||||||
&.updated {
|
background-color: var(--line-color);
|
||||||
white-space: initial;
|
gap: 1px;
|
||||||
}
|
border: 1px solid var(--line-color);
|
||||||
|
|
||||||
.date {
|
&>div>div {
|
||||||
white-space: nowrap;
|
padding: 8px 12px;
|
||||||
font-weight: bold;
|
background-color: #fff;
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.name {
|
|
||||||
white-space: initial;
|
|
||||||
/*overflow-wrap: anywhere;*/
|
|
||||||
word-break: break-all;
|
|
||||||
color: var(--color1);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-node {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, min-content);
|
|
||||||
grid-gap: 32px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
.el-prev {
|
&.index {
|
||||||
font-weight: bold;
|
text-align: right;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-next {
|
&.updated {
|
||||||
font-weight: bold;
|
white-space: initial;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
white-space: initial;
|
||||||
|
/*overflow-wrap: anywhere;*/
|
||||||
|
word-break: break-all;
|
||||||
|
color: var(--color1);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-node {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
grid-gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.el-prev,
|
||||||
|
.el-next {
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -456,7 +456,24 @@ class NodeHistoryStore extends SimpleNodeStore {
|
||||||
request.onerror = (event) => reject(event.target.error)
|
request.onerror = (event) => reject(event.target.error)
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
retrievePage(uuid, perPage, page) {
|
hasNode(uuid, updated) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = this.db
|
||||||
|
.transaction(['nodes', this.storeName], 'readonly')
|
||||||
|
.objectStore(this.storeName)
|
||||||
|
.getKey([uuid, updated])
|
||||||
|
|
||||||
|
req.onsuccess = (event) => {
|
||||||
|
resolve(event.target.result !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.onerror = (event) => {
|
||||||
|
console.log(event.target.error)
|
||||||
|
reject(event.target.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
retrievePage(uuid, perPage, page) {// {{{
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
const cursor = this.db
|
const cursor = this.db
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
.transaction(['nodes', this.storeName], 'readonly')
|
||||||
|
|
@ -497,10 +514,10 @@ class NodeHistoryStore extends SimpleNodeStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uuidv7() {
|
export function uuidv7() {// {{{
|
||||||
// random bytes
|
// random bytes
|
||||||
const value = new Uint8Array(16)
|
const value = new Uint8Array(16)
|
||||||
crypto.getRandomValues(value)
|
crypto.getRandomValues(value)
|
||||||
|
|
@ -524,6 +541,6 @@ export function uuidv7() {
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
|
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
|
||||||
}
|
}// }}}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
|
import { Node } from './page_node.mjs'
|
||||||
|
|
||||||
export class N2PageHistory extends CustomHTMLElement {
|
export class N2PageHistory extends CustomHTMLElement {
|
||||||
static PAGESIZE = 25
|
static PAGESIZE = 15
|
||||||
|
|
||||||
static {
|
static {
|
||||||
this.tmpl = document.createElement('template')
|
this.tmpl = document.createElement('template')
|
||||||
|
|
@ -9,22 +10,30 @@ export class N2PageHistory extends CustomHTMLElement {
|
||||||
<style>
|
<style>
|
||||||
n2-pagehistory {
|
n2-pagehistory {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
|
|
||||||
.layout {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="back">
|
||||||
<img data-el="back-image" src="/images/${_VERSION}/icon_back.svg" class="colorize">
|
<img data-el="back-image" src="/images/${_VERSION}/icon_back.svg" class="colorize">
|
||||||
<div data-el="back-text">Back to node</div>
|
<div data-el="back-text">Back to node</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-name">
|
||||||
<img src="/images/${_VERSION}/icon_history.svg" class="colorize">
|
<img src="/images/${_VERSION}/icon_history.svg" class="colorize">
|
||||||
<h1 data-el="node-name"></h1>
|
<h1 data-el="node-name"></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-label">Actions</div>
|
||||||
|
<div class="group">
|
||||||
|
<button data-el="download-history">Fetch all history from server</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="group-label">History</div>
|
||||||
|
<div class="group">
|
||||||
<div data-el="nodes"></div>
|
<div data-el="nodes"></div>
|
||||||
|
|
||||||
<div class="pagination">
|
<div data-el="pagination">
|
||||||
<div data-el="prev"><</div>
|
<div data-el="prev"><</div>
|
||||||
<div data-el="page"></div>
|
<div data-el="page"></div>
|
||||||
<div data-el="next">></div>
|
<div data-el="next">></div>
|
||||||
|
|
@ -44,6 +53,7 @@ export class N2PageHistory extends CustomHTMLElement {
|
||||||
this.elBackText.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'node' }))
|
this.elBackText.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'node' }))
|
||||||
this.elPrev.addEventListener('click', () => this.prevPage())
|
this.elPrev.addEventListener('click', () => this.prevPage())
|
||||||
this.elNext.addEventListener('click', () => this.nextPage())
|
this.elNext.addEventListener('click', () => this.nextPage())
|
||||||
|
this.elDownloadHistory.addEventListener('click', () => this.downloadHistory())
|
||||||
|
|
||||||
_mbus.subscribe('NODE_UI_OPEN', async (event) => {
|
_mbus.subscribe('NODE_UI_OPEN', async (event) => {
|
||||||
await this.useNode(event.detail.data)
|
await this.useNode(event.detail.data)
|
||||||
|
|
@ -104,6 +114,11 @@ export class N2PageHistory extends CustomHTMLElement {
|
||||||
this.elNodeName.innerText = this.node.get('Name')
|
this.elNodeName.innerText = this.node.get('Name')
|
||||||
this.elPage.innerText = `${this.page} / ${this.pages}`
|
this.elPage.innerText = `${this.page} / ${this.pages}`
|
||||||
|
|
||||||
|
if (this.nodesTotal <= N2PageHistory.PAGESIZE)
|
||||||
|
this.elPagination.style.display = 'none'
|
||||||
|
else
|
||||||
|
this.elPagination.style.display = ''
|
||||||
|
|
||||||
let nodes = await nodeStore.nodesHistory.retrievePage(this.node.UUID, N2PageHistory.PAGESIZE, this.page)
|
let nodes = await nodeStore.nodesHistory.retrievePage(this.node.UUID, N2PageHistory.PAGESIZE, this.page)
|
||||||
let i = 0
|
let i = 0
|
||||||
let divs = nodes.map(n => {
|
let divs = nodes.map(n => {
|
||||||
|
|
@ -124,5 +139,50 @@ export class N2PageHistory extends CustomHTMLElement {
|
||||||
})
|
})
|
||||||
this.elNodes.replaceChildren(...divs)
|
this.elNodes.replaceChildren(...divs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadHistory() {
|
||||||
|
try {
|
||||||
|
const nodes = []
|
||||||
|
let offset = 0
|
||||||
|
let hasMore = true
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const history = await this.downloadHistoryPage(offset)
|
||||||
|
hasMore = history.HasMore
|
||||||
|
for (const nodeData of history.Nodes) {
|
||||||
|
nodes.push(new Node(nodeData))
|
||||||
|
}
|
||||||
|
offset = nodes.length
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = 0
|
||||||
|
for (const node of nodes) {
|
||||||
|
const ok = await nodeStore.nodesHistory.hasNode(node.UUID, node.get('Updated'))
|
||||||
|
if (ok) num++
|
||||||
|
await nodeStore.nodesHistory.add(node)
|
||||||
|
}
|
||||||
|
console.log(num)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadHistoryPage(offset) {
|
||||||
|
const res = await fetch(`/node/history/retrieve/${this.node.UUID}/${offset}`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": 'Bearer ' + localStorage.getItem('token'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (!json.OK) {
|
||||||
|
alert(json.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
}
|
}
|
||||||
customElements.define('n2-pagehistory', N2PageHistory)
|
customElements.define('n2-pagehistory', N2PageHistory)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue