Skip to content

Commit a574df8

Browse files
Merge pull request #355 from AutomatingSciencePipeline/development
String List Base Implementation and Fix Dispatch Step Button
2 parents 8b886aa + 34d1e0d commit a574df8

File tree

13 files changed

+383
-88
lines changed

13 files changed

+383
-88
lines changed
Lines changed: 167 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,63 @@
1-
import { Fragment } from 'react';
2-
import { Switch, ActionIcon, Center } from '@mantine/core';
1+
import { Fragment, useState, useEffect } from 'react';
2+
import { Switch, ActionIcon, Center, Button, Modal } from '@mantine/core';
33
import { Draggable } from 'react-beautiful-dnd';
4-
import { GripVertical } from 'tabler-icons-react';
4+
import { GripVertical, Plus } from 'tabler-icons-react';
55
import { TrashIcon as Trash } from '@heroicons/react/24/solid';
6+
import { string } from 'joi';
67

7-
const Parameter = ({ form, type, index, ...rest }) => {
8+
const Parameter = ({ form, type, index, confirmedValues, setConfirmedValues, ...rest }) => {
89
const remains = {
910
string: StringParam,
1011
integer: NumberParam,
1112
float: NumberParam,
1213
bool: BoolParam,
14+
stringlist: MultiStringParam,
1315
};
1416
const Component = remains[type];
17+
18+
const updateConfirmedValues = (index, newValues) => {
19+
20+
const hasConfirmedValues = confirmedValues.some(item => item.index === index);
21+
if (hasConfirmedValues) {
22+
const newConfirmedValues = confirmedValues.map(item => {
23+
if (item.index === index) {
24+
return {
25+
index,
26+
values: newValues,
27+
};
28+
}
29+
return item;
30+
});
31+
setConfirmedValues(newConfirmedValues);
32+
} else {
33+
const newConfirmedValues = [...confirmedValues, { index, values: newValues }];
34+
setConfirmedValues(newConfirmedValues);
35+
}
36+
37+
};
38+
39+
useEffect(() => {
40+
if (form.values.hyperparameters[index].values && (form.values.hyperparameters[index].values.length != 1 && form.values.hyperparameters[index].values[0] != '')) {
41+
updateConfirmedValues(index, form.values.hyperparameters[index].values);
42+
}
43+
}, []);
44+
45+
46+
const handleRemove = () => {
47+
form.removeListItem('hyperparameters', index);
48+
const newConfirmedValues = confirmedValues.filter(item => item.index !== index);
49+
setConfirmedValues(newConfirmedValues);
50+
};
51+
1552
return (
16-
<Draggable key={index} index={index} draggableId={index.toString()}>
17-
{(provided) => {
18-
return (
19-
<div
20-
ref={provided.innerRef}
21-
{...provided.draggableProps}
22-
// className={'mt-8 justify-start'}
23-
className={'flex items-center mb-2'}
24-
>
53+
<Draggable key={index} index={index} draggableId={index.toString()} isDragDisabled={true}>
54+
{(provided) => (
55+
<div
56+
ref={provided.innerRef}
57+
{...provided.draggableProps}
58+
className='flex flex-col mb-2'
59+
>
60+
<div className='flex items-center'>
2561
<Center {...provided.dragHandleProps}>
2662
<GripVertical className='mr-2' />
2763
</Center>
@@ -30,20 +66,33 @@ const Parameter = ({ form, type, index, ...rest }) => {
3066
type='text'
3167
placeholder='name'
3268
className='block w-full rounded-l-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm'
33-
{...form.getListInputProps('hyperparameters', index, 'name')}
69+
{...form.getInputProps(`hyperparameters.${index}.name`)}
3470
required
3571
/>
36-
<Component form={form} type={type} index={index} {...rest} />
72+
<Component form={form} type={type} index={index} updateConfirmedValues={updateConfirmedValues} {...rest} />
73+
3774
<ActionIcon
3875
color='red'
39-
onClick={() => form.removeListItem('hyperparameters', index)}
76+
onClick={handleRemove}
4077
className='ml-2'
4178
>
42-
<Trash/>
79+
<Trash />
4380
</ActionIcon>
4481
</div>
45-
);
46-
}}
82+
{((confirmedValues?.find(item => item.index === index)?.values?.length ?? 0) > 0 && type === 'stringlist') && (
83+
<div className='mt-4 p-4 bg-gray-100 rounded-md shadow-sm'>
84+
<h3 className='text-lg font-medium text-gray-700 mb-2'>Values:</h3>
85+
{confirmedValues.find(item => item.index === index)?.values.map((value, idx) => (
86+
<div key={idx} className='flex items-center mb-2'>
87+
<span className='block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 bg-white'>
88+
{value}
89+
</span>
90+
</div>
91+
))}
92+
</div>
93+
)}
94+
</div>
95+
)}
4796
</Draggable>
4897
);
4998
};
@@ -52,14 +101,15 @@ const NumberParam = ({ form, type, index, ...rest }) => {
52101
return (
53102
<Fragment>
54103
{['default', 'min', 'max', 'step'].map((label, i) => {
104+
55105
return (
56106
<input
57107
key={`number_${type}_${label}`}
58108
type='number'
59109
placeholder={`${label}`}
60110
className='block w-full last-of-type:rounded-r-md border-gray-300 shadow-sm focus:border-blue-500 sm:text-sm'
61111
required
62-
{...form.getListInputProps('hyperparameters', index, label)}
112+
{...form.getInputProps(`hyperparameters.${index}.${label}`)}
63113
/>
64114
);
65115
})}
@@ -73,7 +123,7 @@ const BoolParam = ({ form, type, index, ...rest }) => {
73123
label={'value'}
74124
onLabel={'True'}
75125
offLabel={'False'}
76-
{...form.getListInputProps('hyperparameters', index, 'default')}
126+
{...form.getInputProps(`hyperparameters.${index}.default`)}
77127
/>
78128
);
79129
};
@@ -85,11 +135,106 @@ const StringParam = ({ form, type, index, ...rest }) => {
85135
type='text'
86136
placeholder={`${type} value`}
87137
className='block w-full rounded-r-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm'
88-
{...form.getListInputProps('hyperparameters', index, 'default')}
138+
{...form.getInputProps(`hyperparameters.${index}.default`)}
89139
required
90140
/>
91141
</>
92142
);
93143
};
94144

145+
const MultiStringParam = ({ form, type, index, updateConfirmedValues, ...rest }) => {
146+
const [opened, setOpened] = useState(false);
147+
const [values, setValues] = useState(form.values.hyperparameters[index].values || ['']);
148+
149+
useEffect(() => {
150+
console.log('Updated values:', values);
151+
}, [values]);
152+
153+
const handleAddValue = () => {
154+
setValues([...values, '']);
155+
form.insertListItem(`hyperparameters[${index}].values`, '');
156+
};
157+
158+
const handleChange = (e, idx) => {
159+
console.log('replacing list item:', idx, e.target.value);
160+
form.replaceListItem(`hyperparameters[${index}].values`, idx, e.target.value);
161+
console.log('after replace:', form.values.hyperparameters[index].values);
162+
const newValues = [...values];
163+
newValues[idx] = e.target.value;
164+
setValues(newValues);
165+
};
166+
167+
const handleDelete = (idx) => {
168+
const newValues = values.filter((_, i) => i !== idx);
169+
setValues(newValues);
170+
171+
const originalValues = form.values.hyperparameters[index]?.values || [];
172+
for (let i = originalValues.length - 1; i >= 0; i--) {
173+
if (!newValues.includes(originalValues[i])) {
174+
console.log('deleting:', originalValues[i]);
175+
form.removeListItem(`hyperparameters[${index}].values`, i);
176+
}
177+
}
178+
179+
console.log('after delete:', form.values.hyperparameters[index].values);
180+
};
181+
182+
const handleClose = () => {
183+
setOpened(false);
184+
};
185+
186+
const handleConfirm = () => {
187+
form.values.hyperparameters[index].values = values;
188+
updateConfirmedValues(index, values);
189+
setOpened(false);
190+
};
191+
192+
const handleOpen = () => {
193+
setOpened(true);
194+
};
195+
196+
return (
197+
<>
198+
<Button onClick={handleOpen} className='ml-2 rounded-md w-1/6 border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2'>
199+
Edit Strings
200+
</Button>
201+
<Modal
202+
opened={opened}
203+
onClose={handleClose}
204+
title="Edit String Values"
205+
>
206+
{values.map((value, idx) => {
207+
return (
208+
<div key={idx} className='flex items-center mb-2'>
209+
<input
210+
type='text'
211+
placeholder={`Value ${idx + 1}`}
212+
className='block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm'
213+
value={value}
214+
onChange={(e) => handleChange(e, idx)}
215+
required
216+
/>
217+
<ActionIcon onClick={() => handleDelete(idx)} color='red' className='ml-2'>
218+
<Trash />
219+
</ActionIcon>
220+
</div>
221+
);
222+
})}
223+
<ActionIcon onClick={handleAddValue} color='blue'>
224+
<Plus />
225+
</ActionIcon>
226+
<div className="flex justify-end mt-4">
227+
<button
228+
className='rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2'
229+
onClick={handleConfirm}
230+
>
231+
Confirm
232+
</button>
233+
</div>
234+
</Modal>
235+
</>
236+
);
237+
};
238+
95239
export default Parameter;
240+

apps/frontend/app/components/auth/SignInModal.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const SignInModal = ({ afterSignIn }: SignInModalProps) => {
1919
email: '',
2020
password: '',
2121
},
22-
schema: joiResolver(signInSchema),
22+
validate: joiResolver(signInSchema),
2323
});
2424

