diff --git a/pyflask/app.py b/pyflask/app.py
index 977dae0f3..ea666c0ce 100644
--- a/pyflask/app.py
+++ b/pyflask/app.py
@@ -105,6 +105,13 @@ def get_cpu_count():
return dict(physical=physical, logical=logical)
+@app.route("/get-recommended-species")
+def get_species():
+ from dandi.metadata import species_map
+
+ return species_map
+
+
@api.route("/server_shutdown", endpoint="shutdown")
class Shutdown(Resource):
def get(self):
diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts
index 372979e22..8cd5c7912 100644
--- a/schemas/base-metadata.schema.ts
+++ b/schemas/base-metadata.schema.ts
@@ -1,23 +1,90 @@
+import { serverGlobals, resolve } from '../src/renderer/src/server/globals'
+
+import { header } from '../src/renderer/src/stories/forms/utils'
+
import baseMetadataSchema from './json/base_metadata_schema.json' assert { type: "json" }
-export const preprocessMetadataSchema = (schema: any = baseMetadataSchema) => {
+function getSpeciesNameComponents(arr: any[]) {
+ const split = arr[arr.length - 1].split(' - ')
+ return {
+ name: split[0],
+ label: split[1]
+ }
+}
+
+
+function getSpeciesInfo(species: any[][] = []) {
+
+
+ return {
+
+ enumLabels: species.reduce((acc, arr) => {
+ acc[getSpeciesNameComponents(arr).name] = arr[arr.length - 1]
+ return acc
+ }, {}),
+
+ enumKeywords: species.reduce((acc, arr) => {
+ const info = getSpeciesNameComponents(arr)
+ acc[info.name] = info.label ? [`${header(info.label)} — ${arr[0].join(', ')}`] : arr[0]
+ return acc
+ }, {}),
+
+ enum: species.map(arr => getSpeciesNameComponents(arr).name), // Remove common names so this passes the validator
+ }
+
+}
+
+export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, global = false) => {
+
+
+ const copy = structuredClone(schema)
// Add unit to weight
- schema.properties.Subject.properties.weight.unit = 'kg'
+ const subjectProps = copy.properties.Subject.properties
+ subjectProps.weight.unit = 'kg'
+
+ // subjectProps.order = ['weight', 'age', 'age__reference', 'date_of_birth', 'genotype', 'strain']
- schema.properties.Subject.properties.sex.enumLabels = {
+ subjectProps.sex.enumLabels = {
M: 'Male',
F: 'Female',
U: 'Unknown',
O: 'Other'
}
+
+ subjectProps.species = {
+ type: 'string',
+ ...getSpeciesInfo(),
+ items: {
+ type: 'string'
+ },
+ strict: false,
+ description: 'The species of your subject.'
+ }
+
+ // Resolve species suggestions
+ resolve(serverGlobals.species, (res) => {
+ const info = getSpeciesInfo(res)
+ Object.assign(subjectProps.species, info)
+ })
+
// Ensure experimenter schema has custom structure
- schema.properties.NWBFile.properties.experimenter = baseMetadataSchema.properties.NWBFile.properties.experimenter
+ copy.properties.NWBFile.properties.experimenter = baseMetadataSchema.properties.NWBFile.properties.experimenter
// Override description of keywords
- schema.properties.NWBFile.properties.keywords.description = 'Terms to describe your dataset (e.g. Neural circuits, V1, etc.)' // Add description to keywords
- return schema
+ copy.properties.NWBFile.properties.keywords.description = 'Terms to describe your dataset (e.g. Neural circuits, V1, etc.)' // Add description to keywords
+
+
+
+ // Remove non-global properties
+ if (global) {
+ Object.entries(copy.properties).forEach(([globalProp, schema]) => {
+ instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]);
+ });
+ }
+
+ return copy
}
@@ -39,13 +106,7 @@ export const instanceSpecificFields = {
};
-const globalSchema = structuredClone(preprocessMetadataSchema());
-Object.entries(globalSchema.properties).forEach(([globalProp, schema]) => {
- instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]);
-});
+export const globalSchema = preprocessMetadataSchema(undefined, true);
-export {
- globalSchema
-}
export default preprocessMetadataSchema()
diff --git a/schemas/subject.schema.ts b/schemas/subject.schema.ts
index 2aa8b0f26..36c2db437 100644
--- a/schemas/subject.schema.ts
+++ b/schemas/subject.schema.ts
@@ -1,4 +1,4 @@
-import nwbBaseSchema from './base-metadata.schema'
+import { preprocessMetadataSchema } from './base-metadata.schema'
const removeSubset = (data, subset) => {
const subsetData = subset.reduce((acc, key) => { acc[key] = data[key]; return acc }, {})
@@ -6,39 +6,22 @@ const removeSubset = (data, subset) => {
return subsetData
}
- const species = [
- "Mus musculus - House mouse",
- "Homo sapiens - Human",
- "Rattus norvegicus - Norway rat",
- "Rattus rattus - Black rat",
- "Macaca mulatta - Rhesus monkey",
- "Callithrix jacchus - Common marmoset",
- "Drosophila melanogaster - Fruit fly",
- "Danio rerio - Zebra fish",
- "Caenorhabditis elegans"
- ].map(str => str.split(' - ')[0]) // Remove common names so this passes the validator
+export default (schema) => {
-nwbBaseSchema.properties.Subject.properties.species = {
- type: 'string',
- enum: species,
- items: {
- type: 'string'
- },
- strict: false,
- description: 'The species of your subject.'
-}
+ const nwbBaseSchema = preprocessMetadataSchema(schema)
-// Sort the subject schema
-const ageGroupKeys = ['age', 'age__reference', 'date_of_birth']
-const genotypeGroupKeys = ['genotype', 'strain']
-const groups = [...ageGroupKeys, ...genotypeGroupKeys]
-const standardOrder = {...nwbBaseSchema.properties.Subject.properties}
-const group = removeSubset(standardOrder, groups)
-const required = removeSubset(standardOrder, nwbBaseSchema.properties.Subject.required)
+ // Sort the subject schema
+ const ageGroupKeys = ['age', 'age__reference', 'date_of_birth']
+ const genotypeGroupKeys = ['genotype', 'strain']
+ const groups = [...ageGroupKeys, ...genotypeGroupKeys]
+ const standardOrder = {...nwbBaseSchema.properties.Subject.properties}
+ const group = removeSubset(standardOrder, groups)
+ const required = removeSubset(standardOrder, nwbBaseSchema.properties.Subject.required)
-let newRequiredArray = [...nwbBaseSchema.properties.Subject.required, 'sessions']
+ let newRequiredArray = [...nwbBaseSchema.properties.Subject.required, 'sessions']
-export default {
+
+ return {
...nwbBaseSchema.properties.Subject,
properties: {
sessions: {
@@ -53,3 +36,4 @@ export default {
},
required: newRequiredArray
}
+}
diff --git a/src/renderer/src/globals.js b/src/renderer/src/globals.js
index b235577dd..e9d98b013 100644
--- a/src/renderer/src/globals.js
+++ b/src/renderer/src/globals.js
@@ -1,4 +1,4 @@
-import { path, port } from "./electron/index.js";
+import { path } from "./electron/index.js";
import guideGlobalMetadata from "../../../guideGlobalMetadata.json" assert { type: "json" };
@@ -9,7 +9,4 @@ export let runOnLoad = (fn) => {
else window.addEventListener("load", fn);
};
-// Base Request URL for Python Server
-export const baseUrl = `http://127.0.0.1:${port}`;
-
export const supportedInterfaces = guideGlobalMetadata.supported_interfaces;
diff --git a/src/renderer/src/index.ts b/src/renderer/src/index.ts
index 7d25747a0..7cc801eac 100644
--- a/src/renderer/src/index.ts
+++ b/src/renderer/src/index.ts
@@ -10,7 +10,9 @@ import {
} from './dependencies/globals.js'
import Swal from 'sweetalert2'
-import { loadServerEvents, pythonServerOpened, statusBar } from "./server.js";
+import { loadServerEvents, pythonServerOpened } from "./server/index.js";
+
+import { statusBar } from "./server/globals.js";
// Set the sidebar subtitle to the current app version
const dashboard = document.querySelector('nwb-dashboard') as Dashboard
diff --git a/src/renderer/src/server/globals.ts b/src/renderer/src/server/globals.ts
new file mode 100644
index 000000000..7570a881e
--- /dev/null
+++ b/src/renderer/src/server/globals.ts
@@ -0,0 +1,79 @@
+import { isElectron, app, port } from '../electron/index.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";
+
+// Base Request URL for Python Server
+export const baseUrl = `http://127.0.0.1:${port}`;
+
+const isPromise = (o) => typeof o === 'object' && typeof o.then === 'function'
+
+export const resolve = (object, callback) => {
+ if (isPromise(object)) {
+ return new Promise(resolvePromise => {
+ object.then((res) => resolvePromise((callback) ? callback(res) : res))
+ })
+ } else return (callback) ? callback(object) : object
+}
+
+// -------------------------------------------------
+
+import { StatusBar } from "../stories/status/StatusBar.js";
+import { unsafeSVG } from "lit/directives/unsafe-svg.js";
+
+const appVersion = app?.getVersion();
+
+export const statusBar = new StatusBar({
+ items: [
+ { label: unsafeSVG(webAssetSVG), value: isElectron ? appVersion ?? 'ERROR' : 'Web' },
+ { label: unsafeSVG(wifiSVG) },
+ { label: unsafeSVG(serverSVG) }
+ ]
+})
+
+
+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 const activateServer = () => {
+ statusBar.items[2].status = true
+
+ serverCallbacks.forEach(cb => cb())
+ serverCallbacks = []
+}
+
+export const serverGlobals = {
+ species: new Promise((res, rej) => {
+ onServerOpen(() => {
+ fetch(new URL("get-recommended-species", baseUrl))
+ .then((res) => res.json())
+ .then((species) => {
+ res(species)
+ serverGlobals.species = species
+ })
+ .catch(() => rej());
+ });
+ }),
+ cpus: new Promise((res, rej) => {
+ onServerOpen(() => {
+ fetch(new URL("cpus", baseUrl))
+ .then((res) => res.json())
+ .then((cpus) => {
+ res(cpus)
+ serverGlobals.cpus = cpus
+ })
+ .catch(() => rej());
+ });
+ })
+ }
diff --git a/src/renderer/src/server.ts b/src/renderer/src/server/index.ts
similarity index 71%
rename from src/renderer/src/server.ts
rename to src/renderer/src/server/index.ts
index d0add2853..e2dae2fcb 100644
--- a/src/renderer/src/server.ts
+++ b/src/renderer/src/server/index.ts
@@ -1,31 +1,13 @@
-import { isElectron, electron, app } from './electron/index.js'
+import { isElectron, electron, app, port } from '../electron/index.js'
const { ipcRenderer } = electron;
import {
notyf,
-} from './dependencies/globals.js'
-
-import { baseUrl } from './globals.js'
+} from '../dependencies/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) }
- ]
-})
-
+import { activateServer, baseUrl } from './globals.js';
// Check if the Flask server is live
const serverIsLiveStartup = async () => {
@@ -39,27 +21,6 @@ const serverIsLiveStartup = async () => {
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 const activateServer = () => {
- statusBar.items[2].status = true
-
- serverCallbacks.forEach(cb => cb())
- serverCallbacks = []
-}
-
export async function pythonServerOpened() {
// Confirm requests are actually received by the server
diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js
index 259f0644f..3fe751608 100644
--- a/src/renderer/src/stories/JSONSchemaForm.js
+++ b/src/renderer/src/stories/JSONSchemaForm.js
@@ -376,7 +376,7 @@ export class JSONSchemaForm extends LitElement {
let message = isValid
? ""
: requiredButNotSpecified.length === 1
- ? `${requiredButNotSpecified[0]} is not defined`
+ ? `${header(requiredButNotSpecified[0])} is not defined`
: `${requiredButNotSpecified.length} required inputs are not specified properly`;
if (requiredButNotSpecified.length !== nMissingRequired)
console.warn("Disagreement about the correct error to throw...");
@@ -387,7 +387,6 @@ export class JSONSchemaForm extends LitElement {
if (flaggedInputs.length) {
flaggedInputs[0].focus();
if (!message) {
- console.log(flaggedInputs);
if (flaggedInputs.length === 1)
message = `${header(flaggedInputs[0].path.join("."))} is not valid`;
else message = `${flaggedInputs.length} invalid form values`;
diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js
index 97ea5729e..176fddd34 100644
--- a/src/renderer/src/stories/JSONSchemaInput.js
+++ b/src/renderer/src/stories/JSONSchemaInput.js
@@ -11,6 +11,7 @@ import { Modal } from "./Modal";
import { capitalize } from "./forms/utils";
import { JSONSchemaForm } from "./JSONSchemaForm";
+import { Search } from "./Search";
const isFilesystemSelector = (name, format) => {
if (Array.isArray(format)) return format.map((f) => isFilesystemSelector(name, f)).every(Boolean) ? format : null;
@@ -111,7 +112,8 @@ export class JSONSchemaInput extends LitElement {
// onValidate = () => {}
updateData(value, forceValidate = false) {
- if (this.value === value && !forceValidate) {
+ if (this.value !== value && !forceValidate) {
+ // Update the actual input element
const el = this.getElement();
if (el.type === "checkbox") el.checked = value;
else if (el.classList.contains("list"))
@@ -120,6 +122,7 @@ export class JSONSchemaInput extends LitElement {
return { value };
})
: [];
+ else if (el instanceof Search) el.shadowRoot.querySelector("input").value = value;
else el.value = value;
}
@@ -356,7 +359,33 @@ export class JSONSchemaInput extends LitElement {
}
// Basic enumeration of properties on a select element
- if (info.enum) {
+ if (info.enum && info.enum.length) {
+ if (info.strict === false) {
+ // const category = categories.find(({ test }) => test.test(key))?.value;
+
+ const options = info.enum.map((v) => {
+ return {
+ key: v,
+ keywords: info.enumKeywords?.[v],
+ };
+ });
+
+ const search = new Search({
+ options,
+ value: this.value,
+ showAllWhenEmpty: false,
+ listMode: "click",
+ onSelect: async ({ value, key }) => {
+ const result = value ?? key;
+ this.#updateData(fullPath, result);
+ if (validateOnChange) await this.#triggerValidation(name, path);
+ },
+ });
+
+ search.classList.add("schema-input");
+ return search;
+ }
+
return html`