-
${this.#selected}
+
${this.controls.map((o) => {
return html`
{
return html`
- ${this.renderInstance(item.id, item.metadata)}
+ ${this.getInstanceContent(item)}
`;
})}
diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js
index 4ecd4447f..abb1918d1 100644
--- a/src/renderer/src/stories/JSONSchemaForm.js
+++ b/src/renderer/src/stories/JSONSchemaForm.js
@@ -8,6 +8,7 @@ import { merge } from "./pages/utils";
import { resolveProperties } from "./pages/guided-mode/data/utils";
import { JSONSchemaInput } from "./JSONSchemaInput";
+import { InspectorListItem } from "./preview/inspector/InspectorList";
const componentCSS = `
@@ -29,30 +30,6 @@ const componentCSS = `
background: rgb(255, 229, 228) !important;
}
- .errors {
- color: #9d0b0b;
- }
-
- .errors > * {
- padding: 25px;
- background: #f8d7da;
- border: 1px solid #f5c2c7;
- border-radius: 4px;
- margin: 0 0 1em;
- }
-
- .warnings {
- color: #856404;
- }
-
- .warnings > * {
- padding: 25px;
- background: #fff3cd;
- border: 1px solid #ffeeba;
- border-radius: 4px;
- margin: 0 0 1em;
- }
-
.guided--form-label {
display: block;
width: 100%;
@@ -277,9 +254,8 @@ export class JSONSchemaForm extends LitElement {
#addMessage = (name, message, type) => {
if (Array.isArray(name)) name = name.join("-"); // Convert array to string
const container = this.shadowRoot.querySelector(`#${name} .${type}`);
- const p = document.createElement("p");
- p.innerText = message;
- container.appendChild(p);
+ const item = new InspectorListItem(message);
+ container.appendChild(item);
};
#clearMessages = (fullPath, type) => {
@@ -625,7 +601,7 @@ export class JSONSchemaForm extends LitElement {
this.checkStatus();
// Show aggregated errors and warnings (if any)
- warnings.forEach((info) => this.#addMessage(fullPath, info.message, "warnings"));
+ warnings.forEach((info) => this.#addMessage(fullPath, info, "warnings"));
const isFunction = typeof valid === "function";
@@ -658,7 +634,7 @@ export class JSONSchemaForm extends LitElement {
[...path, name]
);
- errors.forEach((info) => this.#addMessage(fullPath, info.message, "errors"));
+ errors.forEach((info) => this.#addMessage(fullPath, info, "errors"));
// element.title = errors.map((info) => info.message).join("\n"); // Set all errors to show on hover
return false;
diff --git a/src/renderer/src/stories/List.stories.js b/src/renderer/src/stories/List.stories.js
index 36fc9152b..718492ef9 100644
--- a/src/renderer/src/stories/List.stories.js
+++ b/src/renderer/src/stories/List.stories.js
@@ -6,9 +6,11 @@ export default {
const Template = (args) => new List(args);
+const generateString = () => Math.floor(Math.random() * Date.now()).toString(36);
+
export const Default = Template.bind({});
Default.args = {
- items: [{ value: "test" }],
+ items: [{ value: "test" }, { value: Array.from({ length: 1000 }).map(generateString).join("") }],
};
export const WithKeys = Template.bind({});
diff --git a/src/renderer/src/stories/List.ts b/src/renderer/src/stories/List.ts
index 7a6a3bb21..69c57b1b8 100644
--- a/src/renderer/src/stories/List.ts
+++ b/src/renderer/src/stories/List.ts
@@ -1,17 +1,20 @@
import { LitElement, html, css } from 'lit';
import { Button } from './Button'
-import { empty } from 'handsontable/helpers/dom';
+import { styleMap } from "lit/directives/style-map.js";
type ListItemType = {
key: string,
- label: string,
+ content: string,
value: any,
}
export interface ListProps {
onChange?: () => void;
items?: ListItemType[]
- emptyMessage?: string
+ emptyMessage?: string,
+ editable?: boolean,
+ unordered?: boolean,
+ listStyles?: any
}
export class List extends LitElement {
@@ -19,16 +22,15 @@ export class List extends LitElement {
static get styles() {
return css`
+ :host {
+ overflow: auto;
+ }
+
#empty {
padding: 20px 10px;
color: gray;
}
- li > div {
- display: flex;
- align-items: center;
- }
-
li {
padding-bottom: 10px;
}
@@ -37,19 +39,35 @@ export class List extends LitElement {
padding-bottom: 0px;
}
+ li > div {
+ display: flex;
+ align-items: center;
+ }
+
li > div > div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
}
- :host([keys]) ol {
- list-style-type:none;
+ :host([unordered]) ol {
+ list-style-type: none;
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0;
+ padding: 0;
}
- :host([keys]) ol > li > div {
+ :host([unordered]) ol > li {
+ width: 100%;
+ }
+
+ :host([unordered]) ol > li {
justify-content: center;
+ display: flex;
+ align-items: center;
}
+
`;
}
@@ -57,12 +75,22 @@ export class List extends LitElement {
return {
items: {
type: Array,
- reflect: true
- },
- emptyMessage: {
- type: String,
- reflect: true
- }
+ // reflect: true // NOTE: Cannot reflect items since they could include an element
+ },
+
+ editable: {
+ type: Boolean,
+ reflect: true
+ },
+
+ unordered: {
+ type: Boolean,
+ // reflect: true
+ },
+ emptyMessage: {
+ type: String,
+ reflect: true
+ }
};
}
@@ -74,22 +102,36 @@ export class List extends LitElement {
return this.items.map(o => o.value)
}
- declare items: ListItemType[]
+ #items: ListItemType[] = []
+ set items(value) {
+ const oldVal = this.#items
+ this.#items = value
+ this.onChange()
+ this.requestUpdate('items', oldVal)
+ }
+
+ get items() { return this.#items }
+
declare emptyMessage: string
+ declare editable: boolean
+ declare unordered: boolean
+
+ declare listStyles: any
+
+
constructor(props: ListProps = {}) {
super();
this.items = props.items ?? []
this.emptyMessage = props.emptyMessage ?? ''
- if (props.onChange) this.onChange = props.onChange
+ this.editable = props.editable ?? true
+ this.unordered = props.unordered ?? false
+ this.listStyles = props.listStyles ?? {}
- }
+ if (props.onChange) this.onChange = props.onChange
- attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
- super.attributeChangedCallback(name, _old, value)
- if (name === 'items') this.onChange()
}
add = (item: ListItemType) => {
@@ -97,13 +139,13 @@ export class List extends LitElement {
}
#renderListItem = (item: ListItemType, i: number) => {
- const { key, value, label = value } = item;
+ const { key, value, content = value } = item;
const li = document.createElement("li");
- const div = document.createElement('div')
- li.append(div)
- const innerDiv = document.createElement('div')
- div.append(innerDiv)
+ const outerDiv = document.createElement('div')
+ const div = document.createElement('div')
+ li.append(outerDiv)
+ outerDiv.append(div)
const keyEl = document.createElement("span");
@@ -112,7 +154,7 @@ export class List extends LitElement {
if (key) {
- this.setAttribute('keys', '')
+ this.setAttribute('unordered', '')
// Ensure no duplicate keys
let i = 0;
@@ -124,51 +166,58 @@ export class List extends LitElement {
keyEl.innerText = resolvedKey;
keyEl.contentEditable = true;
keyEl.style.cursor = "text";
- innerDiv.appendChild(keyEl);
const sepEl = document.createElement("span");
sepEl.innerHTML = " - ";
- innerDiv.appendChild(sepEl);
+ div.append(keyEl, sepEl);
this.object[resolvedKey] = value;
} else this.object[i] = value;
- const valueEl = document.createElement("span");
- valueEl.innerText = label;
- innerDiv.appendChild(valueEl);
+ if (typeof content === 'string') {
+ const valueEl = document.createElement("span");
+ valueEl.innerText = content;
+ div.appendChild(valueEl);
+ }
+ else li.append(content)
+
- const button = new Button({
- label: "Delete",
- size: "small",
- });
- button.style.marginLeft = "1rem";
+ if (div.innerText) li.title = div.innerText
- div.appendChild(button);
- // Stop enter key from creating new line
- keyEl.addEventListener("keydown", function (e) {
- if (e.keyCode === 13) {
- keyEl.blur();
- return false;
- }
- });
+ if (this.editable) {
+ const button = new Button({
+ label: "Delete",
+ size: "small",
+ });
- const deleteListItem = () => this.delete(i);
+ button.style.marginLeft = "1rem";
- keyEl.addEventListener("blur", () => {
- const newKey = keyEl.innerText;
- if (newKey === "") keyEl.innerText = resolvedKey; // Reset to original value
- else {
- delete this.object[resolvedKey];
- resolvedKey = newKey;
- this.object[resolvedKey] = value;
- }
- });
+ outerDiv.appendChild(button);
- button.onClick = deleteListItem;
+ // Stop enter key from creating new line
+ keyEl.addEventListener("keydown", function (e) {
+ if (e.keyCode === 13) {
+ keyEl.blur();
+ return false;
+ }
+ });
- innerDiv.title = innerDiv.innerText
+ const deleteListItem = () => this.delete(i);
+
+ keyEl.addEventListener("blur", () => {
+ const newKey = keyEl.innerText;
+ if (newKey === "") keyEl.innerText = resolvedKey; // Reset to original value
+ else {
+ delete this.object[resolvedKey];
+ resolvedKey = newKey;
+ this.object[resolvedKey] = value;
+ }
+ });
+
+ button.onClick = deleteListItem;
+ }
return li
};
@@ -184,13 +233,15 @@ export class List extends LitElement {
render() {
- this.removeAttribute('keys')
+ this.removeAttribute('unordered')
+ if (this.unordered) this.setAttribute('unordered', '')
+
this.object = {}
const { items, emptyMessage} = this
return items.length || !emptyMessage ? html`
-
+
${items.map(this.#renderListItem)}
` : html`${emptyMessage}
`
}
diff --git a/src/renderer/src/stories/Loader.ts b/src/renderer/src/stories/Loader.ts
index 9e0b93062..97e3febf9 100644
--- a/src/renderer/src/stories/Loader.ts
+++ b/src/renderer/src/stories/Loader.ts
@@ -10,6 +10,19 @@ export class Loader extends LitElement {
:host {
display: block;
}
+
+ span {
+ font-size: 90%;
+ padding-left: 10px;
+ }
+
+ :host > div {
+ display: flex;
+ align-items: center;
+ justif-content: center;
+ }
+
+
.lds-default {
display: inline-block;
position: relative;
@@ -96,12 +109,20 @@ export class Loader extends LitElement {
`
}
- constructor(){
+ declare message: string
+
+ constructor(props: any){
super()
+ Object.assign(this, props)
}
render() {
- return html` `
+ return html`
+
+
+ ${this.message ? html`
${this.message}` : ''}
+
+ `
}
}
diff --git a/src/renderer/src/stories/Neurosift.js b/src/renderer/src/stories/Neurosift.js
deleted file mode 100644
index 1080a04e9..000000000
--- a/src/renderer/src/stories/Neurosift.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { LitElement, css, html } from "lit";
-import { baseUrl } from "../globals";
-
-export function getURLFromFilePath(file, projectName) {
- const regexp = new RegExp(`.+(${projectName}.+)`);
- return `${baseUrl}/stubs/${file.match(regexp)[1]}`;
-}
-
-export class Neurosift extends LitElement {
- static get styles() {
- return css`
- iframe {
- width: 100%;
- height: 100%;
- border: 0;
- }
- `;
- }
-
- constructor({ url }) {
- super();
- this.url = url;
- }
-
- render() {
- return html``;
- }
-}
-
-customElements.get("neurosift-iframe") || customElements.define("neurosift-iframe", Neurosift);
diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
index 4cda34c94..fc65953bd 100644
--- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
+++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js
@@ -10,7 +10,7 @@ import Swal from "sweetalert2";
import { SimpleTable } from "../../../SimpleTable.js";
import { onThrow } from "../../../../errors";
import { merge } from "../../utils.js";
-import { Neurosift, getURLFromFilePath } from "../../../Neurosift.js";
+import { NWBFilePreview } from "../../../preview/NWBFilePreview.js";
const getInfoFromId = (key) => {
let [subject, session] = key.split("/");
@@ -39,11 +39,12 @@ export class GuidedMetadataPage extends ManagedPage {
// Preview a single random conversion
delete this.info.globalState.stubs; // Clear the preview results
- const results = await this.runConversions({ stub_test: true }, undefined, {
+ const stubs = await this.runConversions({ stub_test: true }, undefined, {
title: "Running stub conversion on all sessions...",
});
- this.info.globalState.stubs = results; // Save the preview results
+ // Save the preview results
+ this.info.globalState.stubs = stubs;
this.unsavedUpdates = true;
@@ -199,11 +200,6 @@ export class GuidedMetadataPage extends ManagedPage {
throw e;
});
- const firstSubject = Object.values(results)[0];
- const file = Object.values(firstSubject)[0]; // Get the information from the first subject
-
- console.log(firstSubject, file, results);
-
const modal = new Modal({
header: `Conversion Preview: ${key}`,
open: true,
@@ -212,9 +208,9 @@ export class GuidedMetadataPage extends ManagedPage {
height: "100%",
});
- modal.append(
- new Neurosift({ url: getURLFromFilePath(file, this.info.globalState.project.name) })
- );
+ const { project } = this.info.globalState;
+
+ modal.append(new NWBFilePreview({ project: project.name, files: results }));
document.body.append(modal);
},
},
diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js
index cdab8f1b0..a3f60670e 100644
--- a/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js
+++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js
@@ -15,11 +15,11 @@ export class GuidedConversionOptionsPage extends Page {
await this.form.validate(); // Will throw an error in the callback
// Preview a random conversion
- delete this.info.globalState.preview; // Clear the preview results
+ delete this.info.globalState.stubs; // Clear the preview results
const results = await this.runConversions({ stub_test: true }, 1, {
title: "Testing conversion on a random session",
});
- this.info.globalState.preview = results[0]; // Save the preview results
+ this.info.globalState.stubs = results; // Save the preview results
this.to(1);
},
diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedStubPreview.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedStubPreview.js
index 55780c2d2..b39a1e91b 100644
--- a/src/renderer/src/stories/pages/guided-mode/options/GuidedStubPreview.js
+++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedStubPreview.js
@@ -5,22 +5,30 @@ import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import folderOpenSVG from "../../../assets/folder_open.svg?raw";
import { electron } from "../../../../electron/index.js";
-import { Neurosift, getURLFromFilePath } from "../../../Neurosift.js";
-import { InstanceManager } from "../../../InstanceManager.js";
-
+import { NWBFilePreview, getSharedPath } from "../../../preview/NWBFilePreview.js";
const { shell } = electron;
+const getStubArray = (stubs) =>
+ Object.values(stubs)
+ .map((o) => Object.values(o))
+ .flat();
+
export class GuidedStubPreviewPage extends Page {
constructor(...args) {
super(...args);
}
header = {
- subtitle: () => this.info.globalState.stubs[0],
+ subtitle: () => `${getStubArray(this.info.globalState.stubs).length} Files`,
controls: () =>
html` (shell ? shell.showItemInFolder(this.info.globalState.stubs[0]) : "")}
+ @click=${() =>
+ shell
+ ? shell.showItemInFolder(
+ getSharedPath(getStubArray(this.info.globalState.stubs).map((o) => o.file))
+ )
+ : ""}
>${unsafeSVG(folderOpenSVG)}`,
};
@@ -38,33 +46,11 @@ export class GuidedStubPreviewPage extends Page {
},
};
- createInstance = ({ subject, session, info }) => {
- const { project, stubs } = this.info.globalState;
-
- return {
- subject,
- session,
- display: new Neurosift({ url: getURLFromFilePath(stubs[subject][session], project.name) }),
- };
- };
-
render() {
- const { stubs } = this.info.globalState;
-
- const _instances = this.mapSessions(this.createInstance);
-
- const instances = _instances.reduce((acc, { subject, session, display }) => {
- if (!acc[`sub-${subject}`]) acc[`sub-${subject}`] = {};
- acc[`sub-${subject}`][`ses-${session}`] = display;
- return acc;
- }, {});
+ const { stubs, project } = this.info.globalState;
return stubs
- ? (this.manager = new InstanceManager({
- header: "Sessions",
- instanceType: "Session",
- instances,
- }))
+ ? new NWBFilePreview({ project: project.name, files: stubs })
: html`Your conversion preview failed. Please try again.
`;
}
}
diff --git a/src/renderer/src/stories/pages/guided-mode/options/utils.js b/src/renderer/src/stories/pages/guided-mode/options/utils.js
index deda08db6..6d1d0020e 100644
--- a/src/renderer/src/stories/pages/guided-mode/options/utils.js
+++ b/src/renderer/src/stories/pages/guided-mode/options/utils.js
@@ -21,7 +21,7 @@ export const openProgressSwal = (options) => {
};
export const run = async (url, payload, options = {}) => {
- const needsSwal = !options.swal;
+ const needsSwal = !options.swal && options.swal !== false;
if (needsSwal) openProgressSwal(options).then((swal) => (options.onOpen ? options.onOpen(swal) : undefined));
const results = await fetch(`${baseUrl}/neuroconv/${url}`, {
diff --git a/src/renderer/src/stories/pages/guided-mode/results/GuidedResults.js b/src/renderer/src/stories/pages/guided-mode/results/GuidedResults.js
index f0a97aed5..fc3d64df2 100644
--- a/src/renderer/src/stories/pages/guided-mode/results/GuidedResults.js
+++ b/src/renderer/src/stories/pages/guided-mode/results/GuidedResults.js
@@ -93,7 +93,8 @@ export class GuidedResultsPage extends Page {
${Object.values(results)
- .flat((v) => Object.values(v))
+ .map((v) => Object.values(v))
+ .flat()
.map((o) => html`- ${o.file}
`)}
diff --git a/src/renderer/src/stories/preview/NWBFilePreview.js b/src/renderer/src/stories/preview/NWBFilePreview.js
new file mode 100644
index 000000000..b752f9917
--- /dev/null
+++ b/src/renderer/src/stories/preview/NWBFilePreview.js
@@ -0,0 +1,149 @@
+import { LitElement, css, html } from "lit";
+import { InspectorList } from "./inspector/InspectorList";
+import { Neurosift, getURLFromFilePath } from "./Neurosift";
+import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { run } from "../pages/guided-mode/options/utils";
+import { until } from "lit/directives/until.js";
+import { InstanceManager } from "../InstanceManager";
+import { path } from "../../electron";
+
+export function getSharedPath(array) {
+ array = array.map((str) => str.replace(/\\/g, "/")); // Convert to Mac-style path
+ const mapped = array.map((str) => str.split("/"));
+ let shared = mapped.shift();
+ mapped.forEach((arr, i) => {
+ for (let j in arr) {
+ if (arr[j] !== shared[j]) {
+ shared = shared.slice(0, j);
+ break;
+ }
+ }
+ });
+
+ return path.normalize(shared.join("/")); // Convert back to OS-specific path
+}
+
+class NWBPreviewInstance extends LitElement {
+ constructor({ file }, project) {
+ super();
+ this.file = file;
+ this.project = project;
+
+ window.addEventListener("online", () => this.requestUpdate());
+ window.addEventListener("offline", () => this.requestUpdate());
+ }
+
+ render() {
+ const isOnline = navigator.onLine;
+
+ return isOnline
+ ? new Neurosift({ url: getURLFromFilePath(this.file, this.project) })
+ : until(
+ (async () => {
+ const htmlRep = await run("html", { nwbfile_path: this.file }, { swal: false });
+ return unsafeHTML(htmlRep);
+ })(),
+ html`
Loading HTML representation...`
+ );
+ }
+}
+
+customElements.get("nwb-preview-instance") || customElements.define("nwb-preview-instance", NWBPreviewInstance);
+
+export class NWBFilePreview extends LitElement {
+ static get styles() {
+ return css`
+ iframe {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ }
+ `;
+ }
+
+ constructor({ files = {}, project }) {
+ super();
+ this.project = project;
+ this.files = files;
+ }
+
+ createInstance = ({ subject, session, info }) => {
+ return {
+ subject,
+ session,
+ display: () => new NWBPreviewInstance(info, this.project),
+ };
+ };
+
+ render() {
+ const fileArr = Object.entries(this.files)
+ .map(([subject, v]) =>
+ Object.entries(v).map(([session, info]) => {
+ return { subject, session, info };
+ })
+ )
+ .flat();
+
+ const onlyFirstFile = fileArr.length <= 1;
+
+ return html`
+
+ ${(() => {
+ if (onlyFirstFile) return new NWBPreviewInstance(fileArr[0].info, this.project);
+ else {
+ const _instances = fileArr.map(this.createInstance);
+
+ const instances = _instances.reduce((acc, { subject, session, display }) => {
+ if (!acc[`sub-${subject}`]) acc[`sub-${subject}`] = {};
+ acc[`sub-${subject}`][`ses-${session}`] = display;
+ return acc;
+ }, {});
+
+ return new InstanceManager({
+ header: "Stub Files",
+ instances,
+ });
+ }
+ })()}
+
+
+
Inspector Report
+ ${until(
+ (async () => {
+ const opts = {}; // NOTE: Currently options are handled on the Python end until exposed to the user
+
+ const title = "Inspecting your file";
+
+ const items = onlyFirstFile
+ ? (
+ await run("inspect_file", { nwbfile_path: fileArr[0].info.file, ...opts }, { title })
+ ).map((o) => {
+ delete o.file_path;
+ return o;
+ }) // Inspect the first file
+ : await (async () => {
+ const path = getSharedPath(fileArr.map((o) => o.info.file));
+ const report = await run("inspect_folder", { path, ...opts }, { title: title + "s" });
+ return report.map((o) => {
+ o.file_path = o.file_path
+ .replace(`${path}/`, "") // Mac
+ .replace(`${path}\\`, ""); // Windows
+ return o;
+ });
+ })();
+
+ const list = new InspectorList({
+ items: items,
+ listStyles: { maxWidth: "350px" },
+ });
+ list.style.padding = "10px";
+ return list;
+ })(),
+ html`Loading inspector report...`
+ )}
+
+
`;
+ }
+}
+
+customElements.get("nwb-file-preview") || customElements.define("nwb-file-preview", NWBFilePreview);
diff --git a/src/renderer/src/stories/preview/Neurosift.js b/src/renderer/src/stories/preview/Neurosift.js
new file mode 100644
index 000000000..5c8b81d8a
--- /dev/null
+++ b/src/renderer/src/stories/preview/Neurosift.js
@@ -0,0 +1,69 @@
+import { LitElement, css, html } from "lit";
+import { baseUrl } from "../../globals";
+
+import { Loader } from "../Loader";
+
+export function getURLFromFilePath(file, projectName) {
+ const regexp = new RegExp(`.+(${projectName}.+)`);
+ return `${baseUrl}/stubs/${file.match(regexp)[1]}`;
+}
+
+export class Neurosift extends LitElement {
+ static get styles() {
+ return css`
+ :host {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ grid-template-rows: 100%;
+ grid-template-columns: 100%;
+ position: relative;
+ --loader-color: hsl(200, 80%, 50%);
+ }
+
+ :host > * {
+ width: 100%;
+ height: 100%;
+ }
+
+ div {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ span {
+ font-size: 14px;
+ }
+
+ small {
+ padding-left: 10px;
+ }
+
+ iframe {
+ border: 0;
+ }
+ `;
+ }
+
+ constructor({ url }) {
+ super();
+ this.url = url;
+ }
+
+ render() {
+ return html`
${new Loader({ message: "Loading Neurosift view..." })}
+
`;
+ }
+}
+
+customElements.get("neurosift-iframe") || customElements.define("neurosift-iframe", Neurosift);
diff --git a/src/renderer/src/stories/preview/inspector/InspectorList.js b/src/renderer/src/stories/preview/inspector/InspectorList.js
new file mode 100644
index 000000000..5475d3a5e
--- /dev/null
+++ b/src/renderer/src/stories/preview/inspector/InspectorList.js
@@ -0,0 +1,119 @@
+import { LitElement, css, html } from "lit";
+import { List } from "../../List";
+
+const sortList = (items) => {
+ return items
+ .sort((a, b) => {
+ const aCritical = a.importance === "CRITICAL";
+ const bCritical = b.importance === "CRITICAL";
+ if (aCritical && bCritical) return 0;
+ else if (aCritical) return -1;
+ else return 1;
+ })
+ .sort((a, b) => {
+ const aLow = a.severity == "LOW";
+ const bLow = b.severity === "LOW";
+ if (aLow && bLow) return 0;
+ else if (aLow) return 1;
+ else return -1;
+ });
+};
+
+export class InspectorList extends List {
+ constructor({ items, listStyles }) {
+ super({
+ editable: false,
+ unordered: true,
+ items: sortList(items).map((o) => {
+ const item = new InspectorListItem(o);
+ item.style.flexGrow = "1";
+ return { content: item };
+ }),
+ listStyles,
+ });
+ }
+}
+
+customElements.get("inspector-list") || customElements.define("inspector-list", InspectorList);
+
+export class InspectorListItem extends LitElement {
+ static get styles() {
+ return css`
+ :host {
+ display: block;
+ background: gainsboro;
+ border: 1px solid gray;
+ border-radius: 10px;
+ padding: 5px 10px;
+ overflow: hidden;
+ text-wrap: wrap;
+ }
+
+ #message {
+ display: block;
+ font-size: 14px;
+ font-weight: bold;
+ }
+
+ #filepath {
+ font-size: 10px;
+ }
+
+ :host > * {
+ margin: 0px;
+ }
+
+ :host([type="error"]) {
+ color: #9d0b0b;
+ padding: 25px;
+ background: #f8d7da;
+ border: 1px solid #f5c2c7;
+ border-radius: 4px;
+ margin: 0 0 1em;
+ }
+
+ :host([type="warning"]) {
+ color: #856404;
+ padding: 25px;
+ background: #fff3cd;
+ border: 1px solid #ffeeba;
+ border-radius: 4px;
+ margin: 0 0 1em;
+ }
+ `;
+ }
+
+ constructor(props) {
+ super();
+ this.ORIGINAL_TYPE = props.type;
+ Object.assign(this, props);
+ }
+
+ static get properties() {
+ return {
+ type: {
+ type: String,
+ reflect: true,
+ },
+ };
+ }
+
+ render() {
+ this.type = this.ORIGINAL_TYPE ?? (this.importance === "CRITICAL" ? "error" : "warning");
+
+ this.setAttribute("title", this.message);
+
+ const hasObjectType = "object_type" in this;
+ const hasMetadata = hasObjectType && "object_name" in this;
+
+ return html`
+ ${this.file_path ? html`
${this.file_path}` : ""}
+ ${hasMetadata ? html`
${this.message}` : html`
${this.message}
`}
+ ${hasMetadata
+ ? html`
${this.object_name}${hasObjectType ? ` (${this.object_type})` : ""} `
+ : ""}
+ `;
+ }
+}
+
+customElements.get("inspector-list-item") || customElements.define("inspector-list-item", InspectorListItem);
diff --git a/src/renderer/src/stories/preview/inspector/InspectorList.stories.js b/src/renderer/src/stories/preview/inspector/InspectorList.stories.js
new file mode 100644
index 000000000..bcb331913
--- /dev/null
+++ b/src/renderer/src/stories/preview/inspector/InspectorList.stories.js
@@ -0,0 +1,13 @@
+import { InspectorList } from "./InspectorList";
+import testInspectorList from "./test.json";
+
+export default {
+ title: "Components/Inspector List",
+};
+
+const Template = (args) => new InspectorList(args);
+
+export const Default = Template.bind({});
+Default.args = {
+ items: testInspectorList,
+};
diff --git a/src/renderer/src/stories/preview/inspector/test.json b/src/renderer/src/stories/preview/inspector/test.json
new file mode 100644
index 000000000..aa7f9d4dc
--- /dev/null
+++ b/src/renderer/src/stories/preview/inspector/test.json
@@ -0,0 +1,162 @@
+[
+ {
+ "message": "Experimenter is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experimenter_exists",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-070623.nwb"
+ },
+ {
+ "message": "Experiment description is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experiment_description",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-070623.nwb"
+ },
+ {
+ "message": "Metadata /general/institution is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_institution",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-070623.nwb"
+ },
+ {
+ "message": "Metadata /general/keywords is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_keywords",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-070623.nwb"
+ },
+ {
+ "message": "Experimenter is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experimenter_exists",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-070623.nwb"
+ },
+ {
+ "message": "Experiment description is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experiment_description",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-070623.nwb"
+ },
+ {
+ "message": "Metadata /general/institution is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_institution",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-070623.nwb"
+ },
+ {
+ "message": "Metadata /general/keywords is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_keywords",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-070623.nwb"
+ },
+ {
+ "message": "Experimenter is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experimenter_exists",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-060623.nwb"
+ },
+ {
+ "message": "Experiment description is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experiment_description",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-060623.nwb"
+ },
+ {
+ "message": "Metadata /general/institution is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_institution",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-060623.nwb"
+ },
+ {
+ "message": "Metadata /general/keywords is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_keywords",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse2/sub-mouse2_ses-060623.nwb"
+ },
+ {
+ "message": "Experimenter is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experimenter_exists",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-060623.nwb"
+ },
+ {
+ "message": "Experiment description is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_experiment_description",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-060623.nwb"
+ },
+ {
+ "message": "Metadata /general/institution is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_institution",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-060623.nwb"
+ },
+ {
+ "message": "Metadata /general/keywords is missing.",
+ "importance": "BEST_PRACTICE_SUGGESTION",
+ "severity": "LOW",
+ "check_function_name": "check_keywords",
+ "object_type": "NWBFile",
+ "object_name": "root",
+ "location": "/",
+ "file_path": "/Users/garrettflynn/NWB_GUIDE/stubs/NWB GUIDE Tutorial Data/sub-mouse1/sub-mouse1_ses-060623.nwb"
+ }
+]