2525
const { data: session } = useSession();
@@ -194,8 +194,7 @@ export const SignInModal = ({ afterSignIn }: SignInModalProps) => {
194194
</div>
195195
</div>
196196
</div>
197-
{/* For dev testing!!! */}
198-
{/* <button onClick={() => {signIn("keycloak", {redirectTo: "/dashboard"})}}>Keycloak</button> */}
197+
199198
</div>
200199
</div>
201200
);

apps/frontend/app/components/auth/SignUpModal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const SignUpModal = ({ afterSignUp }) => {
1414
password: '',
1515
passwordRepeat: '',
1616
},
17-
schema: joiResolver(signUpSchema),
17+
validate: joiResolver(signUpSchema),
1818
});
1919
const DEFAULT_SIGN_UP_TEXT = 'Create your account';
2020
const SIGN_UP_LOADING_TEXT = 'Loading...';
@@ -67,6 +67,8 @@ export const SignUpModal = ({ afterSignUp }) => {
6767
<AiOutlineGithub className='w-5 h-5' />
6868
</a>
6969
</div>
70+
{/* For dev testing!!! */}
71+
{/* <button onClick={() => {signIn("keycloak", {redirectTo: "/dashboard"})}}>Keycloak</button> */}
7072
</div>
7173
</div>
7274
</div>

apps/frontend/app/components/flows/AddExperiment/NewExperiment.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Fragment, useState, useLayoutEffect, useEffect } from 'react';
22
import { Dialog, Transition } from '@headlessui/react';
33
import { Toggle } from '../../Toggle';
44
import Parameter from '../../Parameter';
5-
import { useForm, formList, joiResolver } from '@mantine/form';
5+
import { useForm, joiResolver } from '@mantine/form';
66
import { experimentSchema } from '../../../../utils/validators';
77

