512 lines
22 KiB
TypeScript
512 lines
22 KiB
TypeScript
/* TODO:
|
|
- history
|
|
- find example and correctly handle "merge" messages
|
|
- filter table by message type or path
|
|
- loading icons
|
|
- expand referenced object in place or accordian
|
|
- paste bytes -> analyze
|
|
- edit fields / files?
|
|
- different menu for message / enum / extend / literal
|
|
- display integers in base16 and base10
|
|
- search bytes?
|
|
- highlight results in inspector
|
|
*/
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import type { MouseEvent as ReactMouseEvent, ChangeEventHandler, ChangeEvent } from 'react';
|
|
import './App.css';
|
|
import { ObjectInspector, ObjectLabel, ObjectName, ObjectValue } from 'react-inspector';
|
|
import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels';
|
|
import { process, read, ParsedFile, TableItem } from './iwa';
|
|
import { parse_protos, ProtoMap } from './messages';
|
|
import type { $_TSP_MessageInfo, $_TSP_Reference } from './messages';
|
|
import { Menu, Item, Separator, useContextMenu } from 'react-contexify';
|
|
import 'react-contexify/dist/ReactContexify.css';
|
|
import { ToastContainer, toast } from 'react-toastify';
|
|
import 'react-toastify/dist/ReactToastify.css';
|
|
|
|
const uuid2str = (l: bigint, u: bigint): string => [
|
|
((l >> 0n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((l >> 8n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((l >> 16n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((l >> 24n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
"-",
|
|
((l >> 32n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((l >> 40n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
"-",
|
|
((l >> 48n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((l >> 56n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
"-",
|
|
((u >> 0n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((u >> 8n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
"-",
|
|
((u >> 16n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((u >> 24n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((u >> 32n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((u >> 40n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((u >> 48n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
((u >> 56n) & 0xFFn).toString(16).padStart(2, "0"),
|
|
].join("").toUpperCase();
|
|
|
|
//#region Xxd
|
|
|
|
import { vsprintf } from 'printj';
|
|
const X = "%02hhx", Y = X + X + " ";
|
|
const FMT = [...Array.from({length:16}).map((_,i) =>
|
|
Y.repeat(i>>1) + (i%2 ? X:" ") + " " + " ".repeat(7 - (i >> 1)) + "|" + "%c".repeat(i) + " ".repeat(16-i) + "|\n"
|
|
), Y.repeat(8) + "|" + "%c".repeat(16) + "|\n"];
|
|
|
|
const xxd = (u8: Uint8Array): string => {
|
|
const out: string[] = [];
|
|
for(let i = 0; i < u8.length; i+=16) {
|
|
const d = [...u8.slice(i, i+16)];
|
|
out.push(vsprintf(`%04x: ${FMT[d.length]}`, [i, ...d, ...d.map(x => String.fromCharCode(x).replace(/[^\x20-\x7E]/g,"."))]))
|
|
}
|
|
return out.join("");
|
|
}
|
|
|
|
/*type XxdProps = {
|
|
data?: Uint8Array;
|
|
};
|
|
|
|
function Xxd({data}: XxdProps) {
|
|
return ( <pre>{data && xxd(data)}</pre> );
|
|
}*/
|
|
//<div className="proto"><Xxd data={id && file.space[+id]?.[0]?.data || void 0} /></div>
|
|
|
|
//#endregion
|
|
|
|
//#region TableView
|
|
|
|
type TableViewProps = {
|
|
id?: string;
|
|
data: any[];
|
|
cols: string[];
|
|
sort?: string;
|
|
desc?: boolean;
|
|
filter?: (row: any, R: number) => boolean;
|
|
rowclick?: (row: any, R: number, e: ReactMouseEvent<HTMLTableRowElement, MouseEvent>) => void;
|
|
cellclick?: (value: any, R: number, C: number, e: ReactMouseEvent<HTMLTableCellElement, MouseEvent>) => void;
|
|
onsort?: (sort: string) => void;
|
|
};
|
|
|
|
function TableView({id, data, cols, filter, rowclick, cellclick, sort, desc, onsort}: TableViewProps) {
|
|
return (
|
|
<table>
|
|
<thead>
|
|
<tr>{cols.map((c,idx) => (
|
|
<th key={idx} onClick={()=>{ if(onsort) onsort(c); }}>{c} {sort == c ? (desc ? "\u25BC" : "\u25B2") : ""}</th>
|
|
))}</tr>
|
|
</thead>
|
|
<tbody>{data.filter(filter || (()=>true)).map((row, R) => (
|
|
<tr id={`tr-${R}`} key={R}
|
|
{...(rowclick ? {onClick: (e) => {e.preventDefault(); e.stopPropagation(); rowclick(row, R, e)}} : {})}
|
|
{...(row["id"] == id ? {style: {backgroundColor: "#646cff", color: "#FFFFFF" }} : {})}
|
|
>{/* TODO: forward-ref? */}
|
|
{cols.map((c,C) => (<td key={`${R}-${C}`}
|
|
{...(typeof row[c] == "string" ? {className: "left"} : {})}
|
|
{...(typeof row[c] == "number" ? {className: "right"} : {})}
|
|
{...(cellclick ? {onClick: (e) => {e.preventDefault(); e.stopPropagation(); cellclick(row, R, C, e)}} : {})}
|
|
>{row[c]??""}</td>))}
|
|
</tr>
|
|
))}</tbody>
|
|
</table>
|
|
);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region ContextMenu
|
|
|
|
interface ContextMenuProps {
|
|
ID: string;
|
|
menuType: string;
|
|
menuField: string;
|
|
menuId: string;
|
|
onClickId?: ({props}: any)=>void;
|
|
onClickCopyByteArray?: ({props}: any)=>void;
|
|
onClickCopyJSON: ({props}: any)=>void;
|
|
showProtoDef: ({props}: any)=>void;
|
|
showXXD: ({props}: any) => void;
|
|
}
|
|
const ContextMenu = ({ID, menuType, menuField, menuId, onClickId, onClickCopyByteArray, onClickCopyJSON, showProtoDef, showXXD}: ContextMenuProps) => (
|
|
<Menu id={ID}>
|
|
{menuField && (<Item disabled><b>{menuField}</b></Item>)}
|
|
<Item disabled><b>{menuType}</b></Item>
|
|
<Item hidden={()=>menuType != ".TSP.Reference"} onClick={onClickId}>Go to {menuId}</Item>
|
|
<Separator />
|
|
<Item onClick={onClickCopyByteArray}>Copy byte array</Item>
|
|
<Item onClick={onClickCopyJSON}>Copy JSON</Item>
|
|
<Item hidden={()=>menuType == "bytes"} onClick={showProtoDef}>Show Definition</Item>
|
|
<Item hidden={()=>menuType != "bytes"} onClick={showXXD}>Dump XXD to console</Item>
|
|
</Menu> );
|
|
|
|
//#endregion
|
|
|
|
//#region preparse
|
|
|
|
const replacer = (_: string, v: any): any => {
|
|
switch(true) {
|
|
case (typeof v == "bigint"): return v.toString();
|
|
case (v instanceof Uint8Array): return [...v];
|
|
case (v != null && typeof v.lower == "bigint" && typeof v.upper == "bigint"): return uuid2str(v.lower, v.upper);
|
|
}
|
|
return v;
|
|
};
|
|
|
|
/* parse if message has not been parsed */
|
|
const preparse = (id: any, message: string, f: ParsedFile, p: ProtoMap) => {
|
|
const item = f.space[+id][0];
|
|
if(!item.parsed) {
|
|
item.parsed = process(item.data, message, p);
|
|
item.pre = JSON.stringify(item.parsed, replacer);
|
|
}
|
|
if(!item.parsedmeta) {
|
|
const m: $_TSP_MessageInfo = item.parsedmeta = process(item.rawmeta, ".TSP.MessageInfo", p);
|
|
/* .TSP.MessageInfo */
|
|
if(m.object_references) m.$object_references = m.object_references.map((n: bigint) => {
|
|
/* create a fake reference for the inspector */
|
|
const o: $_TSP_Reference = ({ identifier: n });
|
|
Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
|
|
return o;
|
|
});
|
|
if(m.field_infos) m.field_infos.forEach((fi) => {
|
|
/* .TSP.FieldInfo */
|
|
if(fi.object_references) fi.$object_references = fi.object_references.map((n: bigint) => {
|
|
/* create a fake reference for the inspector */
|
|
const o: $_TSP_Reference = ({ identifier: n });
|
|
Object.defineProperty(o, "PB_TYPE", {value: ".TSP.Reference", enumerable: false});
|
|
return o;
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
|
|
function App() {
|
|
/* selected message ID */
|
|
const [id, setId] = useState<string>("0");
|
|
/* parsed file */
|
|
const [file, setFile] = useState<ParsedFile>({ space: {}, tbl: [], type: "N" });
|
|
/* current object */
|
|
const [obj, setObj] = useState<any>({});
|
|
/* current meta */
|
|
const [meta, setMeta] = useState<$_TSP_MessageInfo>({} as any);
|
|
/* current deps */
|
|
const [deps, setDeps] = useState<any[]>([]);
|
|
/* protobuf definitions */
|
|
const [protos, setProtos] = useState<ProtoMap>({});
|
|
/* selected message type */
|
|
const [sel, setSel] = useState<string>("");
|
|
/* current protobuf definition */
|
|
const [__html, setProto] = useState<string>("Select a Row");
|
|
/* "dirty" if inspecting a subfield */
|
|
const [dirty, setDirty] = useState<boolean>(false);
|
|
/* loading */
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
/* search */
|
|
const [search, setSearch] = useState<string>("");
|
|
/* level */
|
|
const [level, setLevel] = useState<number>(1);
|
|
/* history stack */
|
|
const [stack, setStack] = useState<string[]>([]);
|
|
/* react-contexify */
|
|
const MENU_ID = "insp-menu";
|
|
const { show } = useContextMenu({ id: MENU_ID });
|
|
const [menuField, setMenuField] = useState<string>("");
|
|
const [menuType, setMenuType] = useState<string>("");
|
|
const [menuId, setMenuId] = useState<string>("");
|
|
const tblRef = useRef<HTMLDivElement>(null);
|
|
/* sorting */
|
|
const [ sort, setSort ] = useState<string>("");
|
|
const [ desc, setDesc ] = useState<boolean>(false);
|
|
|
|
/* scroll to selected row */
|
|
const tblScroll = (R: number) => {
|
|
if(R == -1) return;
|
|
const rowelt = document.getElementById(`tr-${R}`);
|
|
const top = rowelt?.offsetTop || 0;
|
|
if(tblRef.current) {
|
|
const tbl = tblRef.current;
|
|
if(top > tbl.scrollTop + tbl.clientHeight - (rowelt?.clientHeight||0) || top < tbl.scrollTop + (rowelt?.clientHeight||0)) tbl.scrollTop = Math.max(0, top - tbl.clientHeight/2 - (rowelt?.clientHeight||0)/2);
|
|
}
|
|
};
|
|
|
|
/* Sorting machinations */
|
|
const onsort = (s: string) => {
|
|
let d = false;
|
|
console.log(sort == s, desc);
|
|
if(sort == s) setDesc(d = !desc);
|
|
else {
|
|
setDesc(d = false);
|
|
setSort(s);
|
|
}
|
|
file.tbl.sort((x: any, y: any) => (typeof x[s] == "number" ? x[s] - y[s] : String(x[s]).localeCompare(String(y[s]))) * (d ? -1 : 1));
|
|
};
|
|
useEffect(() => {
|
|
if(id) tblScroll(file.tbl.findIndex(row => row["id"] == +id));
|
|
}, [sort, desc]);
|
|
|
|
/* update selection based on table row */
|
|
const doitRow = (row: TableItem, R: number, reset?: boolean) => {
|
|
let obj: any, meta: $_TSP_MessageInfo;
|
|
try {
|
|
preparse(row.id, row.message, file, protos);
|
|
const item = file.space[+row.id][0];
|
|
obj = item.parsed;
|
|
meta = item.parsedmeta as $_TSP_MessageInfo;
|
|
} catch(e) {
|
|
console.error(row, e); toast.error(`Could not parse ${row.id} (${row.type})`, {position: toast.POSITION.TOP_CENTER}); return;
|
|
}
|
|
if(reset == true) setStack([]); else if(typeof reset != "undefined") setStack([...stack, id]);
|
|
setSel(row.message);
|
|
setProto(protos[row.message] || "");
|
|
setDirty(false);
|
|
setObj(obj);
|
|
setMeta(meta);
|
|
setDeps(file.tbl.filter(r => ((file.space[+r.id][0].parsedmeta?.object_references||[])?.indexOf(BigInt(row.id)) > -1)).map(v => {
|
|
/* this copy uses non-enumerable fields */
|
|
const o = {}; Object.entries(v).forEach(([k,v])=> Object.defineProperty(o, k, { enumerable: false, value: v })); return o;
|
|
}));
|
|
setId(String(row.id));
|
|
tblScroll(R);
|
|
};
|
|
/* click event handler for the messages table */
|
|
const rowclick = (row: any, R: number) => { doitRow(row, R, true); };
|
|
/* helper for .TSP.Reference */
|
|
const gotoRef = (id: string) => {
|
|
const R = file.tbl.findIndex(t => +t.id == +id);
|
|
if (R == -1) throw new Error(`Message ${id} not found`);
|
|
doitRow(file.tbl[R], R, false);
|
|
};
|
|
/* helper for selecting sub-proto */
|
|
const selectProto = (type: string) => { setProto(protos[type] || ""); setDirty(type != sel); };
|
|
/* go back to the previous message */
|
|
const pop = () => {
|
|
const oldId = stack.pop()||"0";
|
|
setStack([...stack]);
|
|
const R = file.tbl.findIndex(t => +t.id == +oldId);
|
|
if (R == -1) throw new Error(`Message ${oldId} not found`);
|
|
doitRow(file.tbl[R], R);
|
|
};
|
|
|
|
/* filter message table */
|
|
const filter = (row: any) => {
|
|
if(!search) return true;
|
|
preparse(row.id, row.message, file, protos);
|
|
return (file.space[+row.id][0].pre||"").toLowerCase().indexOf(search.toLowerCase()) > -1;
|
|
//return row.message == ".TN.SheetArchive";
|
|
}
|
|
|
|
/* load file */
|
|
const process_ab = (ab: ArrayBuffer, p: ProtoMap = protos) => {
|
|
const f = read(ab);
|
|
f.tbl.forEach(row => preparse(row.id, row.message, f, p));
|
|
setFile(f);
|
|
};
|
|
|
|
/* on page load, get protobuf definitions and process the test file */
|
|
useEffect(() => {
|
|
const id = toast.loading(`Processing test.numbers`);
|
|
let ignore = false;
|
|
(async () => {
|
|
await new Promise((res) => setTimeout(res, 100)); // "sleep" to give toast a chance to shine
|
|
const protos = await (await fetch("protos")).text();
|
|
const testfile = await (await fetch("test.numbers")).arrayBuffer();
|
|
if(ignore) return;
|
|
const p = parse_protos(protos);
|
|
setProtos(p);
|
|
process_ab(testfile, p);
|
|
toast.dismiss(id);
|
|
setLoading(false);
|
|
})();
|
|
return () => { ignore = true; toast.dismiss(id); }
|
|
}, []);
|
|
useEffect(() => { if(file.tbl[0]) doitRow(file.tbl[0], 0, true); }, [file]);
|
|
|
|
const onChange: ChangeEventHandler<HTMLInputElement> = async (e: ChangeEvent<HTMLInputElement>) => {
|
|
setLoading(true);
|
|
if(!e.target.files) { toast.error("must select a file!"); throw new Error("No file selected!"); setLoading(false); }
|
|
try {
|
|
const id = toast.loading(`Processing ${e.target.files[0].name}`);
|
|
await new Promise((res) => setTimeout(res, 100)); // "sleep" to give toast a chance to shine
|
|
process_ab(await e.target.files?.[0].arrayBuffer());
|
|
toast.dismiss(id);
|
|
} catch(e) {
|
|
if((e as any).message?.includes("Failed to read archive")) toast.error("Please select an iWork file", { position: toast.POSITION.TOP_CENTER });
|
|
else toast.error(e && (e as any).message || e, { position: toast.POSITION.TOP_CENTER });
|
|
console.error(e);
|
|
} finally {
|
|
e.target.value = "";
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
/* Menu machinations */
|
|
interface MenuProps {
|
|
type: string;
|
|
field?: string;
|
|
id: string;
|
|
data: any;
|
|
}
|
|
function displayMenu(event: ReactMouseEvent, props?: MenuProps) {
|
|
setMenuField(props?.field||"");
|
|
setMenuType(props?.type||"");
|
|
if(props?.id) setMenuId(String(props.id));
|
|
show({ event, props });
|
|
}
|
|
function onClickId(){ gotoRef(menuId); }
|
|
function onClickCopyByteArray({ props }: {props: MenuProps}){
|
|
const _data = props?.data?.PB_RAW?.data || (+props.id == +id) && file.space[+id][0].data;
|
|
if(!_data) throw new Error("Could not find raw data");
|
|
navigator.clipboard.writeText("[" + [..._data].map(x => "0x" + x.toString(16).toUpperCase().padStart(2,"0")).join(", ") + "]");
|
|
}
|
|
function onClickCopyJSON({ props }: {props: MenuProps}){
|
|
if(!props?.data) throw new Error("Could not find raw data");
|
|
navigator.clipboard.writeText(JSON.stringify(props.data, (_,v) => typeof v == "bigint" ? v.toString() : v instanceof Uint8Array ? [...v]: v));
|
|
}
|
|
function showProtoDef({props}: {props: MenuProps}) { selectProto(props.type); }
|
|
function showXXD({props}: {props: MenuProps}) { console.log(xxd(props.data)); }
|
|
|
|
/* Inspector machinations */
|
|
type NodeRendererProps = {
|
|
depth: number;
|
|
name: string;
|
|
data: any;
|
|
isNonenumerable: boolean;
|
|
expanded: boolean;
|
|
}
|
|
const nodeRenderer = ({ depth, name, data, isNonenumerable }: NodeRendererProps) => {
|
|
if(depth === 0) return ( <b>Message {id} <a onClick={()=>{selectProto(sel);}} onContextMenu={e => {displayMenu(e, { type: sel, id, data })}}><b>[{sel}]</b></a></b> );
|
|
if(typeof data == "bigint" && name.includes("identifier")) return ( <>
|
|
<ObjectName name={name} />: <ObjectValue object={data} /> -> <a onClick={() => {gotoRef(String(data))}}><b>{String(data)}</b></a>
|
|
</> );
|
|
if(data.PB_TYPE) {
|
|
const frag = ( <a onClick={() => {selectProto(data.PB_TYPE);}}>
|
|
{data.PB_ENUM && <ObjectValue object={data.value} />}
|
|
<b>{data.PB_ENUM && <> = {data.PB_ENUM}</>} [{data.PB_TYPE}]</b>
|
|
</a> );
|
|
if(data.PB_TYPE == ".TSP.Reference") {
|
|
const id = String(data?.identifier);
|
|
return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
|
|
<ObjectName name={name} />: <b>{frag} -> <a onClick={() => {gotoRef(id)}}><b>{id}</b></a></b>
|
|
</span> );
|
|
}
|
|
if(data.PB_TYPE == ".TSP.UUID") {
|
|
const uuid = uuid2str(data.lower, data.upper);
|
|
return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
|
|
<ObjectName name={name} />: <b>{frag} -> {uuid}</b>
|
|
</span> );
|
|
}
|
|
return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
|
|
<ObjectName name={name} />: {frag}
|
|
</span> );
|
|
}
|
|
if(data instanceof Uint8Array) return (
|
|
<span onContextMenu={(e) => {displayMenu(e, { type: "bytes", id, data, field: (data as any).PB_FIELD })}}>
|
|
<ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} />
|
|
</span>
|
|
);
|
|
// Uncomment to show hex representation of unsigned 32-bit ints
|
|
//if(typeof data == "number" && (data>>>0) == data) return ( <>
|
|
// <ObjectName name={name} />: <ObjectValue object={data} /> 0x{data.toString(16)}
|
|
//</>);
|
|
return ( <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} /> );
|
|
};
|
|
const metaRenderer = ({ depth, name, data, isNonenumerable }: NodeRendererProps) => {
|
|
if(depth === 0) return ( <b>Metadata</b> );
|
|
if(typeof data == "bigint" && name.includes("identifier")) return ( <>
|
|
<ObjectName name={name} />: <ObjectValue object={data} /> -> <a onClick={() => {gotoRef(String(data))}}><b>{String(data)}</b></a>
|
|
</> );
|
|
if(data.PB_TYPE) {
|
|
const frag = ( <a onClick={() => {selectProto(data.PB_TYPE);}}>
|
|
{data.PB_ENUM && <ObjectValue object={data.value} />}
|
|
<b>{data.PB_ENUM && <> = {data.PB_ENUM}</>} [{data.PB_TYPE}]</b>
|
|
</a> );
|
|
if(data.PB_TYPE == ".TSP.Reference") {
|
|
const id = String(data?.identifier);
|
|
return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
|
|
<ObjectName name={name} />: <b>{frag} -> <a onClick={() => {gotoRef(id)}}><b>{id}</b></a></b>
|
|
</span> );
|
|
}
|
|
return ( <span onContextMenu={(e) => {displayMenu(e, { type: data.PB_TYPE, id, data, field: data.PB_FIELD })}}>
|
|
<ObjectName name={name} />: {frag}
|
|
</span> );
|
|
}
|
|
return ( <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} /> );
|
|
};
|
|
const depsRenderer = ({ depth, data, isNonenumerable }: NodeRendererProps) => {
|
|
if(depth === 0) return ( <b>Dependents</b> );
|
|
return ( <a onClick={()=>gotoRef(data.id)}><ObjectLabel name={data.id} data={data.message} isNonenumerable={isNonenumerable} /></a> );
|
|
};
|
|
|
|
const showHelp = () => { toast.info(<a href="https://git.sheetjs.com/sheetjs/iwa-inspector" target="_blank">Click for more info</a>); }
|
|
|
|
return ( <>
|
|
{/* header */}
|
|
<div id="header" className="header" style={{display: "grid", gridTemplateColumns: "repeat(3, 1fr)", alignItems:"center"}}>
|
|
<div style={{display: "flex", justifyContent: "flex-start"}}><input type="file" id="file" onChange={onChange} disabled={loading} accept=".numbers,.key,.pages"/></div>
|
|
<div><b><a href="https://sheetjs.com">SheetJS</a> IWA Inspector</b> <b onClick={showHelp}>(?)</b></div>
|
|
<div style={{display: "flex", justifyContent: "flex-end"}}><input type="text" value={search} placeholder="search for text" onChange={(e)=>setSearch(e.target.value)}/></div>
|
|
</div>
|
|
|
|
<div className="page">
|
|
<PanelGroup direction='vertical'>
|
|
<Panel defaultSize={25}><div className="overflow" ref={tblRef}>
|
|
|
|
{/* message table */}
|
|
<TableView data={file.tbl} cols={["id", "type", "message", "path"]} filter={filter} id={id} rowclick={rowclick} sort={sort} desc={desc} onsort={onsort}/>
|
|
|
|
</div></Panel>
|
|
<PanelResizeHandle style={{ height: "3px", backgroundColor: "#EEEEEE" }} />
|
|
<Panel><div className="overflow" style={{display: "grid", gridTemplateColumns: "1fr", gridTemplateRows: "max-content 1fr"}}>
|
|
{/* selected message bar */}
|
|
<div style={{boxShadow: "0 2px 2px -1px rgba(0, 0, 0, 0.4)", height: "24px"}}>{sel ? (<>
|
|
<b>Selected message {(stack.length > 4 ? [...stack.slice(0,2), "...", ...stack.slice(-1)] : stack).map(i => i + " > ").join("")} {id} ({sel}) {file.space[+id]?.[0]?.data?.length || 0} bytes {stack.length && <a onClick={pop}>Return to {stack[stack.length - 1]}</a> || ""}</b>
|
|
</>) : "Select a message to see the contents"}</div>
|
|
|
|
<div className='overflow'><PanelGroup direction='horizontal'>
|
|
<Panel><div className='overflow'>
|
|
|
|
{/* proto definition */}
|
|
<pre style={{textAlign: "left"}}>
|
|
{dirty && (<><a onClick={() => { selectProto(sel);}}>Return to {sel}</a><br /><br /></>)}
|
|
{__html.split("\n").map((r, idx) => ( <>{!r.match(/(optional|repeated|required) \./) ? (
|
|
<span key={idx}>{r}</span>
|
|
) : (
|
|
<a key={idx} onClick={() => {selectProto(r.trim().split(" ")[1]);}}>{r}</a>
|
|
)}<br/></> ))}
|
|
</pre>
|
|
|
|
</div></Panel>
|
|
<PanelResizeHandle style={{ width: "2px", backgroundColor: "#EEEEEE" }} />
|
|
<Panel><div className="overflow" style={{textAlign: "left", marginTop: "5px", paddingLeft: "5px"}}>
|
|
|
|
{/* inspector */}
|
|
<>
|
|
<b>Message</b> (showing {level} level{level > 1 ? "s" : ""})
|
|
{ level >= 5 ? void 0 : <span onClick={() => setLevel(level + 1)}> (+) </span> }
|
|
{ level <= 1 ? void 0 : <span onClick={() => setLevel(level - 1)}> (-) </span> }
|
|
</>
|
|
<ObjectInspector data={obj} expandLevel={level} nodeRenderer={nodeRenderer} key={level} />
|
|
<b>Meta</b>
|
|
<ObjectInspector data={meta} expandLevel={1} nodeRenderer={metaRenderer} />
|
|
{deps.length && <ObjectInspector data={deps} expandLevel={1} nodeRenderer={depsRenderer} /> || void 0}
|
|
<div style={{height:"13px"}}></div>
|
|
|
|
</div></Panel>
|
|
</PanelGroup></div>
|
|
</div></Panel>
|
|
</PanelGroup>
|
|
|
|
{/* Menu */}
|
|
<ContextMenu ID={MENU_ID} menuField={menuField} menuType={menuType} menuId={menuId} onClickId={onClickId} onClickCopyByteArray={onClickCopyByteArray} onClickCopyJSON={onClickCopyJSON} showProtoDef={showProtoDef} showXXD={showXXD} />
|
|
|
|
{/* Toast */}
|
|
<ToastContainer />
|
|
</div>
|
|
</> );
|
|
}
|
|
|
|
export default App;
|