WASM UI including export/import

main
Ziyang Hu 2 years ago
parent 064ded8d53
commit 1cf691b6b5

@ -10,6 +10,7 @@
"@testing-library/user-event": "^13.5.0",
"ansicolor": "^1.1.100",
"cozo-lib-wasm": "file:../pkg",
"file-saver": "^2.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",

@ -7,15 +7,28 @@
*/
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 React, {useEffect, useState} from "react";
import init, {CozoDb} from "cozo-lib-wasm";
import {parse} from "ansicolor";
import {saveAs} from 'file-saver';
function App() {
const [db, setDb] = useState(null);
const [params, setParams] = useState('{\n\n}');
const [params, setParams] = useState('{}');
const [showParams, setShowParams] = useState(false);
const [queryText, setQueryText] = useState('');
const [inProgress, setInProgress] = useState(false);
@ -83,7 +96,7 @@ function App() {
const t1 = performance.now();
const res = JSON.parse(res_str);
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) {
res.headers = [];
if (res.rows.length) {
@ -140,20 +153,26 @@ function App() {
</div>
<div/>
<div style={{paddingTop: 10, display: 'flex', flexDirection: 'row'}}>
<Button text={db ? "Run script" : "Loading WASM ..."}
onClick={() => handleQuery()}
disabled={!db || inProgress}
intent={Intent.PRIMARY}
<Button
icon="play"
text={db ? "Run script" : "Loading WASM ..."}
onClick={() => handleQuery()}
disabled={!db || inProgress}
intent={Intent.PRIMARY}
/>
&nbsp;
<Button onClick={() => {
setShowParams(!showParams)
}}>{showParams ? 'Hide' : 'Show'} params</Button>
<div style={{marginLeft: 10, marginTop: 5}}>
{statusMessage ? <Tag intent={errorMessage.length ? Intent.DANGER : Intent.SUCCESS} minimal>
{statusMessage}
</Tag> : null}
</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>
{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;

@ -14,9 +14,9 @@ import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
// <React.StrictMode>
<App/>
// </React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function

@ -4510,6 +4510,11 @@ file-loader@^6.2.0:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
filelist@^1.0.1:
version "1.0.4"
resolved "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"

Loading…
Cancel
Save