Skip to content

Commit

Permalink
Add FileInfo class, re-organize utils folder, add backend endpoint to…
Browse files Browse the repository at this point in the history
… retrieve PyNM directory
  • Loading branch information
toni-neurosc committed Dec 4, 2024
1 parent 96c7770 commit 802db51
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 61 deletions.
4 changes: 2 additions & 2 deletions gui_dev/src/components/FileBrowser/FileBrowser.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useReducer, useEffect } from "react";
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";
import {
Box,
Button,
Expand Down Expand Up @@ -36,7 +36,7 @@ import {
import { QuickAccessSidebar } from "./QuickAccess";
import { FileManager } from "@/utils/FileManager";

const fileManager = new FileManager("");
const fileManager = new FileManager(getBackendURL("/api/files"));

const ALLOWED_EXTENSIONS = [".npy", ".vhdr", ".fif", ".edf", ".bdf"];

Expand Down
2 changes: 1 addition & 1 deletion gui_dev/src/components/FileBrowser/QuickAccess.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";
import {
Paper,
Typography,
Expand Down
19 changes: 14 additions & 5 deletions gui_dev/src/pages/SourceSelection/FileSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useSessionStore } from "@/stores";

import { FileBrowser, TitledBox } from "@/components";

import { getPyNMDirectory } from "@/utils";

export const FileSelector = () => {
const fileSource = useSessionStore((state) => state.fileSource);
const setFileSource = useSessionStore((state) => state.setFileSource);
Expand All @@ -19,17 +21,22 @@ export const FileSelector = () => {
);
const setSourceType = useSessionStore((state) => state.setSourceType);

const fileBrowserDirRef = useRef(
"C:\\code\\py_neuromodulation\\py_neuromodulation\\data\\sub-testsub\\ses-EphysMedOff\\ieeg\\sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr"
);
const fileBrowserDirRef = useRef("");

const [isSelecting, setIsSelecting] = useState(false);
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showFolderBrowser, setShowFolderBrowser] = useState(false);

useEffect(() => {
setSourceType("lsl");
}, []);

const fetchPyNMDirectory = async () => {
const pynmDir = await getPyNMDirectory();
fileBrowserDirRef.current =
pynmDir + "\\data\\sub-testsub\\ses-EphysMedOff\\ieeg\\";
};
fetchPyNMDirectory();
}, [setSourceType]);

