import m, { ClosureComponent } from "mithril"; import { parse, stringify, map } from "../lib/common/expression/parser.ts"; import memoize from "../lib/common/memoize.ts"; import Autocomplete from "./autocomplete-compnent.ts"; import * as smartQuery from "./smart-query.ts"; import { validQuery } from "../lib/db/synth.ts"; import { Expression } from "../lib/types.ts"; const getAutocomplete = memoize((resource) => { const labels = smartQuery.getLabels(resource); const autocomplete = new Autocomplete((txt, cb) => { txt = txt.toLowerCase(); cb( labels .filter((s) => s.toLowerCase().includes(txt)) .map((s) => ({ value: `${s}: `, tip: smartQuery.getTip(resource, s), })), ); }); return autocomplete; }); function parseFilter(resource, f): Expression { let exp; if (/^[\s0-9a-zA-Z]+:/.test(f)) { const k = f.split(":", 1)[0]; const v = f.slice(k.length + 1).trim(); exp = ["FUNC", "Q", k.trim(), v]; } else { exp = parse(f); } const unpacked = map(exp, (e) => { if (Array.isArray(e) && e[0] === "FUNC") { if (e[1] === "Q") return smartQuery.unpack(resource, e[2], e[3]); else if (e[1] === "NOW") return Date.now(); } return e; }); // Throws exception if invalid Mongo query validQuery(unpacked, resource); return exp; } function stringifyFilter(f: Expression): string { if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q") return `${f[2]}: ${f[3]}`; return stringify(f); } function splitFilter(filter: string): string[] { if (!filter) return [""]; const list: string[] = []; const f = parse(filter); if (Array.isArray(f) && f[0] === "AND") for (const ff of f.slice(1)) list.push(stringifyFilter(ff)); else list.push(stringifyFilter(f)); list.push(""); return list; } interface Attrs { resource: string; filter: string; onChange: (filter: string) => void; } const component: ClosureComponent = (initialVnode) => { let filterList = splitFilter(initialVnode.attrs.filter); let filterInvalid = 0; let filterTouched = false; let attrs: Attrs = initialVnode.attrs; function onChange(): void { filterTouched = false; filterInvalid = 0; filterList = filterList.filter((f) => f); const list = filterList.map((f, idx) => { try { return parseFilter(attrs.resource, f); } catch (err) { filterInvalid |= 1 << idx; } return null; }); filterList.push(""); if (filterInvalid) { m.redraw(); return; } if (list.length === 0) attrs.onChange(""); else if (list.length > 1) attrs.onChange(stringify(["AND", ...list])); else attrs.onChange(stringify(list[0])); } return { onupdate: (vnode) => { getAutocomplete(vnode.attrs.resource).reposition(); }, view: (vnode) => { if (attrs.filter !== vnode.attrs.filter) { filterInvalid = 0; filterList = splitFilter(vnode.attrs.filter); } attrs = vnode.attrs; return m("div.mb-5", [ m("label.text-sm font-semibold text-stone-700", "Filter"), m( "div.shadow-sm rounded-md mt-1 max-w-screen-sm -space-y-px", ...filterList.map((fltr, idx) => { let classNames = "appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm"; if (idx === 0) classNames += " rounded-t-md"; if (idx === filterList.length - 1) classNames += " rounded-b-md"; if (filterInvalid & (1 << idx)) classNames += " text-red-700"; return m(`input`, { type: "text", class: classNames, value: fltr, oninput: (e) => { e.redraw = false; filterList[idx] = e.target.value; filterTouched = true; }, oncreate: (vn) => { const el = vn.dom as HTMLInputElement; getAutocomplete(vnode.attrs.resource).attach(el); el.addEventListener("blur", () => { if (filterTouched) onChange(); }); el.addEventListener("keydown", (e) => { if (e.key === "Enter" && filterTouched) onChange(); }); }, }); }), ), ]); }, }; }; export default component;