diff --git a/openbas-front/src/admin/components/scenarios/scenario/Index.tsx b/openbas-front/src/admin/components/scenarios/scenario/Index.tsx index 381f7dfd0b..3b544aef5d 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/Index.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/Index.tsx @@ -137,8 +137,8 @@ const IndexScenarioComponent: FunctionComponent<{ scenario: ScenarioStore }> = (
{!cronExpression && ( - - + setOpenScenarioRecurringFormDialog(true)} style={{ marginRight: 5 }}> + )} {cronExpression && !scenario.scenario_recurrence && ( diff --git a/openbas-front/src/admin/components/scenarios/scenario/ScenarioHeader.tsx b/openbas-front/src/admin/components/scenarios/scenario/ScenarioHeader.tsx index e5750d98c5..f592ed15a0 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/ScenarioHeader.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/ScenarioHeader.tsx @@ -138,7 +138,7 @@ const ScenarioHeader = ({ size="small" onClick={() => setOpenScenarioRecurringFormDialog(true)} > - {t('Simulate Now')} + {t('Launch')} )} diff --git a/openbas-front/src/admin/components/scenarios/scenario/ScenarioRecurringFormDialog.tsx b/openbas-front/src/admin/components/scenarios/scenario/ScenarioRecurringFormDialog.tsx index 1cc856b7a5..d9cc69d772 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/ScenarioRecurringFormDialog.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/ScenarioRecurringFormDialog.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Stack, Switch } from '@mui/material'; -import { DateTimePicker, TimePicker } from '@mui/x-date-pickers'; +import { TimePicker, DateTimePicker } from '@mui/x-date-pickers'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -103,16 +103,31 @@ const ScenarioRecurringFormDialog: React.FC = ({ onSubmit, selectRecurrin }, ).refine( (data) => { - if (data.endDate) { - return new Date(data.endDate).getTime() > new Date(data.startDate).getTime(); + if (['daily', 'weekly', 'monthly'].includes(selectRecurring)) { + if (data.endDate) { + return new Date(data.endDate).getTime() > new Date(data.startDate).getTime(); + } } + return true; }, { message: t('End date need to be stricly after start date'), path: ['endDate'], }, - ), + ) + .refine( + (data) => { + if (data.startDate) { + return new Date(data.startDate).getTime() >= new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime(); + } + return true; + }, + { + message: t('Start date should be at least today'), + path: ['startDate'], + }, + ), ), }); @@ -137,6 +152,7 @@ const ScenarioRecurringFormDialog: React.FC = ({ onSubmit, selectRecurrin reset(defaultFormValues()); setOpen(false); }; + return ( = ({ onSubmit, selectRecurrin render={({ field, fieldState }) => ( { return (startDate ? field.onChange(new Date(startDate).toISOString()) : field.onChange('')); @@ -254,8 +270,11 @@ const ScenarioRecurringFormDialog: React.FC = ({ onSubmit, selectRecurrin name="time" render={({ field, fieldState }) => ( = ({ onSubmit, selectRecurrin render={({ field, fieldState }) => ( { return (endDate ? field.onChange(new Date(new Date(endDate).setUTCHours(0, 0, 0, 0)).toISOString()) : field.onChange('')); @@ -305,7 +324,7 @@ const ScenarioRecurringFormDialog: React.FC = ({ onSubmit, selectRecurrin color="secondary" type="submit" > - {t('Start')} + {t('Save')} diff --git a/openbas-front/src/admin/components/simulations/simulation/ExerciseDateForm.js b/openbas-front/src/admin/components/simulations/simulation/ExerciseDateForm.js deleted file mode 100644 index 26c82eb702..0000000000 --- a/openbas-front/src/admin/components/simulations/simulation/ExerciseDateForm.js +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { Form } from 'react-final-form'; -import { Button } from '@mui/material'; -import DateTimePicker from '../../../../components/DateTimePicker'; -import inject18n from '../../../../components/i18n'; - -class ExerciseDateForm extends Component { - render() { - const { t, onSubmit, initialValues, editing, handleClose } = this.props; - return ( -
{ - changeValue(state, field, () => value); - }, - }} - > - {({ handleSubmit, submitting, pristine }) => ( - - -
- {handleClose && ( - - )} - -
- - )} - - ); - } -} - -ExerciseDateForm.propTypes = { - t: PropTypes.func, - onSubmit: PropTypes.func.isRequired, - handleClose: PropTypes.func, - editing: PropTypes.bool, -}; - -export default inject18n(ExerciseDateForm); diff --git a/openbas-front/src/admin/components/simulations/simulation/ExerciseDateForm.tsx b/openbas-front/src/admin/components/simulations/simulation/ExerciseDateForm.tsx new file mode 100644 index 0000000000..c81fc0ed25 --- /dev/null +++ b/openbas-front/src/admin/components/simulations/simulation/ExerciseDateForm.tsx @@ -0,0 +1,181 @@ +import React, { useState } from 'react'; +import { Button, FormControlLabel, Switch, Stack } from '@mui/material'; +import { DatePicker, TimePicker } from '@mui/x-date-pickers'; +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import type { ExerciseUpdateStartDateInput } from '../../../../utils/api-types'; +import { useFormatter } from '../../../../components/i18n'; +import { zodImplement } from '../../../../utils/Zod'; +import { minutesInFuture } from '../../../../utils/Time'; + +interface Props { + onSubmit: SubmitHandler; + initialValues?: ExerciseUpdateStartDateInput; + handleClose: () => void; +} + +interface ExerciseStartDateAndTime { + date: string; + time: string; +} + +// eslint-disable-next-line no-underscore-dangle +const _MS_DELAY_TOO_CLOSE = 1000 * 60 * 2; + +const ExerciseDateForm: React.FC = ({ + onSubmit, + handleClose, + initialValues, +}) => { + const { t } = useFormatter(); + + const defaultFormValues = () => { + if (initialValues?.exercise_start_date) { + return ({ date: initialValues.exercise_start_date, time: initialValues.exercise_start_date }); + } + return ({ + date: new Date(new Date().setUTCHours(0, 0, 0, 0)).toISOString(), + time: minutesInFuture(5).toISOString(), + }); + }; + + const [checked, setChecked] = useState(!initialValues?.exercise_start_date); + const handleChange = (event: React.ChangeEvent) => { + setChecked(event.target.checked); + }; + + const submit = (data: ExerciseStartDateAndTime) => { + if (checked) { + onSubmit({ exercise_start_date: '' }); + } else { + const date = new Date(data.date); + const time = new Date(data.time); + date.setHours(time.getHours()); + date.setMinutes(time.getMinutes()); + date.setSeconds(time.getSeconds()); + onSubmit({ exercise_start_date: date.toISOString() }); + } + }; + + const { + control, + handleSubmit, + clearErrors, + getValues, + } = useForm({ + defaultValues: defaultFormValues(), + resolver: zodResolver( + zodImplement().with({ + date: z.string().min(1, t('Required')), + time: z.string().min(1, t('Required')), + }).refine( + (data) => { + if (data.date) { + return new Date(data.date).getTime() >= new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime(); + } + return true; + }, + { + message: t('Date should be at least today'), + path: ['date'], + }, + ) + .refine( + (data) => { + if (data.time) { + return (new Date().getTime() + _MS_DELAY_TOO_CLOSE) < new Date(data.time).getTime(); + } + return true; + }, + { + message: t('The time and start date do not match, as the time provided is either too close to the current moment or in the past'), + path: ['time'], + }, + ), + ), + }); + + return ( +
+ } + label={t('Manual launch')} + /> + + + ( + { + return (date ? field.onChange(new Date(date).toISOString()) : field.onChange('')); + }} + onAccept={() => { + clearErrors('time'); + }} + slotProps={{ + textField: { + fullWidth: true, + error: !!fieldState.error, + helperText: fieldState.error && fieldState.error?.message, + }, + }} + /> + )} + /> + + ( + (time ? field.onChange(new Date(time).toISOString()) : field.onChange(''))} + slotProps={{ + textField: { + fullWidth: true, + error: !!fieldState.error, + helperText: fieldState.error && fieldState.error?.message, + }, + }} + /> + )} + /> + + +
+ {handleClose && ( + + )} + +
+ + ); +}; + +export default ExerciseDateForm; diff --git a/openbas-front/src/admin/components/simulations/simulation/ExerciseDatePopover.tsx b/openbas-front/src/admin/components/simulations/simulation/ExerciseDatePopover.tsx index c63f4f05fd..48e029b663 100644 --- a/openbas-front/src/admin/components/simulations/simulation/ExerciseDatePopover.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/ExerciseDatePopover.tsx @@ -24,8 +24,8 @@ const ExerciseDatePopover: React.FC = ({ exercise }) => { return ( <> - setOpenEdit(true)} style={{ marginRight: 5 }}> - + setOpenEdit(true)} style={{ marginRight: 5 }} disabled={exercise.exercise_status !== 'SCHEDULED'}> + = ({ exercise }) => { open={openEdit} onClose={() => setOpenEdit(false)} PaperProps={{ elevation: 1 }} + maxWidth="xs" + fullWidth > {t('Update the simulation')} setOpenEdit(false)} /> diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index 78b428d70e..3b723cb4cf 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -388,6 +388,7 @@ const i18n = { 'Do you want to enable this team?': 'Souhaitez-vous activer cette équipe ?', Trigger: 'Déclencheur', + 'Manual launch': 'Lancement manuel', 'Manage content': 'Gérer le contenu', Controls: 'Contrôles', 'Lessons learned': 'Expérience', @@ -846,7 +847,7 @@ const i18n = { Friday: 'Vendredi', Saturday: 'Samedi', Sunday: 'Dimanche', - Hour: 'Heure', + Scheduling_time: 'Heure', Save: 'Enregistrer', 'Launch a simulation or start a recurring simuation from this scenario': 'Lancer une simulation ou démarrer une simulation récurrente à partir de ce scénario', recurrence_The: 'Le', @@ -1162,6 +1163,7 @@ const i18n = { phone_number_tooltip: 'Phone number should start with a plus sign ( + )\n' + 'It may contain white spaces or hyphens ( – ) or parenthesis.\n', Exercise: 'Simulation', + Scheduling_time: 'Time', // -- FILTERS -- // Endpoint endpoint_hostname: 'Hostname',