const handleFileSelect = (file) => {
setIsSelecting(true);
Expand Down Expand Up @@ -72,7 +79,9 @@ export const FileSelector = () => {
</Typography>
)}
{fileSource.size != "0" && (
<Typography variant="body2">File Size: {fileSource.size}</Typography>
<Typography variant="body2">
File Size: {fileSource.size} bytes
</Typography>
)}
{fileSource.path && (
<Typography variant="body2">File Path: {fileSource.path}</Typography>
Expand Down
2 changes: 1 addition & 1 deletion gui_dev/src/stores/appInfoStore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createStore } from "./createStore";
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";

export const useAppInfoStore = createStore("appInfo", (set) => ({
version: "",
Expand Down
3 changes: 1 addition & 2 deletions gui_dev/src/stores/sessionStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// the data source, stream paramerters, the output files paths, etc

import { createStore } from "@/stores/createStore";
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";

// Workflow stages enum-like object
export const WorkflowStage = Object.freeze({
Expand Down Expand Up @@ -110,7 +110,6 @@ export const useSessionStore = createStore("session", (set, get) => ({

// Check that all stream parameters are valid
checkStreamParameters: () => {
// const { samplingRate, lineNoise, samplingRateFeatures } = get();
set({
areParametersValid:
get().streamParameters.samplingRate &&
Expand Down
2 changes: 1 addition & 1 deletion gui_dev/src/stores/settingsStore.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";
import { createStore } from "./createStore";

const INITIAL_DELAY = 3000; // wait for Flask
Expand Down
9 changes: 4 additions & 5 deletions gui_dev/src/stores/socketStore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createStore } from "./createStore";
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";
import CBOR from "cbor-js";

const WEBSOCKET_URL = getBackendURL("/ws");
Expand Down Expand Up @@ -89,16 +89,15 @@ export const useSocketStore = createStore("socket", (set, get) => ({

// check if this is the same:
Object.entries(decodedData).forEach(([key, value]) => {
(key.startsWith("decode") ? decodingData : dataNonDecodingFeatures)[key] = value;
(key.startsWith("decode") ? decodingData : dataNonDecodingFeatures)[
key
] = value;
});

set({ availableDecodingOutputs: Object.keys(decodingData) });



set({ graphDecodingData: decodingData });
set({ graphData: dataNonDecodingFeatures });

}
} catch (error) {
console.error("Failed to decode CBOR message:", error);
Expand Down
137 changes: 137 additions & 0 deletions gui_dev/src/utils/FileInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Represents information about a file or directory in the system
*/
export class FileInfo {
/**
* Creates a new FileInfo instance
* @param {Object} params - The parameters to initialize the FileInfo object
* @param {string} [params.name=''] - The name of the file or directory
* @param {string} [params.path=''] - The full path of the file or directory
* @param {string} [params.dir=''] - The directory containing the file or directory
* @param {boolean} [params.is_directory=false] - Whether the entry is a directory
* @param {number} [params.size=0] - The size of the file in bytes (0 for directories)
* @param {string} [params.created_at=''] - The creation timestamp of the file
* @param {string} [params.modified_at=''] - The last modification timestamp of the file
*/
constructor({
name = "",
path = "",
dir = "",
is_directory = false,
size = 0,
created_at = "",
modified_at = "",
} = {}) {
this.name = name;
this.path = path;
this.dir = dir;
this.is_directory = is_directory;
this.size = size;
this.created_at = created_at;
this.modified_at = modified_at;
}

/**
* Creates a FileInfo instance from a plain object
* @param {Object} obj - The object containing file information
* @returns {FileInfo} A new FileInfo instance
*/
static fromObject(obj) {
return new FileInfo(obj);
}

/**
* Resets all properties to their default values
*/
reset() {
Object.assign(this, new FileInfo());
}

/**
* Updates the FileInfo instance with new values
* @param {Partial<FileInfo>} updates - The properties to update
*/
update(updates) {
Object.assign(this, updates);
}

/**
* Gets the file extension
* @returns {string} The file extension (empty string for directories)
*/
getExtension() {
if (this.is_directory) return "";
const ext = this.name.split(".").pop();
return ext === this.name ? "" : ext;
}

/**
* Gets the base name without extension
* @returns {string} The base name
*/
getBaseName() {
if (this.is_directory) return this.name;
const lastDotIndex = this.name.lastIndexOf(".");
return lastDotIndex === -1 ? this.name : this.name.slice(0, lastDotIndex);
}

/**
* Formats the file size in a human-readable format
* @returns {string} The formatted file size
*/
getFormattedSize() {
if (this.is_directory) return "-";
const units = ["B", "KB", "MB", "GB", "TB"];
let size = this.size;
let unitIndex = 0;

while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}

return `${Math.round(size * 100) / 100} ${units[unitIndex]}`;
}

/**
* Checks if the file/directory is hidden
* @returns {boolean} Whether the file/directory is hidden
*/
isHidden() {
return this.name.startsWith(".");
}

/**
* Creates a plain object representation of the FileInfo instance
* @returns {Object} A plain object containing the file information
*/
toObject() {
return {
name: this.name,
path: this.path,
dir: this.dir,
is_directory: this.is_directory,
size: this.size,
created_at: this.created_at,
modified_at: this.modified_at,
};
}

/**
* Creates a clone of the FileInfo instance
* @returns {FileInfo} A new FileInfo instance with the same values
*/
clone() {
return new FileInfo(this.toObject());
}

/**
* Compares this FileInfo instance with another
* @param {FileInfo} other - The other FileInfo instance to compare with
* @returns {boolean} Whether the two instances have the same values
*/
equals(other) {
if (!(other instanceof FileInfo)) return false;
return JSON.stringify(this.toObject()) === JSON.stringify(other.toObject());
}
}
17 changes: 4 additions & 13 deletions gui_dev/src/utils/FileManager.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
/**
* @typedef {Object} FileInfo
* @property {string} name - The name of the file or directory
* @property {string} path - The full path of the file or directory
* @property {string} dir - The directory containing the file or directory
* @property {boolean} is_directory - Whether the entry is a directory
* @property {number} size - The size of the file in bytes (0 for directories)
* @property {string} created_at - The creation timestamp of the file
* @property {string} modified_at - The last modification timestamp of the file
*/
import { getBackendURL } from "@/utils/getBackendURL";
import { FileInfo } from "./FileInfo";

/**
* Manages file operations and interactions with the file API
Expand Down Expand Up @@ -41,13 +31,14 @@ export class FileManager {
show_hidden: showHidden,
});

const response = await fetch(getBackendURL(`${this.apiBaseUrl}/api/files?${queryParams}`));
const response = await fetch(`${this.apiBaseUrl}?${queryParams}`);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return await response.json();
const filesData = await response.json();
return filesData.map((fileData) => FileInfo.fromObject(fileData));
}

/**
Expand Down
52 changes: 26 additions & 26 deletions gui_dev/src/utils/debounced_sync.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { debounce } from "@/utils";
import { getBackendURL } from "@/utils/getBackendURL";
import { getBackendURL } from "@/utils";

const DEBOUNCE_MS = 500; // Adjust as needed

Expand All @@ -22,28 +22,28 @@ const syncWithBackend = async (state) => {
const debouncedSync = debounce(syncWithBackend, DEBOUNCE_MS);


/*****************************/
/******** BACKEND SYNC *******/
/*****************************/

// Wrap state updates with sync logic
setState: async (newState) => {
set((state) => ({ ...state, ...newState, syncStatus: "syncing" }));
try {
await debouncedSync(get());
set({ syncStatus: "synced", syncError: null });
} catch (error) {
set({ syncStatus: "error", syncError: error.message });
}
},

// // Use this for actions that need immediate sync
// setStateAndSync: async (newState) => {
// set((state) => ({ ...state, ...newState, syncStatus: "syncing" }));
// try {
// const syncedState = await syncWithBackend(get());
// set({ ...syncedState, syncStatus: "synced", syncError: null });
// } catch (error) {
// set({ syncStatus: "error", syncError: error.message });
// }
// },
/*****************************/
/******** BACKEND SYNC *******/
/*****************************/

// Wrap state updates with sync logic
setState: async (newState) => {
set((state) => ({ ...state, ...newState, syncStatus: "syncing" }));
try {
await debouncedSync(get());
set({ syncStatus: "synced", syncError: null });
} catch (error) {
set({ syncStatus: "error", syncError: error.message });
}
},

// // Use this for actions that need immediate sync
// setStateAndSync: async (newState) => {
// set((state) => ({ ...state, ...newState, syncStatus: "syncing" }));
// try {
// const syncedState = await syncWithBackend(get());
// set({ ...syncedState, syncStatus: "synced", syncError: null });
// } catch (error) {
// set({ syncStatus: "error", syncError: error.message });
// }
// }
3 changes: 0 additions & 3 deletions gui_dev/src/utils/getBackendURL.js

This file was deleted.

2 changes: 1 addition & 1 deletion gui_dev/src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./functions";
export * from "./utils";
21 changes: 20 additions & 1 deletion gui_dev/src/utils/functions.js → gui_dev/src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const flattenDictionary = (dict, parentKey = "", result = {}) => {
export const filterObjectByKeys = (flatDict, keys) => {
const filteredDict = {};
keys.forEach((key) => {
if (flatDict.hasOwnProperty(key)) {
if (Object.hasOwn(flatDict, key)) {
filteredDict[key] = flatDict[key];
}
});
Expand All @@ -49,3 +49,22 @@ export const filterObjectByKeyPrefix = (obj, prefix = "") => {
}
return result;
};

export const getBackendURL = (route) => {
return "http://localhost:" + import.meta.env.VITE_BACKEND_PORT + route;
};

/**
* Fetches PyNeuromodulation directory from the backend
* @returns {string} PyNeuromodulation directory
*/
export const getPyNMDirectory = async () => {
const response = await fetch(getBackendURL("/api/pynm_dir"));
if (!response.ok) {
throw new Error("Failed to fetch settings");
}

const data = await response.json();

return data.pynm_dir;
};

0 comments on commit 802db51

Please sign in to comment.