From 0a3cc7bdfd0e884348aab00c66c5d0ae6e34e562 Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 31 Oct 2023 08:45:33 -0700 Subject: [PATCH 01/13] Expose jobs and threads input --- pyflask/manageNeuroconv/manage_neuroconv.py | 8 ++++++++ schemas/json/dandi/upload.json | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pyflask/manageNeuroconv/manage_neuroconv.py b/pyflask/manageNeuroconv/manage_neuroconv.py index 89a89544f..f6c633eca 100644 --- a/pyflask/manageNeuroconv/manage_neuroconv.py +++ b/pyflask/manageNeuroconv/manage_neuroconv.py @@ -485,6 +485,8 @@ def upload_folder_to_dandi( nwb_folder_path: Optional[str] = None, staging: Optional[bool] = None, # Override default staging=True cleanup: Optional[bool] = None, + number_of_jobs: Optional[int] = None, + number_of_threads: Optional[int] = None, ): from neuroconv.tools.data_transfers import automatic_dandi_upload @@ -495,6 +497,8 @@ def upload_folder_to_dandi( nwb_folder_path=Path(nwb_folder_path), staging=staging, cleanup=cleanup, + number_of_jobs=number_of_jobs, + number_of_threads=number_of_threads ) @@ -504,6 +508,8 @@ def upload_to_dandi( project: Optional[str] = None, staging: Optional[bool] = None, # Override default staging=True cleanup: Optional[bool] = None, + number_of_jobs: Optional[int] = None, + number_of_threads: Optional[int] = None, ): from neuroconv.tools.data_transfers import automatic_dandi_upload @@ -516,6 +522,8 @@ def upload_to_dandi( nwb_folder_path=CONVERSION_SAVE_FOLDER_PATH / project, # Scope valid DANDI upload paths to GUIDE projects staging=staging, cleanup=cleanup, + number_of_jobs=number_of_jobs, + number_of_threads=number_of_threads ) diff --git a/schemas/json/dandi/upload.json b/schemas/json/dandi/upload.json index c8507690b..a46148d3c 100644 --- a/schemas/json/dandi/upload.json +++ b/schemas/json/dandi/upload.json @@ -4,6 +4,18 @@ "type": "string", "description": "The unique identifier for your Dandiset, manually created on the main archive or staging server." }, + "number_of_jobs": { + "type": "number", + "title": "Job Count", + "description": "The number of files to upload in parallel.", + "default": 1 + }, + "number_of_threads": { + "type": "number", + "title": "Threads per Job", + "description": "The number of threads to handle each file.", + "default": 1 + }, "cleanup": { "type": "boolean", "title": "Delete Local Files After Upload", From 18b48c1c98430ebb87733bfab5805601a8177368 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 20:35:26 +0000 Subject: [PATCH 02/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyflask/manageNeuroconv/manage_neuroconv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyflask/manageNeuroconv/manage_neuroconv.py b/pyflask/manageNeuroconv/manage_neuroconv.py index f23759ce4..65585c00f 100644 --- a/pyflask/manageNeuroconv/manage_neuroconv.py +++ b/pyflask/manageNeuroconv/manage_neuroconv.py @@ -498,7 +498,7 @@ def upload_folder_to_dandi( staging=staging, cleanup=cleanup, number_of_jobs=number_of_jobs, - number_of_threads=number_of_threads + number_of_threads=number_of_threads, ) @@ -523,7 +523,7 @@ def upload_to_dandi( staging=staging, cleanup=cleanup, number_of_jobs=number_of_jobs, - number_of_threads=number_of_threads + number_of_threads=number_of_threads, ) From 7c98f15c1b2d4bb933708b52db716f426e478d8b Mon Sep 17 00:00:00 2001 From: Garrett Date: Tue, 31 Oct 2023 13:35:56 -0700 Subject: [PATCH 03/13] Update UploadsPage.js --- src/renderer/src/stories/pages/uploads/UploadsPage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 6ca0c747b..805dc964f 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -192,6 +192,8 @@ export class UploadsPage extends Page { ${this.form}
${button} +
+
`; } } From 06ff4c1d66047f0eee7e1569f8d4eb1a95aedce5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 20:37:11 +0000 Subject: [PATCH 04/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/pages/uploads/UploadsPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 805dc964f..f039d0eef 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -192,8 +192,8 @@ export class UploadsPage extends Page { ${this.form}
${button} -
-
+
+
`; } } From e22122dabdd2a52fbbc98cbae7d52ff4161d7caa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 02:36:05 +0000 Subject: [PATCH 05/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/JSONSchemaInput.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 753aeef26..f99643d45 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -110,7 +110,6 @@ export class JSONSchemaInput extends LitElement { // onValidate = () => {} updateData(value, forceValidate = false) { - if (this.value === value && !forceValidate) { const el = this.getElement(); if (el.type === "checkbox") el.checked = value; @@ -130,7 +129,6 @@ export class JSONSchemaInput extends LitElement { this.#triggerValidation(name, path); this.#updateData(fullPath, value); - return true; } From eacd75bca52e88484b53c186d1f4f795c47e815c Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Nov 2023 11:49:23 -0700 Subject: [PATCH 06/13] Properly restrict job count --- pyflask/app.py | 5 + schemas/dandi-upload.schema.ts | 13 ++ schemas/json/dandi/upload.json | 12 +- src/renderer/src/index.ts | 111 +------------- src/renderer/src/server.ts | 139 ++++++++++++++++++ src/renderer/src/stories/JSONSchemaInput.js | 45 +++++- .../guided-mode/data/GuidedSourceData.js | 2 + .../pages/guided-mode/options/GuidedUpload.js | 32 +++- .../src/stories/pages/uploads/UploadsPage.js | 67 ++++++--- 9 files changed, 281 insertions(+), 145 deletions(-) create mode 100644 schemas/dandi-upload.schema.ts create mode 100644 src/renderer/src/server.ts diff --git a/pyflask/app.py b/pyflask/app.py index 319dda6c4..95038ca15 100644 --- a/pyflask/app.py +++ b/pyflask/app.py @@ -95,6 +95,11 @@ def send_preview(path): return send_from_directory(STUB_SAVE_FOLDER_PATH, path) +@app.route("/cpus") +def get_cpu_count(): + from psutil import cpu_count + return {"physical": cpu_count(logical=False) } + @api.route("/server_shutdown", endpoint="shutdown") class Shutdown(Resource): def get(self): diff --git a/schemas/dandi-upload.schema.ts b/schemas/dandi-upload.schema.ts new file mode 100644 index 000000000..244acaa2e --- /dev/null +++ b/schemas/dandi-upload.schema.ts @@ -0,0 +1,13 @@ +import upload from './json/dandi/upload.json' assert { type: "json" } + + +upload.properties.number_of_jobs.transform = upload.properties.number_of_threads.transform = (value, prevValue) => { + if (value === 0){ + if (prevValue === -1) return 1 + else return -1 + } + + return value +} + +export default upload \ No newline at end of file diff --git a/schemas/json/dandi/upload.json b/schemas/json/dandi/upload.json index a46148d3c..35b055c16 100644 --- a/schemas/json/dandi/upload.json +++ b/schemas/json/dandi/upload.json @@ -5,16 +5,18 @@ "description": "The unique identifier for your Dandiset, manually created on the main archive or staging server." }, "number_of_jobs": { - "type": "number", + "type": "integer", "title": "Job Count", - "description": "The number of files to upload in parallel.", - "default": 1 + "description": "The number of files to upload in parallel. A value of -1 uses all available processes", + "default": 1, + "min": -1 }, "number_of_threads": { - "type": "number", + "type": "integer", "title": "Threads per Job", "description": "The number of threads to handle each file.", - "default": 1 + "default": 1, + "min": -1 }, "cleanup": { "type": "boolean", diff --git a/src/renderer/src/index.ts b/src/renderer/src/index.ts index 1ba3787db..f84f36604 100644 --- a/src/renderer/src/index.ts +++ b/src/renderer/src/index.ts @@ -1,5 +1,5 @@ import "./pages.js" -import { isElectron, electron, app } from './electron/index.js' +import { isElectron, electron } from './electron/index.js' const { ipcRenderer } = electron; import { Dashboard } from './stories/Dashboard.js' @@ -9,27 +9,11 @@ import { notify } from './dependencies/globals.js' -import { baseUrl } from './globals.js' - import Swal from 'sweetalert2' - -import { StatusBar } from "./stories/status/StatusBar.js"; -import { unsafeSVG } from "lit/directives/unsafe-svg.js"; -import serverSVG from "./stories/assets/server.svg?raw"; -import webAssetSVG from "./stories/assets/web_asset.svg?raw"; -import wifiSVG from "./stories/assets/wifi.svg?raw"; +import { loadServerEvents, pythonServerOpened, statusBar } from "./server.js"; // Set the sidebar subtitle to the current app version const dashboard = document.querySelector('nwb-dashboard') as Dashboard -const appVersion = app?.getVersion(); - -const statusBar = new StatusBar({ - items: [ - { label: unsafeSVG(webAssetSVG), value: isElectron ? appVersion ?? 'ERROR' : 'Web' }, - { label: unsafeSVG(wifiSVG) }, - { label: unsafeSVG(serverSVG) } - ] -}) dashboard.subtitle = statusBar @@ -94,94 +78,10 @@ async function checkInternetConnection() { return hasInternet }; -// Check if the Flask server is live -const serverIsLiveStartup = async () => { - const echoResponse = await fetch(`${baseUrl}/startup/echo?arg=server ready`).then(res => res.json()).catch(e => e) - return echoResponse === "server ready" ? true : false; -}; - -// Preload Flask imports for faster on-demand operations -const preloadFlaskImports = () => fetch(`${baseUrl}/startup/preload-imports`).then(res => { - if (res.ok) return res.json() - else throw new Error('Error preloading Flask imports') -}) - -let openPythonStatusNotyf: undefined | any; - -async function pythonServerOpened() { - - - // Confirm requests are actually received by the server - const isLive = await serverIsLiveStartup() - // initiate preload of Flask imports - if (isLive) await preloadFlaskImports() - .then(() => { - - // Update server status and throw a notification - statusBar.items[2].status = true - - if (openPythonStatusNotyf) notyf.dismiss(openPythonStatusNotyf) - openPythonStatusNotyf = notyf.open({ - type: "success", - message: "Backend services are available", - }); - - }) - .catch(e =>{ - - statusBar.items[2].status = 'issue' - - notyf.open({ - type: "warning", - message: e.message, - }); - - }) - - // If the server is not live, throw an error - else return pythonServerClosed() - -} - - -async function pythonServerClosed(message?: string) { - - if (message) console.error(message) - statusBar.items[2].status = false - - await Swal.fire({ - icon: "error", - html: `

Something went wrong while initializing the application's background services.

Please restart NWB GUIDE and try again. If this issue occurs multiple times, please open an issue on the NWB GUIDE Issue Tracker.`, - heightAuto: false, - backdrop: "rgba(0,0,0, 0.4)", - confirmButtonText: "Exit Now", - allowOutsideClick: false, - allowEscapeKey: false, - }); - - if (isElectron) app.exit(); - else location.reload() - - Swal.close(); - - -} +loadServerEvents() if (isElectron) { - ipcRenderer.send("python.status"); // Trigger status check - - ipcRenderer.on("python.open", pythonServerOpened); - - ipcRenderer.on("python.closed", (_, message) => pythonServerClosed(message)); - ipcRenderer.on("python.restart", () => { - statusBar.items[2].status = undefined - if (openPythonStatusNotyf) notyf.dismiss(openPythonStatusNotyf) - openPythonStatusNotyf = notyf.open({ - type: "warning", - message: "Backend services are restarting...", - }) - }); // Check for update and show the pop up box ipcRenderer.on("update_available", () => { @@ -205,11 +105,12 @@ if (isElectron) { } else { update_downloaded_notification = notyf.open({ type: "app_update_warning", - message: + message: "Update downloaded. It will be installed on the restart of the app. Click here to restart NWB GUIDE now.", }); } - update_downloaded_notification.on("click", async ({ target, event }) => { + + update_downloaded_notification.on("click", async () => { restartApp(); }); }); diff --git a/src/renderer/src/server.ts b/src/renderer/src/server.ts new file mode 100644 index 000000000..50d33894d --- /dev/null +++ b/src/renderer/src/server.ts @@ -0,0 +1,139 @@ +import { isElectron, electron, app } from './electron/index.js' +const { ipcRenderer } = electron; + +import { + notyf, +} from './dependencies/globals.js' + +import { baseUrl } from './globals.js' + +import Swal from 'sweetalert2' + + +import { StatusBar } from "./stories/status/StatusBar.js"; +import { unsafeSVG } from "lit/directives/unsafe-svg.js"; +import serverSVG from "./stories/assets/server.svg?raw"; +import webAssetSVG from "./stories/assets/web_asset.svg?raw"; +import wifiSVG from "./stories/assets/wifi.svg?raw"; + +const appVersion = app?.getVersion(); + +export const statusBar = new StatusBar({ + items: [ + { label: unsafeSVG(webAssetSVG), value: isElectron ? appVersion ?? 'ERROR' : 'Web' }, + { label: unsafeSVG(wifiSVG) }, + { label: unsafeSVG(serverSVG) } + ] +}) + + +// Check if the Flask server is live +const serverIsLiveStartup = async () => { + const echoResponse = await fetch(`${baseUrl}/startup/echo?arg=server ready`).then(res => res.json()).catch(e => e) + return echoResponse === "server ready" ? true : false; + }; + + // Preload Flask imports for faster on-demand operations + const preloadFlaskImports = () => fetch(`${baseUrl}/startup/preload-imports`).then(res => { + if (res.ok) return res.json() + else throw new Error('Error preloading Flask imports') + }) + + +let serverCallbacks: Function[] = [] +export const onServerOpen = (callback:Function) => { + if (statusBar.items[2].status === true) return callback() + else { + return new Promise(res => { + serverCallbacks.push(() => { + res(callback()) + }) + + }) + } +} + +export async function pythonServerOpened() { + + + console.error('Server open') + + // Confirm requests are actually received by the server + const isLive = await serverIsLiveStartup() + + // initiate preload of Flask imports + if (isLive) await preloadFlaskImports() + .then(() => { + + // Update server status and throw a notification + statusBar.items[2].status = true + + serverCallbacks.forEach(cb => cb()) + serverCallbacks = [] + + if (openPythonStatusNotyf) notyf.dismiss(openPythonStatusNotyf) + openPythonStatusNotyf = notyf.open({ + type: "success", + message: "Backend services are available", + }); + + }) + .catch(e =>{ + + statusBar.items[2].status = 'issue' + + notyf.open({ + type: "warning", + message: e.message, + }); + + }) + + // If the server is not live, throw an error + else return pythonServerClosed() + +} + + +export async function pythonServerClosed(message?: string) { + + if (message) console.error(message) + statusBar.items[2].status = false + + await Swal.fire({ + icon: "error", + html: `

Something went wrong while initializing the application's background services.

Please restart NWB GUIDE and try again. If this issue occurs multiple times, please open an issue on the NWB GUIDE Issue Tracker.`, + heightAuto: false, + backdrop: "rgba(0,0,0, 0.4)", + confirmButtonText: "Exit Now", + allowOutsideClick: false, + allowEscapeKey: false, + }); + + if (isElectron) app.exit(); + else location.reload() + + Swal.close(); + + +} + +let openPythonStatusNotyf: undefined | any; + +export const loadServerEvents = () => { + if (isElectron) { + ipcRenderer.send("python.status"); // Trigger status check + + ipcRenderer.on("python.open", pythonServerOpened); + + ipcRenderer.on("python.closed", (_, message) => pythonServerClosed(message)); + ipcRenderer.on("python.restart", () => { + statusBar.items[2].status = undefined + if (openPythonStatusNotyf) notyf.dismiss(openPythonStatusNotyf) + openPythonStatusNotyf = notyf.open({ + type: "warning", + message: "Backend services are restarting...", + }) + }); + } +} \ No newline at end of file diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index f99643d45..35f87a2b7 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -345,7 +345,12 @@ export class JSONSchemaInput extends LitElement { ?checked=${this.value ?? false} @change=${(ev) => validateOnChange && this.#triggerValidation(name, path)} />`; - } else if (info.type === "string" || info.type === "number") { + } else if (info.type === "string" || info.type === "number" || info.type === 'integer') { + + const isInteger = info.type === 'integer'; + if (isInteger) info.type = 'number'; + const isNumber = info.type === 'number'; + const fileSystemFormat = isFilesystemSelector(name, info.format); if (fileSystemFormat) return createFilesystemSelector(fileSystemFormat); // Handle long string formats @@ -372,14 +377,46 @@ export class JSONSchemaInput extends LitElement { + @input=${(ev) => { + + + let value = ev.target.value; + let newValue = value + + // const isBlank = value === ''; + + if (isInteger) newValue = parseInt(value); + + else if (isNumber) newValue = parseFloat(value) + + if (isNumber) { + if ('min' in info && value < info.min) newValue = info.min + else if ('max' in info && value > info.max) newValue = info.max + } + + if (info.transform) newValue = info.transform(newValue, this.value, info) + + // // Do not check patter if value is empty + // if (info.pattern && !isBlank) { + // const regex = new RegExp(info.pattern) + // if (!regex.test(isNaN(newValue) ? value : newValue)) newValue = this.value // revert to last value + // } + + if (!isNaN(newValue) && newValue !== value) { + ev.target.value = newValue + value = newValue + } + + this.#updateData( fullPath, - info.type === "number" ? parseFloat(ev.target.value) : ev.target.value + value )} + } @change=${(ev) => validateOnChange && this.#triggerValidation(name, path)} /> `; diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js index 70fa3bf03..fdbc85b5a 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -124,6 +124,8 @@ export class GuidedSourceDataPage extends ManagedPage { }) ); + await this.save() + this.to(1); }, }; diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index ca4a980c4..c742184db 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -5,9 +5,12 @@ import { run } from "./utils.js"; import { onThrow } from "../../../../errors"; import { merge } from "../../utils.js"; import Swal from "sweetalert2"; -import dandiUploadSchema from "../../../../../../../schemas/json/dandi/upload.json"; +import dandiUploadSchema from "../../../../../../../schemas/dandi-upload.schema"; import { dandisetInfoContent, uploadToDandi } from "../../uploads/UploadsPage.js"; import { InfoBox } from "../../../InfoBox.js"; +import { until } from "lit/directives/until.js"; +import { onServerOpen } from "../../../../server"; +import { baseUrl } from "../../../../globals.js"; export class GuidedUploadPage extends Page { constructor(...args) { @@ -64,17 +67,30 @@ export class GuidedUploadPage extends Page { render() { const state = (this.localState = merge(this.info.globalState.upload ?? { info: {} }, {})); - this.form = new JSONSchemaForm({ - schema: dandiUploadSchema, - results: state.info, - onUpdate: () => (this.unsavedUpdates = true), - onThrow, - }); + + const promise = onServerOpen(() => { + return fetch(new URL("cpus", baseUrl)) + .then((res) => res.json()).then(({ physical }) => { + + dandiUploadSchema.properties.number_of_jobs.max = physical; + + return this.form = new JSONSchemaForm({ + schema: dandiUploadSchema, + results: state.info, + onUpdate: () => (this.unsavedUpdates = true), + onThrow, + }); + + }) + }) return html`${new InfoBox({ header: "How do I create a Dandiset?", content: dandisetInfoContent, - })}

${this.form} `; + })}

${until( + promise, + html`Loading form contents...` + )} `; } } diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index f039d0eef..734565690 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -1,10 +1,12 @@ import { html } from "lit"; +import { until } from 'lit/directives/until.js'; + import { JSONSchemaForm } from "../../JSONSchemaForm.js"; import { Page } from "../Page.js"; import { onThrow } from "../../../errors"; const folderPathKey = "filesystem_paths"; -import dandiUploadSchema from "../../../../../../schemas/json/dandi/upload.json"; +import dandiUploadSchema from "../../../../../../schemas/dandi-upload.schema"; import dandiStandaloneSchema from "../../../../../../schemas/json/dandi/standalone.json"; const dandiSchema = merge(dandiStandaloneSchema, merge(dandiUploadSchema, {}), { arrays: true }); @@ -23,6 +25,9 @@ import { header } from "../../forms/utils"; import { validateDANDIApiKey } from "../../../validation/dandi"; import { InfoBox } from "../../InfoBox.js"; +import { onServerOpen } from '../../../server' +import { baseUrl } from "../../../globals.js"; + export const isStaging = (id) => parseInt(id) >= 100000; export const dandisetInfoContent = html` @@ -162,25 +167,34 @@ export class UploadsPage extends Page { }, }); - // NOTE: API Keys and Dandiset IDs persist across selected project - this.form = new JSONSchemaForm({ - results: globalState, - schema: dandiSchema, - sort: ([k1]) => { - if (k1 === folderPathKey) return -1; - }, - onUpdate: ([id]) => { - if (id === folderPathKey) { - for (let key in dandiSchema.properties) { - const input = this.form.getInput([key]); - if (key !== folderPathKey && input.value) input.updateData(""); // Clear the results of the form - } - } + const promise = onServerOpen(() => { + return fetch(new URL("cpus", baseUrl)) + .then((res) => res.json()).then(({ physical }) => { + console.log(physical) + + dandiSchema.properties.number_of_jobs.max = physical; + + // NOTE: API Keys and Dandiset IDs persist across selected project + return this.form = new JSONSchemaForm({ + results: globalState, + schema: dandiSchema, + sort: ([k1]) => { + if (k1 === folderPathKey) return -1; + }, + onUpdate: ([id]) => { + if (id === folderPathKey) { + for (let key in dandiSchema.properties) { + const input = this.form.getInput([key]); + if (key !== folderPathKey && input.value) input.updateData(""); // Clear the results of the form + } + } - global.save(); - }, - onThrow, - }); + global.save(); + }, + onThrow, + }); + }) + }) return html` ${new InfoBox({ @@ -189,12 +203,19 @@ export class UploadsPage extends Page { })}

- ${this.form} -
- ${button} + ${until( + promise.then(form => { + return html` + ${form} +
+ ${button} + ` + }), + html`

Waiting to connect to the Flask server...

`, + )}

- `; + ` } } From 91c23dca228898c23a2f83f57e09de80a7837054 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:53:52 +0000 Subject: [PATCH 07/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyflask/app.py | 4 +- schemas/dandi-upload.schema.ts | 2 +- src/renderer/src/server.ts | 4 +- src/renderer/src/stories/JSONSchemaInput.js | 37 +++++------- .../guided-mode/data/GuidedSourceData.js | 2 +- .../pages/guided-mode/options/GuidedUpload.js | 29 ++++----- .../src/stories/pages/uploads/UploadsPage.js | 60 ++++++++++--------- 7 files changed, 64 insertions(+), 74 deletions(-) diff --git a/pyflask/app.py b/pyflask/app.py index 95038ca15..065ce4cee 100644 --- a/pyflask/app.py +++ b/pyflask/app.py @@ -98,7 +98,9 @@ def send_preview(path): @app.route("/cpus") def get_cpu_count(): from psutil import cpu_count - return {"physical": cpu_count(logical=False) } + + return {"physical": cpu_count(logical=False)} + @api.route("/server_shutdown", endpoint="shutdown") class Shutdown(Resource): diff --git a/schemas/dandi-upload.schema.ts b/schemas/dandi-upload.schema.ts index 244acaa2e..278121143 100644 --- a/schemas/dandi-upload.schema.ts +++ b/schemas/dandi-upload.schema.ts @@ -10,4 +10,4 @@ upload.properties.number_of_jobs.transform = upload.properties.number_of_threads return value } -export default upload \ No newline at end of file +export default upload diff --git a/src/renderer/src/server.ts b/src/renderer/src/server.ts index 50d33894d..8012b9d32 100644 --- a/src/renderer/src/server.ts +++ b/src/renderer/src/server.ts @@ -32,7 +32,7 @@ const serverIsLiveStartup = async () => { const echoResponse = await fetch(`${baseUrl}/startup/echo?arg=server ready`).then(res => res.json()).catch(e => e) return echoResponse === "server ready" ? true : false; }; - + // Preload Flask imports for faster on-demand operations const preloadFlaskImports = () => fetch(`${baseUrl}/startup/preload-imports`).then(res => { if (res.ok) return res.json() @@ -136,4 +136,4 @@ export const loadServerEvents = () => { }) }); } -} \ No newline at end of file +} diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 35f87a2b7..f2ae8b2da 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -345,11 +345,10 @@ export class JSONSchemaInput extends LitElement { ?checked=${this.value ?? false} @change=${(ev) => validateOnChange && this.#triggerValidation(name, path)} />`; - } else if (info.type === "string" || info.type === "number" || info.type === 'integer') { - - const isInteger = info.type === 'integer'; - if (isInteger) info.type = 'number'; - const isNumber = info.type === 'number'; + } else if (info.type === "string" || info.type === "number" || info.type === "integer") { + const isInteger = info.type === "integer"; + if (isInteger) info.type = "number"; + const isNumber = info.type === "number"; const fileSystemFormat = isFilesystemSelector(name, info.format); if (fileSystemFormat) return createFilesystemSelector(fileSystemFormat); @@ -378,27 +377,23 @@ export class JSONSchemaInput extends LitElement { class="guided--input schema-input ${info.step === null ? "hideStep" : ""}" type="${type}" step=${isNumber && info.step ? info.step : ""} - placeholder="${info.placeholder ?? ""}" .value="${this.value ?? ""}" @input=${(ev) => { - - let value = ev.target.value; - let newValue = value + let newValue = value; // const isBlank = value === ''; - + if (isInteger) newValue = parseInt(value); - - else if (isNumber) newValue = parseFloat(value) + else if (isNumber) newValue = parseFloat(value); if (isNumber) { - if ('min' in info && value < info.min) newValue = info.min - else if ('max' in info && value > info.max) newValue = info.max + if ("min" in info && value < info.min) newValue = info.min; + else if ("max" in info && value > info.max) newValue = info.max; } - if (info.transform) newValue = info.transform(newValue, this.value, info) + if (info.transform) newValue = info.transform(newValue, this.value, info); // // Do not check patter if value is empty // if (info.pattern && !isBlank) { @@ -407,16 +402,12 @@ export class JSONSchemaInput extends LitElement { // } if (!isNaN(newValue) && newValue !== value) { - ev.target.value = newValue - value = newValue + ev.target.value = newValue; + value = newValue; } - - this.#updateData( - fullPath, - value - )} - } + this.#updateData(fullPath, value); + }} @change=${(ev) => validateOnChange && this.#triggerValidation(name, path)} /> `; diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js index fdbc85b5a..f0c98fc80 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -124,7 +124,7 @@ export class GuidedSourceDataPage extends ManagedPage { }) ); - await this.save() + await this.save(); this.to(1); }, diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index c742184db..8d8f5846d 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -67,30 +67,25 @@ export class GuidedUploadPage extends Page { render() { const state = (this.localState = merge(this.info.globalState.upload ?? { info: {} }, {})); - const promise = onServerOpen(() => { return fetch(new URL("cpus", baseUrl)) - .then((res) => res.json()).then(({ physical }) => { - - dandiUploadSchema.properties.number_of_jobs.max = physical; - - return this.form = new JSONSchemaForm({ - schema: dandiUploadSchema, - results: state.info, - onUpdate: () => (this.unsavedUpdates = true), - onThrow, + .then((res) => res.json()) + .then(({ physical }) => { + dandiUploadSchema.properties.number_of_jobs.max = physical; + + return (this.form = new JSONSchemaForm({ + schema: dandiUploadSchema, + results: state.info, + onUpdate: () => (this.unsavedUpdates = true), + onThrow, + })); }); - - }) - }) + }); return html`${new InfoBox({ header: "How do I create a Dandiset?", content: dandisetInfoContent, - })}

