Skip to content

Commit

Permalink
Merge pull request #513 from NeurodataWithoutBorders/loose-select-dro…
Browse files Browse the repository at this point in the history
…pdown

Create loose selector (autocomplete) for JSONSchemaForm
  • Loading branch information
CodyCBakerPhD authored Nov 22, 2023
2 parents 2e94f79 + 118c978 commit 5805725
Show file tree
Hide file tree
Showing 27 changed files with 394 additions and 179 deletions.
7 changes: 7 additions & 0 deletions pyflask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
87 changes: 74 additions & 13 deletions schemas/base-metadata.schema.ts
Original file line number Diff line number Diff line change
@@ -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

}

Expand All @@ -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()
44 changes: 14 additions & 30 deletions schemas/subject.schema.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,27 @@
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 }, {})
for (let key in subsetData) delete data[key]
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: {
Expand All @@ -53,3 +36,4 @@ export default {
},
required: newRequiredArray
}
}
5 changes: 1 addition & 4 deletions src/renderer/src/globals.js
Original file line number Diff line number Diff line change
@@ -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" };

Expand All @@ -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;
4 changes: 3 additions & 1 deletion src/renderer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions src/renderer/src/server/globals.ts
Original file line number Diff line number Diff line change
@@ -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());
});
})
}
45 changes: 3 additions & 42 deletions src/renderer/src/server.ts → src/renderer/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/renderer/src/stories/JSONSchemaForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export class JSONSchemaForm extends LitElement {
let message = isValid
? ""
: requiredButNotSpecified.length === 1
? `<b>${requiredButNotSpecified[0]}</b> is not defined`
? `<b>${header(requiredButNotSpecified[0])}</b> is not defined`
: `${requiredButNotSpecified.length} required inputs are not specified properly`;
if (requiredButNotSpecified.length !== nMissingRequired)
console.warn("Disagreement about the correct error to throw...");
Expand All @@ -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 = `<b>${header(flaggedInputs[0].path.join("."))}</b> is not valid`;
else message = `${flaggedInputs.length} invalid form values`;
Expand Down
Loading

0 comments on commit 5805725

Please sign in to comment.