Skip to content

Commit d8b80a4

Browse files
Merge branch 'microsoft:main' into msri_enhanced_planning
2 parents 977cba1 + c32d85c commit d8b80a4

File tree

6 files changed

+274
-150
lines changed

6 files changed

+274
-150
lines changed

frontend/src/components/settings/SettingsModal.tsx

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ import {
1212
Flex,
1313
message,
1414
Modal,
15-
Select,
15+
Spin,
1616
Tabs,
1717
Typography,
1818
} from "antd";
1919
import { validateAll } from "./validation";
2020

21-
const { Option } = Select;
22-
2321
interface SettingsMenuProps {
2422
isOpen: boolean;
2523
onClose: () => void;
@@ -29,31 +27,37 @@ const SettingsModal: React.FC<SettingsMenuProps> = ({ isOpen, onClose }) => {
2927
const { darkMode, setDarkMode, user } = React.useContext(appContext);
3028
const [isEmailModalOpen, setIsEmailModalOpen] = React.useState(false);
3129
const [hasChanges, setHasChanges] = React.useState(false);
30+
const [isLoading, setIsLoading] = React.useState(false);
31+
const [originalConfig, setOriginalConfig] = React.useState<any>(null);
3232

3333
const { config, updateConfig, resetToDefaults } = useSettingsStore();
3434

3535
React.useEffect(() => {
3636
if (isOpen) {
3737
setHasChanges(false);
38+
setIsLoading(true);
3839

3940
// Load settings when modal opens
4041
const loadSettings = async () => {
4142
if (user?.email) {
4243
try {
4344
const settings = await settingsAPI.getSettings(user.email);
44-
const errors = validateAll(settings)
45+
const errors = validateAll(settings);
4546
if (errors.length > 0) {
4647
message.error("Failed to load settings. Using defaults.");
4748
resetToDefaults();
48-
}
49-
else {
49+
setOriginalConfig(null);
50+
} else {
5051
updateConfig(settings);
52+
setOriginalConfig(settings);
5153
}
5254
} catch (error) {
53-
message.error("Failed to load settings. Using defaults.")
55+
message.error("Failed to load settings. Using defaults.");
5456
resetToDefaults();
57+
setOriginalConfig(null);
5558
}
5659
}
60+
setIsLoading(false);
5761
};
5862
loadSettings();
5963
}
@@ -71,100 +75,116 @@ const SettingsModal: React.FC<SettingsMenuProps> = ({ isOpen, onClose }) => {
7175

7276
const handleClose = useCallback(async () => {
7377
// Check all validation states before saving
74-
const validationErrors = validateAll(config)
78+
const validationErrors = validateAll(config);
7579
if (validationErrors.length > 0) {
7680
const errors = validationErrors.join("\n");
7781
message.error(errors);
82+
return;
7883
}
79-
else {
80-
// Save to database
81-
if (user?.email) {
82-
try {
83-
await settingsAPI.updateSettings(user.email, config);
84-
message.success("Updated settings!")
85-
} catch (error) {
86-
message.error("Failed to save settings");
87-
console.error("Failed to save settings:", error);
88-
}
84+
85+
// Only save if there are actual changes
86+
const hasActualChanges =
87+
originalConfig &&
88+
JSON.stringify(config) !== JSON.stringify(originalConfig);
89+
90+
if (hasActualChanges && user?.email) {
91+
try {
92+
await settingsAPI.updateSettings(user.email, config);
93+
message.success("Updated settings!");
94+
} catch (error) {
95+
message.error("Failed to save settings");
96+
console.error("Failed to save settings:", error);
97+
return;
8998
}
90-
91-
onClose();
9299
}
93100

94-
}, [config, settingsAPI, message]);
101+
onClose();
102+
}, [config, originalConfig, user?.email, onClose]);
95103

96104
const tabItems = {
97-
"general": {
105+
general: {
98106
label: "General",
99107
children: (
100108
<>
101-
<Typography.Text strong>General Settings</Typography.Text>
102-
<Divider />
103-
<GeneralSettings
104-
darkMode={darkMode}
105-
setDarkMode={setDarkMode}
106-
config={config}
107-
handleUpdateConfig={handleUpdateConfig}
109+
<Typography.Text strong>General Settings</Typography.Text>
110+
<Divider />
111+
<GeneralSettings
112+
darkMode={darkMode}
113+
setDarkMode={setDarkMode}
114+
config={config}
115+
handleUpdateConfig={handleUpdateConfig}
108116
/>
109117
</>
110118
),
111119
},
112-
"agents": {
120+
agents: {
113121
label: "Agent Settings",
114122
children: (
115123
<>
116-
<Typography.Text strong>Agent Settings</Typography.Text>
117-
<Divider />
118-
<AgentSettingsTab
119-
config={config}
120-
handleUpdateConfig={handleUpdateConfig}
124+
<Typography.Text strong>Agent Settings</Typography.Text>
125+
<Divider />
126+
<AgentSettingsTab
127+
config={config}
128+
handleUpdateConfig={handleUpdateConfig}
121129
/>
122130
</>
123131
),
124132
},
125-
"advanced_config": {
133+
advanced_config: {
126134
label: "Advanced Settings",
127135
children: (
128136
<>
129-
<Typography.Text strong>Advanced Settings</Typography.Text>
130-
<Divider />
131-
<AdvancedConfigEditor
132-
config={config}
133-
darkMode={darkMode}
134-
handleUpdateConfig={handleUpdateConfig}
137+
<Typography.Text strong>Advanced Settings</Typography.Text>
138+
<Divider />
139+
<AdvancedConfigEditor
140+
config={config}
141+
darkMode={darkMode}
142+
handleUpdateConfig={handleUpdateConfig}
135143
/>
136144
</>
137145
),
138146
},
139-
}
147+
};
140148

141149
return (
142150
<>
143151
<Modal
144152
open={isOpen}
145-
style={{maxHeight: 800, overflow: 'auto' }}
146-
153+
style={{ maxHeight: 800, overflow: "auto" }}
147154
onCancel={handleClose}
148155
closable={true}
149156
width={800}
150157
height={800}
151158
footer={[
152159
<Flex gap="large" justify="start" align="center">
153-
<Button key="reset" onClick={handleResetDefaults}>
160+
<Button
161+
key="reset"
162+
onClick={handleResetDefaults}
163+
disabled={isLoading}
164+
>
154165
Reset to Defaults
155166
</Button>
156167
{hasChanges && (
157-
<Typography.Text italic type="warning">
158-
Warning: Settings changes will only apply when you create a new session
159-
</Typography.Text>
168+
<Typography.Text italic type="warning">
169+
Warning: Settings changes will only apply when you create a new
170+
session
171+
</Typography.Text>
160172
)}
161-
</Flex>
173+
</Flex>,
162174
]}
163175
>
164-
<Tabs
165-
tabPosition="left"
166-
items={Object.entries(tabItems).map(([key, {label, children}]) => ({key, label, children}))}
167-
/>
176+
{isLoading ? (
177+
<Flex justify="center" align="center" style={{ height: "400px" }}>
178+
<Spin size="large" />
179+
</Flex>
180+
) : (
181+
<Tabs
182+
tabPosition="left"
183+
items={Object.entries(tabItems).map(
184+
([key, { label, children }]) => ({ key, label, children })
185+
)}
186+
/>
187+
)}
168188
</Modal>
169189
<SignInModal
170190
isVisible={isEmailModalOpen}

frontend/src/components/settings/tabs/advancedSetings/AdvancedSettings.tsx

Lines changed: 78 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect } from "react";
22
import MonacoEditor from "@monaco-editor/react";
33
import yaml from "js-yaml";
4-
import { Button, Tooltip, Flex } from "antd";
4+
import { Button, Flex, Alert } from "antd";
55
import { message } from "antd";
66
import { UploadOutlined } from "@ant-design/icons";
77
import { validateAll } from "../../validation";
@@ -18,8 +18,12 @@ const AdvancedConfigEditor: React.FC<AdvancedConfigEditorProps> = ({
1818
handleUpdateConfig,
1919
}) => {
2020
const [errors, setErrors] = React.useState<string[]>([]);
21-
const [editorValue, setEditorValue] = React.useState(config ? yaml.dump(config) : "");
21+
const [editorValue, setEditorValue] = React.useState(
22+
config ? yaml.dump(config) : ""
23+
);
24+
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
2225
const fileInputRef = React.useRef<HTMLInputElement>(null);
26+
const validationTimeoutRef = React.useRef<NodeJS.Timeout>();
2327

2428
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
2529
const file = event.target.files?.[0];
@@ -56,9 +60,10 @@ const AdvancedConfigEditor: React.FC<AdvancedConfigEditorProps> = ({
5660
useEffect(() => {
5761
const yamlConfig = config ? yaml.dump(config) : "";
5862
if (yamlConfig !== editorValue) {
59-
setEditorValue(yamlConfig)
63+
setEditorValue(yamlConfig);
64+
setHasUnsavedChanges(false);
6065
}
61-
}, [config])
66+
}, [config]);
6267

6368
return (
6469
<Flex vertical gap="large">
@@ -72,64 +77,71 @@ const AdvancedConfigEditor: React.FC<AdvancedConfigEditorProps> = ({
7277
ref={fileInputRef}
7378
type="file"
7479
accept=".json,.yaml,.yml"
75-
style={{ display: 'none' }}
80+
style={{ display: "none" }}
7681
onChange={handleFileUpload}
7782
/>
7883
</Button>
84+
<Button
85+
type="primary"
86+
disabled={errors.length > 0 || !hasUnsavedChanges}
87+
onClick={() => {
88+
try {
89+
const parsed = yaml.load(editorValue);
90+
const validationErrors = validateAll(parsed);
91+
if (validationErrors.length === 0) {
92+
handleUpdateConfig(parsed);
93+
setHasUnsavedChanges(false);
94+
message.success("Settings updated successfully");
95+
}
96+
} catch (e) {
97+
message.error("Invalid YAML format");
98+
}
99+
}}
100+
>
101+
Apply Changes
102+
</Button>
79103
<Button
80104
danger
105+
disabled={!hasUnsavedChanges}
81106
onClick={() => {
82107
setEditorValue(config ? yaml.dump(config) : "");
83-
setErrors(validateAll(config));
108+
setErrors([]);
109+
setHasUnsavedChanges(false);
84110
}}
85111
>
86112
Discard Changes
87113
</Button>
88-
{errors.length > 0 && (
89-
<Tooltip
90-
title={
91-
<div>
92-
{errors.map((err, idx) => (
93-
<div key={idx} style={{ whiteSpace: 'pre-wrap', color: 'white' }}>{err}</div>
94-
))}
95-
</div>
96-
}
97-
color="red"
98-
placement="right"
99-
>
100-
<span style={{ display: 'flex', alignItems: 'center', color: 'red', cursor: 'pointer' }}>
101-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginLeft: 4 }}>
102-
<circle cx="12" cy="12" r="10" />
103-
<line x1="12" y1="8" x2="12" y2="12" />
104-
<circle cx="12" cy="16" r="1" />
105-
</svg>
106-
<span style={{ marginLeft: 4, fontSize: 12 }}>
107-
{errors.length} error{errors.length > 1 ? 's' : ''}
108-
</span>
109-
</span>
110-
</Tooltip>
111-
)}
112114
</Flex>
113-
<div style={{
114-
padding: 2,
115-
border: errors.length > 0 ? "2px solid red" : "none",
116-
borderRadius: errors.length > 0 ? 6 : undefined,
117-
}}>
115+
116+
<div
117+
style={{
118+
padding: 2,
119+
border: errors.length > 0 ? "2px solid red" : "none",
120+
borderRadius: errors.length > 0 ? 6 : undefined,
121+
}}
122+
>
118123
<MonacoEditor
119-
theme={darkMode === "dark" ? "vs-dark" : "light" }
124+
theme={darkMode === "dark" ? "vs-dark" : "light"}
120125
value={editorValue}
121-
onChange={value => {
122-
setEditorValue(value || "")
123-
try {
124-
const parsed = yaml.load(value || "");
125-
const errors = validateAll(parsed);
126-
setErrors(errors); // Always update errors, even if empty
127-
if (errors.length === 0) {
128-
handleUpdateConfig(parsed)
129-
}
130-
} catch (e) {
131-
setErrors([`${e}`])
126+
onChange={(value) => {
127+
setEditorValue(value || "");
128+
setHasUnsavedChanges(true);
129+
130+
// Clear existing timeout
131+
if (validationTimeoutRef.current) {
132+
clearTimeout(validationTimeoutRef.current);
132133
}
134+
135+
// Set new timeout to validate after user stops typing
136+
validationTimeoutRef.current = setTimeout(() => {
137+
try {
138+
const parsed = yaml.load(value || "");
139+
const validationErrors = validateAll(parsed);
140+
setErrors(validationErrors);
141+
} catch (e) {
142+
setErrors([`${e}`]);
143+
}
144+
}, 500); // 0.5 second delay for validation
133145
}}
134146
language="yaml"
135147
options={{
@@ -141,6 +153,25 @@ const AdvancedConfigEditor: React.FC<AdvancedConfigEditorProps> = ({
141153
height="500px"
142154
/>
143155
</div>
156+
157+
{errors.length > 0 && (
158+
<Alert
159+
message="Configuration Errors"
160+
description={
161+
<div>
162+
{errors.map((err, idx) => (
163+
<div key={idx} style={{ marginBottom: 4 }}>
164+
{err}
165+
</div>
166+
))}
167+
</div>
168+
}
169+
type="error"
170+
showIcon
171+
closable
172+
onClose={() => setErrors([])}
173+
/>
174+
)}
144175
</Flex>
145176
);
146177
};

0 commit comments

Comments
 (0)