Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create loose selector (autocomplete) for JSONSchemaForm #513

Merged
merged 27 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f2d0a8a
Move species enumeration (non-strict) into the base
garrettmflynn Nov 12, 2023
095620d
Basic dropdown using the search component
garrettmflynn Nov 15, 2023
61dfb8c
Merge branch 'main' into loose-select-dropdown
CodyCBakerPhD Nov 15, 2023
ad3e700
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 15, 2023
9ebfc36
Merge branch 'main' into loose-select-dropdown
CodyCBakerPhD Nov 15, 2023
e922792
Merge branch 'main' into loose-select-dropdown
CodyCBakerPhD Nov 15, 2023
a3ba23f
Make search and table behave consistently. Add icon
garrettmflynn Nov 15, 2023
395c20f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 15, 2023
a754ee6
Fix tests
garrettmflynn Nov 16, 2023
29ab587
Merge branch 'loose-select-dropdown' of https://github.com/NeurodataW…
garrettmflynn Nov 16, 2023
4279766
Fix imports
garrettmflynn Nov 16, 2023
e71d3dd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2023
a9ce966
Merge branch 'main' into loose-select-dropdown
CodyCBakerPhD Nov 21, 2023
0f8a230
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2023
0a4fc31
Merge branch 'main' into loose-select-dropdown
CodyCBakerPhD Nov 21, 2023
4dfda5a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2023
bc6869b
Update value and rerender
garrettmflynn Nov 21, 2023
7c37218
Allow for setting value on input directly
garrettmflynn Nov 21, 2023
893bda6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2023
5c7a24d
Properly set input
garrettmflynn Nov 21, 2023
4b7dff2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2023
c279d62
Ensure global properties can be removed using the preprocess script
garrettmflynn Nov 21, 2023
dd307d7
Merge branch 'loose-select-dropdown' of https://github.com/NeurodataW…
garrettmflynn Nov 21, 2023
ec587eb
Use globals when necessary
garrettmflynn Nov 21, 2023
728f975
Update storyStates.ts
garrettmflynn Nov 22, 2023
b67dbd7
Merge branch 'main' into loose-select-dropdown
garrettmflynn Nov 22, 2023
118c978
Merge branch 'main' into loose-select-dropdown
CodyCBakerPhD Nov 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading