|
|
@ -7,15 +7,28 @@
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import './App.css';
|
|
|
|
import './App.css';
|
|
|
|
import {Button, Intent, Tag, TextArea} from "@blueprintjs/core";
|
|
|
|
import {
|
|
|
|
|
|
|
|
Button,
|
|
|
|
|
|
|
|
Checkbox,
|
|
|
|
|
|
|
|
Classes,
|
|
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
|
|
FileInput,
|
|
|
|
|
|
|
|
InputGroup,
|
|
|
|
|
|
|
|
Intent,
|
|
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
|
|
TextArea,
|
|
|
|
|
|
|
|
Toaster
|
|
|
|
|
|
|
|
} from "@blueprintjs/core";
|
|
|
|
import {Cell, Column, Table2} from "@blueprintjs/table";
|
|
|
|
import {Cell, Column, Table2} from "@blueprintjs/table";
|
|
|
|
import React, {useEffect, useState} from "react";
|
|
|
|
import React, {useEffect, useState} from "react";
|
|
|
|
import init, {CozoDb} from "cozo-lib-wasm";
|
|
|
|
import init, {CozoDb} from "cozo-lib-wasm";
|
|
|
|
import {parse} from "ansicolor";
|
|
|
|
import {parse} from "ansicolor";
|
|
|
|
|
|
|
|
import {saveAs} from 'file-saver';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
function App() {
|
|
|
|
const [db, setDb] = useState(null);
|
|
|
|
const [db, setDb] = useState(null);
|
|
|
|
const [params, setParams] = useState('{\n\n}');
|
|
|
|
const [params, setParams] = useState('{}');
|
|
|
|
const [showParams, setShowParams] = useState(false);
|
|
|
|
const [showParams, setShowParams] = useState(false);
|
|
|
|
const [queryText, setQueryText] = useState('');
|
|
|
|
const [queryText, setQueryText] = useState('');
|
|
|
|
const [inProgress, setInProgress] = useState(false);
|
|
|
|
const [inProgress, setInProgress] = useState(false);
|
|
|
@ -83,7 +96,7 @@ function App() {
|
|
|
|
const t1 = performance.now();
|
|
|
|
const t1 = performance.now();
|
|
|
|
const res = JSON.parse(res_str);
|
|
|
|
const res = JSON.parse(res_str);
|
|
|
|
if (res.ok) {
|
|
|
|
if (res.ok) {
|
|
|
|
setStatusMessage(`finished with ${res.rows.length} rows in ${(t1 - t0).toFixed(2)}ms`);
|
|
|
|
setStatusMessage(`finished with ${res.rows.length} rows in ${(t1 - t0).toFixed(1)}ms`);
|
|
|
|
if (!res.headers) {
|
|
|
|
if (!res.headers) {
|
|
|
|
res.headers = [];
|
|
|
|
res.headers = [];
|
|
|
|
if (res.rows.length) {
|
|
|
|
if (res.rows.length) {
|
|
|
@ -140,20 +153,26 @@ function App() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div/>
|
|
|
|
<div/>
|
|
|
|
<div style={{paddingTop: 10, display: 'flex', flexDirection: 'row'}}>
|
|
|
|
<div style={{paddingTop: 10, display: 'flex', flexDirection: 'row'}}>
|
|
|
|
<Button text={db ? "Run script" : "Loading WASM ..."}
|
|
|
|
<Button
|
|
|
|
onClick={() => handleQuery()}
|
|
|
|
icon="play"
|
|
|
|
disabled={!db || inProgress}
|
|
|
|
text={db ? "Run script" : "Loading WASM ..."}
|
|
|
|
intent={Intent.PRIMARY}
|
|
|
|
onClick={() => handleQuery()}
|
|
|
|
|
|
|
|
disabled={!db || inProgress}
|
|
|
|
|
|
|
|
intent={Intent.PRIMARY}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Button onClick={() => {
|
|
|
|
|
|
|
|
setShowParams(!showParams)
|
|
|
|
|
|
|
|
}}>{showParams ? 'Hide' : 'Show'} params</Button>
|
|
|
|
|
|
|
|
<div style={{marginLeft: 10, marginTop: 5}}>
|
|
|
|
<div style={{marginLeft: 10, marginTop: 5}}>
|
|
|
|
{statusMessage ? <Tag intent={errorMessage.length ? Intent.DANGER : Intent.SUCCESS} minimal>
|
|
|
|
{statusMessage ? <Tag intent={errorMessage.length ? Intent.DANGER : Intent.SUCCESS} minimal>
|
|
|
|
{statusMessage}
|
|
|
|
{statusMessage}
|
|
|
|
</Tag> : null}
|
|
|
|
</Tag> : null}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div style={{flex: 1}}/>
|
|
|
|
|
|
|
|
<Export db={db}/>
|
|
|
|
|
|
|
|
<ImportUrl db={db}/>
|
|
|
|
|
|
|
|
<ImportFile db={db}/>
|
|
|
|
|
|
|
|
<Button icon="properties" style={{marginLeft: 5}} onClick={() => {
|
|
|
|
|
|
|
|
setShowParams(!showParams)
|
|
|
|
|
|
|
|
}}>Params</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{errorMessage.length ? <pre id="error-message">
|
|
|
|
{errorMessage.length ? <pre id="error-message">
|
|
|
@ -223,4 +242,165 @@ function App() {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ImportUrl({db}) {
|
|
|
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
|
|
|
const [url, setUrl] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleClose() {
|
|
|
|
|
|
|
|
setOpen(false)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function handleImport() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
let resp = await fetch(url);
|
|
|
|
|
|
|
|
let content = await resp.text();
|
|
|
|
|
|
|
|
const res = JSON.parse(db.import_relations(content));
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
AppToaster.show({message: res.message, intent: Intent.DANGER})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
AppToaster.show({message: '' + e, intent: Intent.DANGER})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <>
|
|
|
|
|
|
|
|
<Button icon="import" style={{marginLeft: 5}} onClick={() => {
|
|
|
|
|
|
|
|
setUrl('');
|
|
|
|
|
|
|
|
setOpen(true)
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
URL
|
|
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
<Dialog isOpen={open} title="Import data from URL" onClose={handleClose}>
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_BODY}>
|
|
|
|
|
|
|
|
<InputGroup
|
|
|
|
|
|
|
|
fill
|
|
|
|
|
|
|
|
placeholder="Enter the file URL"
|
|
|
|
|
|
|
|
value={url}
|
|
|
|
|
|
|
|
onChange={e => setUrl(e.target.value)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_FOOTER}>
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
|
|
|
|
|
|
|
<Button onClick={handleClose}>Cancel</Button>
|
|
|
|
|
|
|
|
<Button intent={Intent.PRIMARY} disabled={!url} onClick={handleImport}>Import</Button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ImportFile({db}) {
|
|
|
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
|
|
|
const [file, setFile] = useState(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleClose() {
|
|
|
|
|
|
|
|
setOpen(false)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function handleImport() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
let content = await file.text();
|
|
|
|
|
|
|
|
const res = JSON.parse(db.import_relations(content));
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
AppToaster.show({message: res.message, intent: Intent.DANGER})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
AppToaster.show({message: '' + e, intent: Intent.DANGER})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <>
|
|
|
|
|
|
|
|
<Button icon="import" style={{marginLeft: 5}} onClick={() => setOpen(true)}>
|
|
|
|
|
|
|
|
File
|
|
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
<Dialog isOpen={open} title="Import data from local file" onClose={handleClose}>
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_BODY}>
|
|
|
|
|
|
|
|
<FileInput fill text={(file && file.name) || 'Choose file ...'} onInputChange={(e) => {
|
|
|
|
|
|
|
|
setFile(e.target.files[0]);
|
|
|
|
|
|
|
|
}}/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_FOOTER}>
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
|
|
|
|
|
|
|
<Button onClick={handleClose}>Cancel</Button>
|
|
|
|
|
|
|
|
<Button intent={Intent.PRIMARY} disabled={!file} onClick={handleImport}>Import</Button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function Export({db}) {
|
|
|
|
|
|
|
|
const [rels, setRels] = useState([]);
|
|
|
|
|
|
|
|
const [selected, setSelected] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggle() {
|
|
|
|
|
|
|
|
if (rels.length) {
|
|
|
|
|
|
|
|
setRels([])
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
const relations = JSON.parse(db.run('::relations', '')).rows;
|
|
|
|
|
|
|
|
if (!relations.length) {
|
|
|
|
|
|
|
|
AppToaster.show({message: 'No stored relations to export', intent: Intent.WARNING})
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setSelected([]);
|
|
|
|
|
|
|
|
setRels(relations)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleClose() {
|
|
|
|
|
|
|
|
setRels([])
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleExport() {
|
|
|
|
|
|
|
|
const res = JSON.parse(db.export_relations(JSON.stringify({relations: selected})));
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
|
|
const blob = new Blob([JSON.stringify(res.data)], {type: "text/plain;charset=utf-8"});
|
|
|
|
|
|
|
|
saveAs(blob, "export.json");
|
|
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
AppToaster.show({message: res.message, intent: Intent.DANGER})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <>
|
|
|
|
|
|
|
|
<Button icon="export" style={{marginLeft: 5}} onClick={toggle}>
|
|
|
|
|
|
|
|
Export
|
|
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
<Dialog isOpen={!!rels.length} onClose={handleClose} title="Export">
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_BODY}>
|
|
|
|
|
|
|
|
<p>Choose stored relations to export:</p>
|
|
|
|
|
|
|
|
{rels.map((row) => <Checkbox
|
|
|
|
|
|
|
|
key={row[0]} label={row[0]} checked={selected.includes(row[0])}
|
|
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
|
|
if (selected.includes(row[0])) {
|
|
|
|
|
|
|
|
setSelected(selected.filter(n => n !== row[0]))
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
setSelected([...selected, row[0]])
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}}/>)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_FOOTER}>
|
|
|
|
|
|
|
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
|
|
|
|
|
|
|
<Button onClick={handleClose}>Cancel</Button>
|
|
|
|
|
|
|
|
<Button intent={Intent.PRIMARY} disabled={!selected.length} onClick={handleExport}>Export</Button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const AppToaster = Toaster.create({position: 'top-right'});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default App;
|
|
|
|
export default App;
|
|
|
|