Skip to content

Commit 11afcd6

Browse files
committed
Refactor UI to both use menu bar
1 parent 5c9f61f commit 11afcd6

3 files changed

Lines changed: 122 additions & 77 deletions

File tree

src/iongraph.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ export interface IonJSON {
55
functions: Func[],
66
}
77

8-
export const emptyIonJSON: IonJSON = { version: currentVersion, functions: [] };
9-
108
export interface Func {
119
name: string,
1210
passes: Pass[],

www/main.tsx

Lines changed: 121 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,111 @@ import { ChangeEvent, useEffect, useState } from 'react';
22
import { createRoot } from 'react-dom/client';
33

44
import { GraphViewer } from '../src/GraphViewer.js';
5-
import { emptyIonJSON, migrate, type IonJSON, type MIRBlock, type SampleCounts } from '../src/iongraph.js';
5+
import { migrate, type IonJSON, type Func, type MIRBlock, type SampleCounts } from '../src/iongraph.js';
6+
import { assert } from '../src/utils.js';
67

78
export function renderWebUI(root: HTMLElement) {
89
const reactRoot = createRoot(root);
9-
reactRoot.render(<TestViewer />);
10+
reactRoot.render(<WebUI />);
1011
}
1112

12-
export function renderGraphOnly(root: HTMLElement, ionjson: {}) {
13+
export function renderStandaloneUI(root: HTMLElement, ionjson: {}) {
1314
const reactRoot = createRoot(root);
1415
const migrated = migrate(ionjson);
15-
reactRoot.render(<GraphViewer func={migrated.functions[0]} />);
16+
reactRoot.render(<StandaloneUI ionjson={migrated} />);
1617
}
1718

18-
function TestViewer() {
19-
const searchParams = new URL(window.location.toString()).searchParams;
19+
const searchParams = new URL(window.location.toString()).searchParams;
2020

21-
const [[ionjson, rawIonJSON], setIonJSON] = useState<readonly [IonJSON, string]>([emptyIonJSON, JSON.stringify(emptyIonJSON)]);
21+
const initialFuncIndex = searchParams.has("func") ? parseInt(searchParams.get("func")!, 10) : undefined;
22+
const initialPass = searchParams.has("pass") ? parseInt(searchParams.get("pass")!, 10) : undefined;
23+
24+
interface MenuBarProps {
25+
browse?: boolean,
26+
export?: boolean,
27+
ionjson?: IonJSON,
28+
29+
funcSelected: (func: Func | null) => void,
30+
}
31+
32+
function MenuBar(props: MenuBarProps) {
33+
const [[ionjson, rawIonJSON], setIonJSON] = useState<readonly [IonJSON | null, string]>(
34+
props.ionjson
35+
? [props.ionjson, JSON.stringify(props.ionjson)]
36+
: [null, ""]
37+
);
38+
const [funcIndex, setFuncIndex] = useState<number>(initialFuncIndex ?? 0);
39+
40+
// One-time initializer
41+
useEffect(() => {
42+
// Trigger funcSelected with any initial ion JSON
43+
if (ionjson) {
44+
props.funcSelected(ionjson.functions[funcIndex] ?? null);
45+
}
46+
}, []);
47+
48+
// Update ionjson if the prop changes.
49+
useEffect(() => {
50+
if (props.ionjson) {
51+
setIonJSON([props.ionjson, JSON.stringify(props.ionjson)]);
52+
props.funcSelected(props.ionjson.functions[funcIndex] ?? null);
53+
}
54+
}, [props.ionjson]);
55+
56+
// Notify when the func index changes.
57+
useEffect(() => {
58+
if (ionjson) {
59+
props.funcSelected(ionjson.functions[funcIndex] ?? null);
60+
}
61+
}, [funcIndex]);
62+
63+
async function fileSelected(e: ChangeEvent<HTMLInputElement>) {
64+
const input = e.target;
65+
if (!input.files?.length) {
66+
return;
67+
}
68+
69+
const file = input.files[0];
70+
const newJSON = JSON.parse(await file.text());
71+
const migrated = migrate(newJSON);
72+
setIonJSON([migrated, JSON.stringify(migrated)]);
73+
setFuncIndex(0);
74+
props.funcSelected(migrated.functions[0] ?? null);
75+
}
76+
77+
const numFunctions = ionjson?.functions.length ?? 0;
78+
const funcIndexValid = 0 <= funcIndex && funcIndex < numFunctions;
79+
80+
return <div className="ig-bb ig-pv2 ig-ph3 ig-flex ig-g2 ig-items-center ig-bg-white">
81+
{props.browse && <div>
82+
<input type="file" onChange={fileSelected} />
83+
</div>}
84+
{(ionjson?.functions.length ?? 0) > 1 && <div>
85+
Function <input
86+
type="number"
87+
value={funcIndex}
88+
onChange={e => {
89+
const newFuncIndex = Math.max(0, Math.min(numFunctions - 1, parseInt(e.target.value, 10)));
90+
setFuncIndex(isNaN(newFuncIndex) ? 0 : newFuncIndex);
91+
}}
92+
/>
93+
</div>}
94+
<div>{ionjson?.functions[funcIndex].name ?? ""}</div>
95+
<div className="ig-flex-grow-1"></div>
96+
{props.export && <div>
97+
<button
98+
disabled={!funcIndexValid}
99+
onClick={() => {
100+
exportStandalone(ionjson?.functions[funcIndex].name ?? "", rawIonJSON, { funcIndex: funcIndex });
101+
}}
102+
>Export</button>
103+
</div>}
104+
</div>;
105+
}
106+
107+
function WebUI() {
108+
const [initialIonJSON, setInitialIonJSON] = useState<IonJSON | undefined>();
109+
const [func, setFunc] = useState<Func | null>(null);
22110
const [sampleCounts, setSampleCounts] = useState<SampleCounts | undefined>();
23111

24112
useEffect(() => {
@@ -36,10 +124,9 @@ function TestViewer() {
36124
migrated = migrate({ functions: [json] });
37125
}
38126

39-
setIonJSON([migrated, JSON.stringify(migrated)]);
127+
setInitialIonJSON(migrated);
40128
}
41129
})();
42-
43130
(async () => {
44131
const sampleCountsFile = searchParams.get("sampleCounts");
45132
if (sampleCountsFile) {
@@ -53,92 +140,52 @@ function TestViewer() {
53140
})();
54141
}, []);
55142

56-
const [func, setFunc] = useState(searchParams.has("func") ? parseInt(searchParams.get("func")!, 10) : 0);
57-
const [pass, setPass] = useState(searchParams.has("pass") ? parseInt(searchParams.get("pass")!, 10) : 0);
58-
59-
async function fileSelected(e: ChangeEvent<HTMLInputElement>) {
60-
const input = e.target;
61-
if (!input.files?.length) {
62-
setIonJSON([emptyIonJSON, JSON.stringify(emptyIonJSON)]);
63-
return;
143+
return <div className="ig-absolute ig-absolute-fill ig-flex ig-flex-column">
144+
<MenuBar browse export ionjson={initialIonJSON} funcSelected={f => setFunc(f)} />
145+
{
146+
func && <div className="ig-relative ig-flex-basis-0 ig-flex-grow-1 ig-overflow-hidden">
147+
<GraphViewer
148+
func={func}
149+
pass={initialPass}
150+
sampleCounts={sampleCounts}
151+
/>
152+
</div>
64153
}
154+
</div >;
155+
}
65156

66-
const file = input.files[0];
67-
const newJSON = JSON.parse(await file.text());
68-
const migrated = migrate(newJSON);
69-
setIonJSON([migrated, JSON.stringify(migrated)]);
70-
}
71-
72-
let blocks: MIRBlock[] = [];
73-
const funcValid = 0 <= func && func < ionjson.functions.length;
74-
const passes = funcValid ? ionjson.functions[func].passes : [];
75-
const passValid = 0 <= pass && pass < passes.length;
76-
if (funcValid && passValid) {
77-
blocks = passes[pass].mir.blocks;
78-
}
157+
interface StandaloneUIProps {
158+
ionjson: IonJSON,
159+
}
79160

161+
function StandaloneUI(props: StandaloneUIProps) {
162+
const [func, setFunc] = useState<Func | null>(null);
80163
return <div className="ig-absolute ig-absolute-fill ig-flex ig-flex-column">
81-
<div className="ig-bb ig-pv2 ig-ph3 ig-flex ig-g2 ig-items-center ig-bg-white">
82-
<div>
83-
<input type="file" onChange={fileSelected} />
84-
</div>
85-
{funcValid && passValid && <>
86-
<div>
87-
Function <input
88-
type="number"
89-
value={func}
90-
onChange={e => {
91-
const newFunc = parseInt(e.target.value, 10);
92-
if (0 <= newFunc && newFunc < ionjson.functions.length) {
93-
setFunc(newFunc);
94-
}
95-
}}
96-
/>
97-
</div>
98-
<div>
99-
Pass: <input
100-
type="number"
101-
value={pass}
102-
onChange={e => {
103-
const newPass = parseInt(e.target.value, 10);
104-
if (0 <= newPass && newPass < ionjson.functions[func].passes.length) {
105-
setPass(newPass);
106-
}
107-
}}
108-
/>
109-
</div>
110-
<div>{ionjson.functions[func].name}</div>
111-
<div className="ig-flex-grow-1"></div>
112-
<div>
113-
<button onClick={() => exportStandalone(ionjson.functions[func].name, rawIonJSON, { func })}>Export</button>
114-
</div>
115-
</>}
116-
</div>
164+
<MenuBar ionjson={props.ionjson} funcSelected={f => setFunc(f)} />
117165
{
118-
funcValid && passValid && <div className="ig-relative ig-flex-basis-0 ig-flex-grow-1 ig-overflow-hidden">
166+
func && <div className="ig-relative ig-flex-basis-0 ig-flex-grow-1 ig-overflow-hidden">
119167
<GraphViewer
120-
func={ionjson.functions[func]}
121-
pass={pass}
122-
sampleCounts={sampleCounts}
168+
func={func}
169+
pass={initialPass}
123170
/>
124171
</div>
125172
}
126-
</div >;
173+
</div>;
127174
}
128175

129176
interface ExportOptions {
130-
func?: number,
177+
funcIndex?: number,
131178
}
132179

133180
async function exportStandalone(name: string, rawIonJSON: string, opts: ExportOptions = {}) {
134181
let jsonString = rawIonJSON;
135-
if (opts.func !== undefined) {
182+
if (opts.funcIndex !== undefined) {
136183
// HACK: Because the iongraph code actually mutates the input ion JSON, we
137184
// can't just JSON.stringify it any more, so we have to take the raw JSON
138185
// from the start of the whole process, re-parse it, filter it, and then
139186
// generate new raw JSON to write to the file!
140187
const parsedIonJSON = JSON.parse(rawIonJSON);
141-
const func = parsedIonJSON.functions[opts.func];
188+
const func = parsedIonJSON.functions[opts.funcIndex];
142189
const filteredIonJSON: IonJSON = { version: 1, functions: [func] };
143190
jsonString = JSON.stringify(filteredIonJSON);
144191
}

www/standalone.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
<script>{{ include-dist "main.standalone.js" }}</script>
1212
<script>window.__exportedIonJSON = {{ IONJSON }}</script>
1313
<script>
14-
iongraph.renderGraphOnly(window.reactRoot, window.__exportedIonJSON);
14+
iongraph.renderStandaloneUI(window.reactRoot, window.__exportedIonJSON);
1515
</script>
1616
</body>

0 commit comments

Comments
 (0)