Skip to content

Commit

Permalink
More improvements to settings
Browse files Browse the repository at this point in the history
  • Loading branch information
toni-neurosc committed Nov 28, 2024
1 parent 59b372b commit 35f3a89
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 416 deletions.
141 changes: 64 additions & 77 deletions gui_dev/src/pages/Settings/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import {
Box,
Button,
InputAdornment,
Popover,
Stack,
Switch,
Expand All @@ -26,10 +27,12 @@ const formatKey = (key) => {
};

// Wrapper components for each type
const BooleanField = ({ value, onChange, error }) => (
<Switch checked={value} onChange={(e) => onChange(e.target.checked)} />
const BooleanField = ({ label, value, onChange, error }) => (
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2">{label}</Typography>
<Switch checked={value} onChange={(e) => onChange(e.target.checked)} />
</Stack>
);

const errorStyle = {
"& .MuiOutlinedInput-root": {
"& fieldset": { borderColor: "error.main" },
Expand All @@ -42,19 +45,22 @@ const errorStyle = {
},
};

const StringField = ({ value, onChange, label, error }) => {
const StringField = ({ label, value, onChange, error }) => {
const errorSx = error ? errorStyle : {};
return (
<TextField
value={value}
onChange={(e) => onChange(e.target.value)}
label={label}
sx={{ ...errorSx }}
/>
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2">{label}</Typography>
<TextField
value={value}
onChange={(e) => onChange(e.target.value)}
label={label}
sx={{ ...errorSx }}
/>
</Stack>
);
};

const NumberField = ({ value, onChange, label, error }) => {
const NumberField = ({ label, value, onChange, error, unit }) => {
const errorSx = error ? errorStyle : {};

const handleChange = (event) => {
Expand All @@ -66,48 +72,59 @@ const NumberField = ({ value, onChange, label, error }) => {
};

return (
<TextField
type="text" // Using "text" instead of "number" for more control
value={value}
onChange={handleChange}
label={label}
sx={{ ...errorSx }}
// InputProps={{
// endAdornment: (
// <InputAdornment position="end">
// <span style={{ lineHeight: 1, display: "inline-block" }}>Hz</span>
// </InputAdornment>
// ),
// }}
inputProps={{
pattern: "[0-9]*",
}}
/>
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2">{label}</Typography>

<TextField
type="text" // Using "text" instead of "number" for more control
value={value}
onChange={handleChange}
label={label}
sx={{ ...errorSx }}
InputProps={{
endAdornment: <InputAdornment position="end">{unit}</InputAdornment>,
}}
inputProps={{
pattern: "[0-9]*",
}}
/>
</Stack>
);
};

const FrequencyRangeField = ({ label, value, onChange, error }) => {
console.log(label, value);
return <FrequencyRange name={label} range={value} onChangeRange={onChange} />;
};

// Map component types to their respective wrappers
const componentRegistry = {
boolean: BooleanField,
string: StringField,
int: NumberField,
float: NumberField,
number: NumberField,
// FrequencyRange: FrequencyRange,
FrequencyRange: FrequencyRangeField,
};

const SettingsField = ({ path, Component, label, value, onChange, error }) => {
const SettingsField = ({
path,
Component,
label,
value,
onChange,
error,
unit,
}) => {
return (
<Tooltip title={error?.msg || ""} arrow placement="top">
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2">{label}</Typography>
<Component
value={value}
onChange={(newValue) => onChange(path, newValue)}
label={label}
error={error}
/>
</Stack>
<Component
value={value}
onChange={(newValue) => onChange(path, newValue)}
label={label}
error={error}
unit={unit}
/>
</Tooltip>
);
};
Expand All @@ -131,48 +148,22 @@ const SettingsSection = ({
errors,
}) => {
const boxTitle = title ? title : formatKey(path[path.length - 1]);
/*
3 possible cases:
1. Primitive type || 2. Object with component -> Don't iterate, render directly
3. Object without component or 4. Array -> Iterate and render recursively
*/

const type = typeof settings;
const isObject = type === "object" && !Array.isArray(settings);
const isArray = Array.isArray(settings);

// __field_type__ should be always present
if (isObject && !settings.__field_type__) {
console.log(settings);
throw new Error("Invalid settings object");
}
const fieldType = isObject ? settings.__field_type__ : type;
const Component = componentRegistry[fieldType];

// Case 1: Primitive type -> Don't iterate, render directly
if (!isObject && !isArray) {
if (!Component) {
console.error(`Invalid component type: ${type}`);
return null;
}

const error = getFieldError(path, errors);

return (
<SettingsField
Component={Component}
label={boxTitle}
value={settings}
onChange={onChange}
path={path}
error={error}
/>
);
}

// Case 2: Object with component -> Don't iterate, render directly
if (isObject && Component) {
const value = "__value__" in settings ? settings.__value__ : settings;
// Case 1: Object or primitive with component -> Don't iterate, render directly
if (Component) {
const value =
isObject && "__value__" in settings ? settings.__value__ : settings;
const unit = isObject && "__unit__" in settings ? settings.__unit__ : null;

return (
<SettingsField
Expand All @@ -182,12 +173,13 @@ const SettingsSection = ({
onChange={onChange}
path={path}
error={getFieldError(path, errors)}
unit={unit}
/>
);
}

// Case 3: Object without component or 4. Array -> Iterate and render recursively
if ((isObject && !Component) || isArray) {
// Case 2: Object without component or Array -> Iterate and render recursively
else {
return (
<TitledBox title={boxTitle} sx={{ borderRadius: 3 }}>
{/* Handle recursing through both objects and arrays */}
Expand All @@ -210,15 +202,10 @@ const SettingsSection = ({
</TitledBox>
);
}

// Default case: return null and log an error
console.error(`Invalid settings object, returning null`);
return null;
};

const StatusBarSettingsInfo = () => {
const validationErrors = useSettingsStore((state) => state.validationErrors);
console.log("validationErrors:", validationErrors);
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);

Expand Down
70 changes: 43 additions & 27 deletions gui_dev/src/pages/Settings/components/FrequencyRange.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { useState } from "react";
import { TextField, Button, IconButton, Stack } from "@mui/material";
import {
TextField,
Button,
IconButton,
Stack,
Typography,
} from "@mui/material";
import { Add, Close } from "@mui/icons-material";
import { debounce } from "@/utils";

Expand All @@ -25,26 +31,30 @@ export const FrequencyRange = ({
range,
onChangeName,
onChangeRange,
onRemove,
error,
nameEditable = false,
}) => {
console.log(range);
const [localName, setLocalName] = useState(name);

const debouncedChangeName = debounce((newName) => {
onChangeName(newName, name);
}, 1000);

const handleNameChange = (e) => {
if (!nameEditable) return;
const newName = e.target.value;
setLocalName(newName);
debouncedChangeName(newName);
};

const handleNameBlur = () => {
if (!nameEditable) return;
onChangeName(localName, name);
};

const handleKeyPress = (e) => {
if (!nameEditable) return;
if (e.key === "Enter") {
console.log(e.target.value, name);
onChangeName(localName, name);
Expand All @@ -58,14 +68,18 @@ export const FrequencyRange = ({

return (
<Stack direction="row" alignItems="center" gap={1}>
<TextField
size="small"
value={localName}
fullWidth
onChange={handleNameChange}
onBlur={handleNameBlur}
onKeyPress={handleKeyPress}
/>
{nameEditable ? (
<TextField
size="small"
value={localName}
fullWidth
onChange={handleNameChange}
onBlur={handleNameBlur}
onKeyPress={handleKeyPress}
/>
) : (
<Typography variant="body2">{name}</Typography>
)}
<NumberField
size="small"
type="number"
Expand All @@ -84,14 +98,6 @@ export const FrequencyRange = ({
}
label="High Hz"
/>
<IconButton
onClick={() => onRemove(name)}
color="primary"
disableRipple
sx={{ m: 0, p: 0 }}
>
<Close />
</IconButton>
</Stack>
);
};
Expand Down Expand Up @@ -159,15 +165,25 @@ export const FrequencyRangeList = ({
return (
<Stack gap={1}>
{rangeOrder.map((name, index) => (
<FrequencyRange
key={index}
id={index}
name={name}
range={ranges[name]}
onChangeName={handleChangeName}
onChangeRange={handleChangeRange}
onRemove={handleRemove}
/>
<Stack direction="row">
<FrequencyRange
key={index}
name={name}
range={ranges[name]}
onChangeName={handleChangeName}
onChangeRange={handleChangeRange}
onRemove={handleRemove}
nameEditable={true}
/>
<IconButton
onClick={() => handleRemove(name)}
color="primary"
disableRipple
sx={{ m: 0, p: 0 }}
>
<Close />
</IconButton>
</Stack>
))}
<Button
variant="outlined"
Expand Down
25 changes: 13 additions & 12 deletions py_neuromodulation/gui/backend/app_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,19 @@ def __init__(
self.logger = logging.getLogger("uvicorn.error")
self.logger.warning(PYNM_DIR)

cors_origins = (
["http://localhost:" + str(dev_port)] if dev_port is not None else []
)

# Configure CORS
self.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if dev:
cors_origins = (
["http://localhost:" + str(dev_port)] if dev_port is not None else []
)
print(cors_origins)
# Configure CORS
self.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Has to be before mounting static files
self.setup_routes()
Expand Down
Loading

0 comments on commit 35f3a89

Please sign in to comment.