88
import { DispatchStep } from './stepComponents/DispatchStep';
@@ -61,11 +61,12 @@ const Steps = ({ steps }) => {
6161
);
6262
};
6363

64+
6465
const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest }) => {
6566
const form = useForm({
6667
// TODO make this follow the schema as closely as we can
6768
initialValues: {
68-
hyperparameters: formList([] as any[]), // TODO type for parameters will remove the need for `any` here
69+
hyperparameters: [] as any[], // TODO type for parameters will remove the need for `any` here
6970
name: '',
7071
description: '',
7172
trialExtraFile: '',
@@ -79,16 +80,16 @@ const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest })
7980
keepLogs: true,
8081
workers: 1,
8182
},
82-
schema: joiResolver(experimentSchema),
83+
validate: joiResolver(experimentSchema),
8384
});
8485

8586
useEffect(() => {
8687
if (copyID != null) {
8788
getDocumentFromId(copyID).then((expInfo) => {
8889
if (expInfo) {
89-
const hyperparameters = expInfo['hyperparameters'];
90+
const hyperparameters = Array.isArray(expInfo['hyperparameters']) ? expInfo['hyperparameters'] : [];
9091
form.setValues({
91-
hyperparameters: formList(hyperparameters),
92+
hyperparameters: hyperparameters,
9293
name: expInfo['name'],
9394
description: expInfo['description'],
9495
trialExtraFile: expInfo['trialExtraFile'],
@@ -112,15 +113,23 @@ const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest })
112113
}
113114
}, [copyID]); // TODO adding form or setCopyId causes render loop?
114115

116+
const [confirmedValues, setConfirmedValues] = useState<{ index: number, values: any[] }[]>([]);
117+
115118

116119
const fields = form.values.hyperparameters.map(({ type, ...rest }, index) => {
117-
return <Parameter key={index} form={form} type={type} index={index} {...rest} />;
120+
return <Parameter key={index} form={form} type={type} index={index} confirmedValues={confirmedValues} setConfirmedValues={setConfirmedValues} {...rest} />;
118121
});
119122

120123
const [open, setOpen] = useState(true);
121124
const [status, setStatus] = useState(0);
122125
const [id, setId] = useState(null);
123126

127+
const onDropComplete = () => {
128+
setFormState(-1);
129+
localStorage.removeItem('ID');
130+
setStatus(FormStates.Info);
131+
};
132+
124133
useLayoutEffect(() => {
125134
if (formState === FormStates.Info) {
126135
setOpen(false);
@@ -182,13 +191,13 @@ const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest })
182191
) : status === FormStates.DumbTextArea ? (
183192
<DumbTextArea form={form}></DumbTextArea>
184193
) : status === FormStates.Params ? (
185-
<ParamStep form={form}>{fields}</ParamStep>
194+
<ParamStep form={form} confirmedValues={confirmedValues} setConfirmedValues={setConfirmedValues}>{fields}</ParamStep>
186195
) : status === FormStates.ProcessStep ? (
187196
<PostProcessStep form={form}>{fields}</PostProcessStep>
188197
) : status === FormStates.Confirmation ? (
189198
<ConfirmationStep form={form} />
190199
) : (
191-
<DispatchStep form={form} id={id} />
200+
<DispatchStep form={form} id={id} onDropComplete={onDropComplete}/>
192201
)}
193202

194203
<div className='flex-shrink-0 border-t border-gray-200 px-4 py-5 sm:px-6'>
@@ -224,7 +233,7 @@ const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest })
224233
>
225234
{status === FormStates.Info ? 'Cancel' : 'Back'}
226235
</button>
227-
<button
236+
{!(status === FormStates.Dispatch) && <button
228237
className='rounded-md w-1/6 border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2'
229238
{...(status === FormStates.Dispatch ?
230239
{
@@ -240,7 +249,7 @@ const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest })
240249
})}
241250
>
242251
{status === FormStates.Dispatch ? 'Dispatch' : 'Next'}
243-
</button>
252+
</button>}
244253
</div>
245254
</div>
246255
</form>
@@ -253,4 +262,4 @@ const NewExperiment = ({ formState, setFormState, copyID, setCopyId, ...rest })
253262
);
254263
};
255264

256-
export default NewExperiment;
265+
export default NewExperiment;

0 commit comments

Comments
 (0)