${until( - promise, - html`Loading form contents...` - )} `; + })}

${until(promise, html`Loading form contents...`)} `; } } diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 734565690..984421c99 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -1,5 +1,5 @@ import { html } from "lit"; -import { until } from 'lit/directives/until.js'; +import { until } from "lit/directives/until.js"; import { JSONSchemaForm } from "../../JSONSchemaForm.js"; import { Page } from "../Page.js"; @@ -25,7 +25,7 @@ import { header } from "../../forms/utils"; import { validateDANDIApiKey } from "../../../validation/dandi"; import { InfoBox } from "../../InfoBox.js"; -import { onServerOpen } from '../../../server' +import { onServerOpen } from "../../../server"; import { baseUrl } from "../../../globals.js"; export const isStaging = (id) => parseInt(id) >= 100000; @@ -169,32 +169,33 @@ export class UploadsPage extends Page { const promise = onServerOpen(() => { return fetch(new URL("cpus", baseUrl)) - .then((res) => res.json()).then(({ physical }) => { - console.log(physical) - - dandiSchema.properties.number_of_jobs.max = physical; - - // NOTE: API Keys and Dandiset IDs persist across selected project - return this.form = new JSONSchemaForm({ - results: globalState, - schema: dandiSchema, - sort: ([k1]) => { - if (k1 === folderPathKey) return -1; - }, - onUpdate: ([id]) => { - if (id === folderPathKey) { - for (let key in dandiSchema.properties) { - const input = this.form.getInput([key]); - if (key !== folderPathKey && input.value) input.updateData(""); // Clear the results of the form + .then((res) => res.json()) + .then(({ physical }) => { + console.log(physical); + + dandiSchema.properties.number_of_jobs.max = physical; + + // NOTE: API Keys and Dandiset IDs persist across selected project + return (this.form = new JSONSchemaForm({ + results: globalState, + schema: dandiSchema, + sort: ([k1]) => { + if (k1 === folderPathKey) return -1; + }, + onUpdate: ([id]) => { + if (id === folderPathKey) { + for (let key in dandiSchema.properties) { + const input = this.form.getInput([key]); + if (key !== folderPathKey && input.value) input.updateData(""); // Clear the results of the form + } } - } - global.save(); - }, - onThrow, + global.save(); + }, + onThrow, + })); }); - }) - }) + }); return html` ${new InfoBox({ @@ -204,18 +205,19 @@ export class UploadsPage extends Page {

${until( - promise.then(form => { + promise.then((form) => { return html` ${form}


${button} - ` + `; }), - html`

Waiting to connect to the Flask server...

`, + html`

Waiting to connect to the Flask server...

+

` )}

- ` + `; } } From a931bbed1a06fbe6b2c051877c44aa85225f76a3 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Nov 2023 14:11:14 -0700 Subject: [PATCH 08/13] Integrate max for threads --- pyflask/app.py | 8 +++++++- src/renderer/src/stories/pages/uploads/UploadsPage.js | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyflask/app.py b/pyflask/app.py index 065ce4cee..7938e89d5 100644 --- a/pyflask/app.py +++ b/pyflask/app.py @@ -99,7 +99,13 @@ def send_preview(path): def get_cpu_count(): from psutil import cpu_count - return {"physical": cpu_count(logical=False)} + physical = cpu_count(logical=False) + logical = cpu_count() + + return dict( + physical=physical, + logical=logical + ) @api.route("/server_shutdown", endpoint="shutdown") diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 984421c99..d0ee4a29a 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -170,11 +170,12 @@ export class UploadsPage extends Page { const promise = onServerOpen(() => { return fetch(new URL("cpus", baseUrl)) .then((res) => res.json()) - .then(({ physical }) => { + .then(({ physical, logical }) => { console.log(physical); dandiSchema.properties.number_of_jobs.max = physical; - + dandiSchema.properties.number_of_threads.max = logical / physical; + // NOTE: API Keys and Dandiset IDs persist across selected project return (this.form = new JSONSchemaForm({ results: globalState, From db4c4bebc7ce999e32ce1d73b5fac669302a6352 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:11:53 +0000 Subject: [PATCH 09/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyflask/app.py | 5 +---- src/renderer/src/stories/pages/uploads/UploadsPage.js | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pyflask/app.py b/pyflask/app.py index 7938e89d5..977dae0f3 100644 --- a/pyflask/app.py +++ b/pyflask/app.py @@ -102,10 +102,7 @@ def get_cpu_count(): physical = cpu_count(logical=False) logical = cpu_count() - return dict( - physical=physical, - logical=logical - ) + return dict(physical=physical, logical=logical) @api.route("/server_shutdown", endpoint="shutdown") diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index d0ee4a29a..392f84166 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -175,7 +175,7 @@ export class UploadsPage extends Page { dandiSchema.properties.number_of_jobs.max = physical; dandiSchema.properties.number_of_threads.max = logical / physical; - + // NOTE: API Keys and Dandiset IDs persist across selected project return (this.form = new JSONSchemaForm({ results: globalState, From b41a2ed4fc8961998d449f1bbe7b71fb5d85114c Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Nov 2023 14:13:05 -0700 Subject: [PATCH 10/13] Cleanup --- .../src/stories/pages/guided-mode/options/GuidedUpload.js | 4 +++- src/renderer/src/stories/pages/uploads/UploadsPage.js | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index 8d8f5846d..c0306f31c 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -70,8 +70,10 @@ export class GuidedUploadPage extends Page { const promise = onServerOpen(() => { return fetch(new URL("cpus", baseUrl)) .then((res) => res.json()) - .then(({ physical }) => { + .then(({ physical, logical }) => { + dandiUploadSchema.properties.number_of_jobs.max = physical; + dandiUploadSchema.properties.number_of_threads.max = logical / physical; return (this.form = new JSONSchemaForm({ schema: dandiUploadSchema, diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index d0ee4a29a..e45e1b34e 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -171,11 +171,10 @@ export class UploadsPage extends Page { return fetch(new URL("cpus", baseUrl)) .then((res) => res.json()) .then(({ physical, logical }) => { - console.log(physical); dandiSchema.properties.number_of_jobs.max = physical; dandiSchema.properties.number_of_threads.max = logical / physical; - + // NOTE: API Keys and Dandiset IDs persist across selected project return (this.form = new JSONSchemaForm({ results: globalState, From 677bdea06f6c84dacb62d6b3280ebeba32c22a5f Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Thu, 2 Nov 2023 14:35:24 -0700 Subject: [PATCH 11/13] Update upload.json Co-authored-by: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> --- schemas/json/dandi/upload.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/json/dandi/upload.json b/schemas/json/dandi/upload.json index 35b055c16..163d75066 100644 --- a/schemas/json/dandi/upload.json +++ b/schemas/json/dandi/upload.json @@ -14,7 +14,7 @@ "number_of_threads": { "type": "integer", "title": "Threads per Job", - "description": "The number of threads to handle each file.", + "description": "The number of threads to handle each file. A value of -1 uses all available threads per process.", "default": 1, "min": -1 }, From 28a6d74a175d641c5b1872bceaa6906877d3c343 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Nov 2023 15:19:15 -0700 Subject: [PATCH 12/13] Mock open server --- src/renderer/src/index.ts | 1 - src/renderer/src/server.ts | 18 +++++--- .../pages/guided-mode/options/GuidedUpload.js | 21 +++++---- .../stories/pages/guided-mode/storyStates.ts | 5 +++ .../src/stories/pages/uploads/UploadsPage.js | 45 ++++++++++--------- 5 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/renderer/src/index.ts b/src/renderer/src/index.ts index f84f36604..7d25747a0 100644 --- a/src/renderer/src/index.ts +++ b/src/renderer/src/index.ts @@ -17,7 +17,6 @@ const dashboard = document.querySelector('nwb-dashboard') as Dashboard dashboard.subtitle = statusBar - ////////////////////////////////// // Connect to Python back-end ////////////////////////////////// diff --git a/src/renderer/src/server.ts b/src/renderer/src/server.ts index 8012b9d32..1485676b7 100644 --- a/src/renderer/src/server.ts +++ b/src/renderer/src/server.ts @@ -53,10 +53,15 @@ export const onServerOpen = (callback:Function) => { } } -export async function pythonServerOpened() { +export const activateServer = () => { + console.log('GO!') + statusBar.items[2].status = true + serverCallbacks.forEach(cb => cb()) + serverCallbacks = [] +} - console.error('Server open') +export async function pythonServerOpened() { // Confirm requests are actually received by the server const isLive = await serverIsLiveStartup() @@ -65,11 +70,8 @@ export async function pythonServerOpened() { if (isLive) await preloadFlaskImports() .then(() => { - // Update server status and throw a notification - statusBar.items[2].status = true - - serverCallbacks.forEach(cb => cb()) - serverCallbacks = [] + // Update server status and throw a notification + activateServer() if (openPythonStatusNotyf) notyf.dismiss(openPythonStatusNotyf) openPythonStatusNotyf = notyf.open({ @@ -136,4 +138,6 @@ export const loadServerEvents = () => { }) }); } + + else activateServer() // Just mock-activate the server if we're in the browser } diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index c0306f31c..00970f13b 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -67,21 +67,20 @@ export class GuidedUploadPage extends Page { render() { const state = (this.localState = merge(this.info.globalState.upload ?? { info: {} }, {})); - const promise = onServerOpen(() => { - return fetch(new URL("cpus", baseUrl)) + const promise = onServerOpen(async () => { + await fetch(new URL("cpus", baseUrl)) .then((res) => res.json()) .then(({ physical, logical }) => { - dandiUploadSchema.properties.number_of_jobs.max = physical; dandiUploadSchema.properties.number_of_threads.max = logical / physical; - - return (this.form = new JSONSchemaForm({ - schema: dandiUploadSchema, - results: state.info, - onUpdate: () => (this.unsavedUpdates = true), - onThrow, - })); - }); + }).catch(() => {}); + + return (this.form = new JSONSchemaForm({ + schema: dandiUploadSchema, + results: state.info, + onUpdate: () => (this.unsavedUpdates = true), + onThrow, + })); }); return html`${new InfoBox({ diff --git a/src/renderer/src/stories/pages/guided-mode/storyStates.ts b/src/renderer/src/stories/pages/guided-mode/storyStates.ts index 72257d829..c9e529244 100644 --- a/src/renderer/src/stories/pages/guided-mode/storyStates.ts +++ b/src/renderer/src/stories/pages/guided-mode/storyStates.ts @@ -2,6 +2,11 @@ import nwbBaseSchema from "../../../../../../schemas/base-metadata.schema"; // import exephysExampleSchema from "../../../../../../schemas/json/ecephys_metadata_schema_example.json"; import { dashboard } from "../../../pages.js"; +import { activateServer } from "../../../server"; + + +activateServer(); + export const PageTemplate = (args = {}) => { for (let k in args) dashboard[k] = args[k]; return dashboard; diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index e45e1b34e..09a73f50a 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -167,34 +167,35 @@ export class UploadsPage extends Page { }, }); - const promise = onServerOpen(() => { - return fetch(new URL("cpus", baseUrl)) + const promise = onServerOpen(async () => { + await fetch(new URL("cpus", baseUrl)) .then((res) => res.json()) .then(({ physical, logical }) => { dandiSchema.properties.number_of_jobs.max = physical; dandiSchema.properties.number_of_threads.max = logical / physical; + }) + .catch(() => {}); + + // NOTE: API Keys and Dandiset IDs persist across selected project + return (this.form = new JSONSchemaForm({ + results: globalState, + schema: dandiSchema, + sort: ([k1]) => { + if (k1 === folderPathKey) return -1; + }, + onUpdate: ([id]) => { + if (id === folderPathKey) { + for (let key in dandiSchema.properties) { + const input = this.form.getInput([key]); + if (key !== folderPathKey && input.value) input.updateData(""); // Clear the results of the form + } + } - // NOTE: API Keys and Dandiset IDs persist across selected project - return (this.form = new JSONSchemaForm({ - results: globalState, - schema: dandiSchema, - sort: ([k1]) => { - if (k1 === folderPathKey) return -1; - }, - onUpdate: ([id]) => { - if (id === folderPathKey) { - for (let key in dandiSchema.properties) { - const input = this.form.getInput([key]); - if (key !== folderPathKey && input.value) input.updateData(""); // Clear the results of the form - } - } - - global.save(); - }, - onThrow, - })); - }); + global.save(); + }, + onThrow, + })); }); return html` From 0d8ebaf968a11cb445aeb99d2e3fbe6e6dba890c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:20:43 +0000 Subject: [PATCH 13/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../src/stories/pages/guided-mode/options/GuidedUpload.js | 3 ++- src/renderer/src/stories/pages/uploads/UploadsPage.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index 00970f13b..f3647e701 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -73,7 +73,8 @@ export class GuidedUploadPage extends Page { .then(({ physical, logical }) => { dandiUploadSchema.properties.number_of_jobs.max = physical; dandiUploadSchema.properties.number_of_threads.max = logical / physical; - }).catch(() => {}); + }) + .catch(() => {}); return (this.form = new JSONSchemaForm({ schema: dandiUploadSchema, diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 09a73f50a..68bbc12da 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -171,7 +171,6 @@ export class UploadsPage extends Page { await fetch(new URL("cpus", baseUrl)) .then((res) => res.json()) .then(({ physical, logical }) => { - dandiSchema.properties.number_of_jobs.max = physical; dandiSchema.properties.number_of_threads.max = logical / physical; })