Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic transaction search UI #53

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/tx-ui1/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn install --production --frozen-lockfile
COPY . .
EXPOSE 8080
ENV NODE_ENV=production
CMD ["node", "server/server.js"]
33 changes: 33 additions & 0 deletions packages/tx-ui1/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"scripts": {
"start": "node server/server.js"
},
"dependencies": {},
"type": "module",
"devDependencies": {
"@agoric/eslint-config": "^0.3.3",
"@agoric/eslint-plugin": "^0.2.3",
"@endo/eslint-config": "^0.5.1",
"@types/better-sqlite3": "^7.6.1",
"@types/node": "^14.14.7",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "^7.24.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^32.3.0",
"eslint-plugin-prettier": "^3.3.1",
"fuse.js": "^6.6.2",
"typescript": "^4.8.4"
},
"eslintConfig": {
"extends": [
"@agoric"
]
},
"prettier": {
"trailingComma": "all",
"arrowParens": "avoid",
"singleQuote": true
}
}
38 changes: 38 additions & 0 deletions packages/tx-ui1/server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @ts-check

const pages = {
// package-relative
'/': { path: './ui/index.html', type: 'text/html' },
'/txUi.js': { path: './ui/txUi.js', type: 'application/javascript' },
'/report.css': { path: './ui/report.css', type: 'text/css' },
'/txs': { path: './data/split_detail.json', type: 'application/json' },
};

/**
* @param {object} io
* @param {typeof import('http')} io.http
* @param {typeof import('fs')} io.fs TODO: narrow to least authority
*/
const main = async ({ http, fs }) => {
const srv = http.createServer((request, response) => {
const { url } = request;
console.log('@@request for', url);
const page = pages[url];
if (!page) {
response.writeHead(404, { 'content-type': 'text/plain' });
response.end();
return;
}
fs.promises.readFile(page.path, 'utf8').then(body => {
response.writeHead(200, { 'content-type': page.type });
response.write(body);
response.end();
});
});
srv.listen(8080);
};

(async () =>
main({ http: await import('http'), fs: await import('fs') }))().catch(err =>
console.error(err),
);
23 changes: 23 additions & 0 deletions packages/tx-ui1/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<head>
<title>Transaction Search</title>
<link href="report.css" rel="stylesheet" />
</head>
<body>
<script type="module">
import {
html,
render,
} from 'https://unpkg.com/htm/preact/standalone.module.js';
import { main, TxRegister } from './txUi.js';

render(html`<${TxRegister} />`, document.body);

// console.log('@@script');
// main({ document, fetch, localStorage, html, Fuse });
</script>

<noscript
>Enable JavaScript to make use of this app; the app is not feasible without
it.</noscript
>
</body>
49 changes: 49 additions & 0 deletions packages/tx-ui1/ui/report.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.report {
border-collapse: collapse;
font-family: sans-serif;
}
th,
td {
border: 1px solid black;
padding: 4px;
}
.report tr:nth-child(odd) {
background-color: #fff;
}

.report tr:nth-child(even) {
background-color: #eee;
}

.report td.amount {
text-align: right;
}
.report td.amount:before {
content: '$';
}

.report td.status {
text-align: center;
}
.report td.status span {
visibility: hidden;
}
.report td.uncleared:before {
content: '?';
}
.report td.cleared:before {
content: '✓';
}
.report td.recurring:before {
content: '♲';
color: green;
}
.report td.recurring_suggested:before {
content: '♲?';
color: grey;
}

.report td.uncleared {
font-style: italic;
color: red;
}
132 changes: 132 additions & 0 deletions packages/tx-ui1/ui/txUi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// @ts-check
import {
html,
useEffect,
useState,
} from 'https://unpkg.com/htm/preact/standalone.module.js';
import Fuse from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.esm.js';

console.log('@@txUi module');

/**
* @param {object} io
* @param { typeof document } io.document
* @param { typeof fetch } io.fetch
* @param { typeof localStorage } io.localStorage
* @param { typeof import('fuse.js').default } io.Fuse
*/
export const main = ({ fetch, document, localStorage, Fuse }) => {
console.log('@@main');
document.addEventListener('DOMContentLoaded', ev => {
console.log('content ready@@');

fetch('/txs').then(resp => {
if (resp.status !== 200) {
throw resp;
}
resp.text().then(txt => {
console.log('got text@@', txt.slice(0, 20));
const lines = txt.split('\n').filter(line => line > '');
console.log(lines[0]);
});
});
});
};

const opts = {
keys: [
'date',
'num',
'description',
{ name: 'memo', getFn: tx => tx.splits.map(s => s.memo).join(' ') },
{ name: 'value', getFn: tx => tx.splits.map(s => `${s.value}`).join(' ') },
],
threshold: 0.3,
minMatchCharLength: 3,
};

export const TxRegister = () => {
const [items, setItems] = useState([]);
const [fuse, setFuse] = useState(new Fuse([], opts));
const [pattern, setPattern] = useState('');
const [matches, setMatches] = useState([]);

console.warn('AMBIENT: window.localStorage');
navigator.storage.estimate().then(est => console.log(est));
const { localStorage } = window;

useEffect(() => {
const found = localStorage.getItem('/txs');
if (!found) return;
const lines = found.split('\n').filter(line => line > '');
setItems(lines);
});

const load = async () => {
const resp = await fetch('/txs');
if (resp.status !== 200) {
throw resp;
}
const txt = await resp.text();
console.log('got text@@', txt.length, txt.slice(0, 20));
const lines = txt.split('\n').filter(line => line > '');
console.log(lines[0]);
const txs = lines.map(line => JSON.parse(line));
setItems(txs);
setFuse(new Fuse(txs, opts));
localStorage.setItem('/txs', txt);
};

const search = () => {
console.log('@@search', pattern);
const hits = fuse.search(pattern);
setMatches(hits);
};

return html`
<form onSubmit=${e => e.preventDefault()}>
<fieldset>
<legend>Transaction Search</legend>
<input name=${pattern} onInput=${e => setPattern(e.target.value)} />
<input type="submit" value="Search" onClick=${search} />
<br />
<button type="button" onClick=${load}>Load Transactions</button>
<div>Transactions loaded: ${items.length}</div>
</fieldset>
</form>
<table class="report">
<thead>
<th style="width: 8em">Date</th>
<th style="width: 2em">Num</th>
<th style="width: 10em">Description</th>
<th style="width: 2em">Code</th>
<th>Account</th>
<!-- <th>Status</th> -->
<th>Amount</th>
</thead>
<tbody>
${matches.map(match => {
const tx = match.item;
return html`
<tr>
<td id=${tx.tx_guid} title=${tx.tx_guid}>${tx.date}</td>
<td>${tx.num}</td>
<td>${tx.description}</td>
</tr>
${tx.splits.map(s => {
return html`
<tr>
<td colspan="2"></td>
<td>${s.memo}</td>
<td>${s.code}</td>
<td>${s.account}</td>
<td class="amount">${s.value}</td>
</tr>
`;
})}
`;
})}
</tbody>
</table>
`;
};