Skip to content

Commit 6fe2469

Browse files
authored
Merge pull request #134 from PytorchConnectomics/fix/neuroglancer-public-url
This PR tightens several workflow contracts across the desktop app so local file-backed usage is more predictable and less brittle.
2 parents 172ddb2 + 6654793 commit 6fe2469

15 files changed

Lines changed: 1114 additions & 94 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Optional runtime environment variables:
3434
```
3535
PYTC_AUTH_SECRET=replace-me
3636
PYTC_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,null
37+
PYTC_NEUROGLANCER_PUBLIC_BASE=http://localhost:4244
3738
```
3839

3940
If restarting after a crash or interrupted session, kill any lingering processes first:

client/src/components/FileTreeSidebar.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,35 @@ const FileTreeSidebar = ({
1313
files,
1414
currentFolder,
1515
onSelect,
16+
onLoadFolder,
1617
onDrop,
1718
onContextMenu,
19+
loadedParents = [],
20+
loadingParents = [],
21+
expandedKeys,
22+
onExpand,
1823
width = 250,
1924
}) => {
2025
// Convert flat folders list to tree data
2126
const treeData = useMemo(() => {
27+
const loadedParentSet = new Set(loadedParents);
28+
const loadingParentSet = new Set(loadingParents);
29+
2230
const buildTree = (parentId) => {
2331
const children = folders
2432
.filter((f) => f.parent === parentId)
2533
.map((f) => ({
2634
title: f.title,
2735
key: `folder-${f.key}`,
2836
isLeaf: false,
37+
loading: loadingParentSet.has(f.key),
2938
icon: ({ expanded }) =>
3039
expanded ? <FolderOpenFilled /> : <FolderFilled />,
31-
children: buildTree(f.key),
40+
children: loadedParentSet.has(f.key) ? buildTree(f.key) : undefined,
3241
}));
3342

3443
// Add files to tree
35-
if (files && files[parentId]) {
44+
if (loadedParentSet.has(parentId) && files && files[parentId]) {
3645
children.push(
3746
...files[parentId].map((f) => ({
3847
title: f.name,
@@ -53,6 +62,7 @@ const FileTreeSidebar = ({
5362
title: f.title,
5463
key: `folder-${f.key}`,
5564
isLeaf: false,
65+
loading: loadingParentSet.has(f.key),
5666
icon: ({ expanded }) =>
5767
expanded ? <FolderOpenFilled /> : <FolderFilled />,
5868
children: buildTree(f.key),
@@ -64,7 +74,7 @@ const FileTreeSidebar = ({
6474
}
6575

6676
return rootNodes;
67-
}, [folders, files]);
77+
}, [files, folders, loadedParents, loadingParents]);
6878

6979
const onSelectHandler = (keys, info) => {
7080
if (keys.length > 0) {
@@ -76,6 +86,15 @@ const FileTreeSidebar = ({
7686
}
7787
};
7888

89+
const handleLoadData = async (node) => {
90+
const key = String(node?.key || "");
91+
if (!key.startsWith("folder-") || !onLoadFolder) {
92+
return;
93+
}
94+
95+
await onLoadFolder(key.replace("folder-", ""));
96+
};
97+
7998
const handleDrop = (info) => {
8099
if (onDrop) {
81100
onDrop(info);
@@ -110,11 +129,14 @@ const FileTreeSidebar = ({
110129
</div>
111130
<DirectoryTree
112131
multiple={false}
113-
defaultExpandAll
114132
selectedKeys={[`folder-${currentFolder}`]}
115133
onSelect={onSelectHandler}
116134
treeData={treeData}
117135
expandAction="click"
136+
expandedKeys={expandedKeys}
137+
onExpand={onExpand}
138+
loadData={handleLoadData}
139+
loadedKeys={loadedParents.map((key) => `folder-${key}`)}
118140
style={{ backgroundColor: "transparent", fontSize: 13 }}
119141
titleRender={(nodeData) => (
120142
<span
@@ -130,6 +152,7 @@ const FileTreeSidebar = ({
130152
title={String(nodeData.title)}
131153
>
132154
{nodeData.title}
155+
{nodeData.loading ? " ..." : ""}
133156
</span>
134157
)}
135158
draggable

client/src/components/InputSelector.js

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,34 +81,30 @@ function InputSelector(props) {
8181
}
8282
/>
8383
</Form.Item>
84-
<Form.Item
85-
label={
86-
<Space align="center">
87-
<span>
88-
{type === "training" ? "Input Label" : "Input Label (Optional)"}
89-
</span>
90-
<InlineHelpChat
91-
taskKey={type}
92-
label="Input Label"
93-
yamlKey="DATASET.LABEL_NAME"
94-
value={workflow.inputLabel}
95-
projectContext={projectContext}
96-
taskContext={taskContext}
97-
/>
98-
</Space>
99-
}
100-
>
101-
<UnifiedFileInput
102-
placeholder="Please select or input label path"
103-
onChange={handleLabelChange}
104-
value={getValue(workflow.inputLabel)}
105-
selectionType={
106-
type === "training" || type === "inference"
107-
? "fileOrDirectory"
108-
: "file"
84+
{type === "training" && (
85+
<Form.Item
86+
label={
87+
<Space align="center">
88+
<span>Input Label</span>
89+
<InlineHelpChat
90+
taskKey={type}
91+
label="Input Label"
92+
yamlKey="DATASET.LABEL_NAME"
93+
value={workflow.inputLabel}
94+
projectContext={projectContext}
95+
taskContext={taskContext}
96+
/>
97+
</Space>
10998
}
110-
/>
111-
</Form.Item>
99+
>
100+
<UnifiedFileInput
101+
placeholder="Please select or input label path"
102+
onChange={handleLabelChange}
103+
value={getValue(workflow.inputLabel)}
104+
selectionType="fileOrDirectory"
105+
/>
106+
</Form.Item>
107+
)}
112108
{type === "training" ? (
113109
<Form.Item
114110
label={
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React from "react";
2+
import { render, screen } from "@testing-library/react";
3+
4+
import InputSelector from "./InputSelector";
5+
import { AppContext } from "../contexts/GlobalContext";
6+
7+
jest.mock("antd", () => {
8+
const Form = ({ children }) => <form>{children}</form>;
9+
Form.Item = ({ label, children, help }) => (
10+
<div>
11+
{label}
12+
{children}
13+
{help ? <div>{help}</div> : null}
14+
</div>
15+
);
16+
17+
return {
18+
Form,
19+
Space: ({ children }) => <div>{children}</div>,
20+
};
21+
});
22+
23+
jest.mock("./UnifiedFileInput", () => (props) => (
24+
<input
25+
aria-label={props.placeholder}
26+
data-selection-type={props.selectionType}
27+
readOnly
28+
value={typeof props.value === "string" ? props.value : ""}
29+
/>
30+
));
31+
32+
jest.mock("./InlineHelpChat", () => () => null);
33+
34+
beforeAll(() => {
35+
Object.defineProperty(window, "matchMedia", {
36+
writable: true,
37+
value: jest.fn().mockImplementation((query) => ({
38+
matches: false,
39+
media: query,
40+
onchange: null,
41+
addListener: jest.fn(),
42+
removeListener: jest.fn(),
43+
addEventListener: jest.fn(),
44+
removeEventListener: jest.fn(),
45+
dispatchEvent: jest.fn(),
46+
})),
47+
});
48+
});
49+
50+
function renderWithContext(type) {
51+
const contextValue = {
52+
trainingState: {
53+
inputImage: "",
54+
inputLabel: "",
55+
outputPath: "",
56+
logPath: "",
57+
checkpointPath: "",
58+
setInputImage: jest.fn(),
59+
setInputLabel: jest.fn(),
60+
setOutputPath: jest.fn(),
61+
setLogPath: jest.fn(),
62+
setCheckpointPath: jest.fn(),
63+
},
64+
inferenceState: {
65+
inputImage: "",
66+
inputLabel: "/tmp/stale-label.tif",
67+
outputPath: "",
68+
logPath: "",
69+
checkpointPath: "",
70+
setInputImage: jest.fn(),
71+
setInputLabel: jest.fn(),
72+
setOutputPath: jest.fn(),
73+
setLogPath: jest.fn(),
74+
setCheckpointPath: jest.fn(),
75+
},
76+
};
77+
78+
return render(
79+
<AppContext.Provider value={contextValue}>
80+
<InputSelector type={type} />
81+
</AppContext.Provider>,
82+
);
83+
}
84+
85+
describe("InputSelector", () => {
86+
it("shows an input label field for training", () => {
87+
renderWithContext("training");
88+
89+
expect(screen.getByText("Input Label")).toBeTruthy();
90+
});
91+
92+
it("does not show an input label field for inference", () => {
93+
renderWithContext("inference");
94+
95+
expect(screen.queryByText("Input Label")).toBeNull();
96+
expect(screen.queryByText("Input Label (Optional)")).toBeNull();
97+
});
98+
});

0 commit comments

Comments
 (0)