From d2738427f4d3225a10fa85e2c48a32542c132f85 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Wed, 20 Sep 2023 11:32:48 +0200 Subject: [PATCH 01/14] Add create-project page --- src/app/App.jsx | 2 ++ src/pages/CreateProjectPage.jsx | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/pages/CreateProjectPage.jsx diff --git a/src/app/App.jsx b/src/app/App.jsx index 20e51315..278efa50 100644 --- a/src/app/App.jsx +++ b/src/app/App.jsx @@ -7,6 +7,7 @@ import NavBar from '../components/NavBar'; import HomePage from '../pages/HomePage'; import CaseStudiesPage from '../pages/CaseStudiesPage'; import AppPage from '../pages/AppPage'; +import CreateProjectPage from '../pages/CreateProjectPage'; import { Amplify } from 'aws-amplify'; import { useAuthenticator } from '@aws-amplify/ui-react'; import * as Tooltip from '@radix-ui/react-tooltip'; @@ -121,6 +122,7 @@ const App = () => { + {/**/} diff --git a/src/pages/CreateProjectPage.jsx b/src/pages/CreateProjectPage.jsx new file mode 100644 index 00000000..b4427fdc --- /dev/null +++ b/src/pages/CreateProjectPage.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Page } from '../components/Page.jsx'; + +const CreateProjectPage = () => { + return ( + +

Create Project

+
+ ) +} + +export default CreateProjectPage; From d1f47b2e1ff214772b0f7bd2a064f60620dd6b7f Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Wed, 20 Sep 2023 12:22:10 +0200 Subject: [PATCH 02/14] Hide create project page if not superuser --- src/components/NotFound.jsx | 20 ++++++++++++++++++++ src/features/user/userSlice.js | 2 +- src/pages/CreateProjectPage.jsx | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/components/NotFound.jsx diff --git a/src/components/NotFound.jsx b/src/components/NotFound.jsx new file mode 100644 index 00000000..722700b5 --- /dev/null +++ b/src/components/NotFound.jsx @@ -0,0 +1,20 @@ +import { styled } from '../theme/stitches.config'; +import { Box } from './Box'; + +const Header = styled('div', { + fontSize: '42px', + fontWeight: '$5', + fontFamily: '$roboto', + color: '$textDark', + textAlign: 'center', + paddingTop: '$8', + paddingBottom: '$8' +}); + +export const NotFound = () => { + return ( + +
Page not found
+
+ ) +} diff --git a/src/features/user/userSlice.js b/src/features/user/userSlice.js index f04ac7f5..a6c64bec 100644 --- a/src/features/user/userSlice.js +++ b/src/features/user/userSlice.js @@ -50,7 +50,7 @@ export const selectUserAuthStatus = state => state.user.authStatus; export const selectUserGroups = state => state.user.groups; export const selectUserUsername = state => state.user.username; export const selectUserProjects = state => state.user.projects; -export const selectUserIsSuperUser = state => state.user.groups.includes('animl_superuser'); +export const selectUserIsSuperUser = state => state.user.groups && state.user.groups.includes('animl_superuser'); export const selectUserHasBetaAccess = state => state.user.groups.includes('beta_access'); export const selectUserCurrentRoles = createSelector( [selectSelectedProject, selectUserProjects, selectUserIsSuperUser], diff --git a/src/pages/CreateProjectPage.jsx b/src/pages/CreateProjectPage.jsx index b4427fdc..44717602 100644 --- a/src/pages/CreateProjectPage.jsx +++ b/src/pages/CreateProjectPage.jsx @@ -1,10 +1,24 @@ import React from 'react'; +import { useSelector } from 'react-redux'; + import { Page } from '../components/Page.jsx'; +import { NotFound } from '../components/NotFound.jsx' +import { selectUserUsername, selectUserAuthStatus, selectUserIsSuperUser } from '../features/user/userSlice.js'; +import LoginForm from '../features/user/LoginForm.jsx'; const CreateProjectPage = () => { + const authStatus = useSelector(selectUserAuthStatus); + const user = useSelector(selectUserUsername); + const isSuperUser = useSelector(selectUserIsSuperUser); + const signedIn = authStatus === 'authenticated' && user; + + if (!signedIn) { + return ; + } + return ( -

Create Project

+ {isSuperUser ?

Create Project

: }
) } From a10de9f0bc1373dc6ddd319c5e05f9dbbe88bb45 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Wed, 20 Sep 2023 17:06:36 +0200 Subject: [PATCH 03/14] Add create project fields --- src/features/projects/CreateProjectForm.jsx | 107 ++++++++++++++++++++ src/pages/CreateProjectPage.jsx | 3 +- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/features/projects/CreateProjectForm.jsx diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx new file mode 100644 index 00000000..ea70fcf9 --- /dev/null +++ b/src/features/projects/CreateProjectForm.jsx @@ -0,0 +1,107 @@ +import { Formik, Form, Field } from 'formik'; +import * as Yup from 'yup'; +import { timeZonesNames } from '@vvo/tzdb'; + +import { styled } from '../../theme/stitches.config.js'; +import { FormWrapper, FormFieldWrapper, FieldRow, ButtonRow } from '../../components/Form'; +import Button from '../../components/Button.jsx'; +import SelectField from '../../components/SelectField.jsx'; + +const PageWrapper = styled('div', { + maxWidth: '600px', + padding: '0 $5', + width: '100%', + margin: '0 auto' +}); + +const Header = styled('div', { + fontSize: '24px', + fontWeight: '$5', + fontFamily: '$roboto', + paddingTop: '$8', + marginBottom: '$4' +}); + +const createProjectSchema = Yup.object().shape({ + name: Yup.string().required('Enter a project name'), + description: Yup.string().required('Enter a short description'), + timezone: Yup.string().required('Select a timezone'), + availableMLModels: Yup.string().required('Select a ML model'), +}); + +const CreateProjectForm = () => { + const tzOptions = timeZonesNames.map((tz) => ({ value: tz, label: tz })); + const mlModelOptions = [ + {value: 'megadetector_v5a', label: 'megadetector_v5a'}, + {value: 'megadetector_v5b', label: 'megadetector_v5b'}, + {value: 'mirav2', label: 'mirav2'} + ]; + + return ( + +
Create project
+ + console.log(values)} + > + {({ values, isValid, touched, setFieldTouched, setFieldValue }) => ( +
+ + + + + + + + + + + + + + + value === values.timezone)} + touched={touched.timezone} + onBlur={setFieldTouched} + onChange={(name, { value }) => setFieldValue(name, value)} + /> + + + + + value === values.availableMLModels)} + touched={touched.availableMLModels} + onChange={(name, { value }) => setFieldValue(name, value)} + onBlur={setFieldTouched} + /> + + + + + +
+ )} +
+
+
+ ); +} + +export default CreateProjectForm; diff --git a/src/pages/CreateProjectPage.jsx b/src/pages/CreateProjectPage.jsx index 44717602..887182b8 100644 --- a/src/pages/CreateProjectPage.jsx +++ b/src/pages/CreateProjectPage.jsx @@ -5,6 +5,7 @@ import { Page } from '../components/Page.jsx'; import { NotFound } from '../components/NotFound.jsx' import { selectUserUsername, selectUserAuthStatus, selectUserIsSuperUser } from '../features/user/userSlice.js'; import LoginForm from '../features/user/LoginForm.jsx'; +import CreateProjectForm from '../features/projects/CreateProjectForm.jsx'; const CreateProjectPage = () => { const authStatus = useSelector(selectUserAuthStatus); @@ -18,7 +19,7 @@ const CreateProjectPage = () => { return ( - {isSuperUser ?

Create Project

: } + {isSuperUser ? : }
) } From 61324909e95e1e41787321c7a56cadce26aac0ec Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Wed, 20 Sep 2023 17:15:51 +0200 Subject: [PATCH 04/14] Add form validation --- src/features/projects/CreateProjectForm.jsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index ea70fcf9..30f35da7 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -3,7 +3,7 @@ import * as Yup from 'yup'; import { timeZonesNames } from '@vvo/tzdb'; import { styled } from '../../theme/stitches.config.js'; -import { FormWrapper, FormFieldWrapper, FieldRow, ButtonRow } from '../../components/Form'; +import { FormWrapper, FormFieldWrapper, FieldRow, ButtonRow, FormError } from '../../components/Form'; import Button from '../../components/Button.jsx'; import SelectField from '../../components/SelectField.jsx'; @@ -51,18 +51,28 @@ const CreateProjectForm = () => { validationSchema={createProjectSchema} onSubmit={(values) => console.log(values)} > - {({ values, isValid, touched, setFieldTouched, setFieldValue }) => ( + {({ values, errors, isValid, touched, setFieldTouched, setFieldValue }) => (
+ {!!errors.name && touched.name && ( + + {errors.name} + + )} + {!!errors.description && touched.description && ( + + {errors.description} + + )} @@ -73,8 +83,9 @@ const CreateProjectForm = () => { options={tzOptions} value={tzOptions.find(({ value }) => value === values.timezone)} touched={touched.timezone} - onBlur={setFieldTouched} onChange={(name, { value }) => setFieldValue(name, value)} + onBlur={(name, { value }) => setFieldTouched(name, value)} + error={errors.timezone} /> @@ -87,7 +98,8 @@ const CreateProjectForm = () => { value={mlModelOptions.find(({ value }) => value === values.availableMLModels)} touched={touched.availableMLModels} onChange={(name, { value }) => setFieldValue(name, value)} - onBlur={setFieldTouched} + onBlur={(name, { value }) => setFieldTouched(name, value)} + error={errors.availableMLModels} /> From 4a192149738667b92500c772aaf225c30ead8d28 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Thu, 21 Sep 2023 09:34:50 +0200 Subject: [PATCH 05/14] Add project-ID creation --- src/features/projects/CreateProjectForm.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index 30f35da7..885ea7cc 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -1,6 +1,7 @@ import { Formik, Form, Field } from 'formik'; import * as Yup from 'yup'; import { timeZonesNames } from '@vvo/tzdb'; +import _ from 'lodash'; import { styled } from '../../theme/stitches.config.js'; import { FormWrapper, FormFieldWrapper, FieldRow, ButtonRow, FormError } from '../../components/Form'; @@ -49,7 +50,13 @@ const CreateProjectForm = () => { availableMLModels: '' }} validationSchema={createProjectSchema} - onSubmit={(values) => console.log(values)} + onSubmit={(values) => { + const payload = { + ...values, + _id: _.kebabCase(values.name) + } + console.log(payload); + }} > {({ values, errors, isValid, touched, setFieldTouched, setFieldValue }) => ( From a4fa69970a37d2834affbc3eec21e082039cf634 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Thu, 21 Sep 2023 11:50:53 +0200 Subject: [PATCH 06/14] WIP: Wire create-project API --- src/api/buildQuery.js | 4 +++ src/features/projects/projectsSlice.js | 50 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/api/buildQuery.js b/src/api/buildQuery.js index cb11bfe9..44bf11a8 100644 --- a/src/api/buildQuery.js +++ b/src/api/buildQuery.js @@ -165,6 +165,10 @@ const queries = { variables: { input: input } }), + createProject: (input) => ({ + // TODO: make create project query + }), + getViews: (input) => ({ template: ` { diff --git a/src/features/projects/projectsSlice.js b/src/features/projects/projectsSlice.js index 6120fa95..6eead18b 100644 --- a/src/features/projects/projectsSlice.js +++ b/src/features/projects/projectsSlice.js @@ -16,6 +16,11 @@ const initialState = { errors: null, noneFound: false, }, + createProject: { + isLoading: false, + operation: null, /* 'fetching', 'updating', 'deleting' */ + errors: null, + }, views: { isLoading: false, operation: null, @@ -52,6 +57,10 @@ export const projectsSlice = createSlice({ name: 'projects', initialState, reducers: { + /* + * Views CRUD + */ + getProjectsStart: (state) => { const ls = { isLoading: true, operation: 'fetching', errors: null }; state.loadingStates.projects = ls; @@ -109,6 +118,25 @@ export const projectsSlice = createSlice({ state.unsavedViewChanges = payload; }, + createProjectStart: (state) => { + const ls = { isLoading: true, operation: 'fetching', errors: null }; + state.loadingStates.createProject = ls; + }, + + createProjectSuccess: (state, { payload }) => { + const ls = { isLoading: false, operation: null, errors: null }; + state.loadingStates.createProject = ls; + state.projects = { + ...state.projects, + // TODO Merge new project + } + }, + + createProjectFailure: (state, { payload }) => { + const ls = { isLoading: false, operation: null, errors: payload }; + state.loadingStates.createProject = ls; + }, + /* * Views CRUD @@ -286,6 +314,9 @@ export const { setSelectedProjAndView, setUnsavedViewChanges, dismissProjectsError, + createProjectStart, + createProjectSuccess, + createProjectFailure, editViewStart, saveViewSuccess, @@ -334,6 +365,25 @@ export const fetchProjects = (payload) => async dispatch => { } }; +export const createProject = (payload) = async dispatch => { + try { + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + // TODO make this work + if (token) { + dispatch(createProjectStart()); + const project = await call({ + request: 'createProject', + ...(payload && { input: payload }) + }); + dispatch(createProjectSuccess(project)); + } + } catch (err) { + console.log('err: ', err) + dispatch(createProjectFailure(err)); + } +} + // editView thunk // TODO: maybe break this up into discrete thunks? // or take the more consolodated approach in editLabels thunk (imagesSlice.js) From 31ec6c3e95a1e1b1ac8651cd32536da9b4c89565 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Mon, 9 Oct 2023 12:14:56 +1100 Subject: [PATCH 07/14] Wire up create project API --- src/api/buildQuery.js | 11 ++++- src/components/SelectField.jsx | 2 + src/features/projects/CreateProjectForm.jsx | 52 ++++++++++++++++----- src/features/projects/projectsSlice.js | 29 +++++++++--- src/pages/CreateProjectPage.jsx | 3 +- 5 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/api/buildQuery.js b/src/api/buildQuery.js index 44bf11a8..6b717ea3 100644 --- a/src/api/buildQuery.js +++ b/src/api/buildQuery.js @@ -166,7 +166,16 @@ const queries = { }), createProject: (input) => ({ - // TODO: make create project query + template: ` + mutation CreateProject($input: CreateProjectInput!) { + createProject(input: $input) { + project { + ${projectFields} + } + } + } + `, + variables: { input: input } }), getViews: (input) => ({ diff --git a/src/components/SelectField.jsx b/src/components/SelectField.jsx index f574bac4..81fc01bd 100644 --- a/src/components/SelectField.jsx +++ b/src/components/SelectField.jsx @@ -61,6 +61,7 @@ const SelectField = ({ error, touched, isSearchable, + isMulti }) => { const handleChange = (value) => { @@ -85,6 +86,7 @@ const SelectField = ({ className='react-select' classNamePrefix='react-select' isSearchable={isSearchable} + isMulti={isMulti} /> {!!error && touched && ( diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index 885ea7cc..a9b2a844 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -1,3 +1,4 @@ +import { useDispatch, useSelector } from 'react-redux'; import { Formik, Form, Field } from 'formik'; import * as Yup from 'yup'; import { timeZonesNames } from '@vvo/tzdb'; @@ -7,6 +8,15 @@ import { styled } from '../../theme/stitches.config.js'; import { FormWrapper, FormFieldWrapper, FieldRow, ButtonRow, FormError } from '../../components/Form'; import Button from '../../components/Button.jsx'; import SelectField from '../../components/SelectField.jsx'; +import { + Toast, + ToastTitle, + ToastAction, + ToastViewport +} from '../../components/Toast'; +import IconButton from '../../components/IconButton'; +import { Cross2Icon } from '@radix-ui/react-icons'; +import { createProject, selectCreateProjectState, dismissStateMsg } from './projectsSlice.js'; const PageWrapper = styled('div', { maxWidth: '600px', @@ -27,10 +37,13 @@ const createProjectSchema = Yup.object().shape({ name: Yup.string().required('Enter a project name'), description: Yup.string().required('Enter a short description'), timezone: Yup.string().required('Select a timezone'), - availableMLModels: Yup.string().required('Select a ML model'), + availableMLModels: Yup.array().required('Select a ML model'), }); const CreateProjectForm = () => { + const dispatch = useDispatch(); + const stateMsg = useSelector(selectCreateProjectState); + const tzOptions = timeZonesNames.map((tz) => ({ value: tz, label: tz })); const mlModelOptions = [ {value: 'megadetector_v5a', label: 'megadetector_v5a'}, @@ -47,16 +60,10 @@ const CreateProjectForm = () => { name: '', description: '', timezone: '', - availableMLModels: '' + availableMLModels: [] }} validationSchema={createProjectSchema} - onSubmit={(values) => { - const payload = { - ...values, - _id: _.kebabCase(values.name) - } - console.log(payload); - }} + onSubmit={(values) => dispatch(createProject(values))} > {({ values, errors, isValid, touched, setFieldTouched, setFieldValue }) => ( @@ -102,11 +109,15 @@ const CreateProjectForm = () => { name='availableMLModels' label='Available ML models' options={mlModelOptions} - value={mlModelOptions.find(({ value }) => value === values.availableMLModels)} + value={mlModelOptions.filter(({ value }) => values.availableMLModels.includes(value))} touched={touched.availableMLModels} - onChange={(name, { value }) => setFieldValue(name, value)} + onChange={(name, value) => { + console.log(value); + setFieldValue(name, value.map((model) => model.value)) + }} onBlur={(name, { value }) => setFieldTouched(name, value)} error={errors.availableMLModels} + isMulti /> @@ -115,6 +126,25 @@ const CreateProjectForm = () => { Save + {stateMsg && ( + <> + dispatch(dismissStateMsg())} + > + + {stateMsg} + + + + + + + + + + )} )} diff --git a/src/features/projects/projectsSlice.js b/src/features/projects/projectsSlice.js index 6eead18b..5651deda 100644 --- a/src/features/projects/projectsSlice.js +++ b/src/features/projects/projectsSlice.js @@ -18,8 +18,9 @@ const initialState = { }, createProject: { isLoading: false, - operation: null, /* 'fetching', 'updating', 'deleting' */ + operation: null, errors: null, + stateMsg: null, }, views: { isLoading: false, @@ -119,21 +120,33 @@ export const projectsSlice = createSlice({ }, createProjectStart: (state) => { - const ls = { isLoading: true, operation: 'fetching', errors: null }; + const ls = { isLoading: true, operation: 'fetching', errors: null, stateMsg: null }; state.loadingStates.createProject = ls; }, createProjectSuccess: (state, { payload }) => { - const ls = { isLoading: false, operation: null, errors: null }; + const { project } = payload.createProject; + const ls = { + isLoading: false, + operation: null, + errors: null, + stateMsg: `Successfully created project ${project.name}` + }; state.loadingStates.createProject = ls; + state.projects = { ...state.projects, - // TODO Merge new project + [project._id]: project } }, createProjectFailure: (state, { payload }) => { - const ls = { isLoading: false, operation: null, errors: payload }; + const ls = { isLoading: false, operation: null, errors: payload, stateMsg: null }; + state.loadingStates.createProject = ls; + }, + + dismissStateMsg: (state) => { + const ls = { isLoading: false, operation: null, errors: null, stateMsg: null }; state.loadingStates.createProject = ls; }, @@ -317,6 +330,7 @@ export const { createProjectStart, createProjectSuccess, createProjectFailure, + dismissStateMsg, editViewStart, saveViewSuccess, @@ -365,7 +379,7 @@ export const fetchProjects = (payload) => async dispatch => { } }; -export const createProject = (payload) = async dispatch => { +export const createProject = (payload) => async dispatch => { try { const currentUser = await Auth.currentAuthenticatedUser(); const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); @@ -374,7 +388,7 @@ export const createProject = (payload) = async dispatch => { dispatch(createProjectStart()); const project = await call({ request: 'createProject', - ...(payload && { input: payload }) + input: payload }); dispatch(createProjectSuccess(project)); } @@ -575,6 +589,7 @@ export const selectProjectsErrors = state => state.projects.loadingStates.projec export const selectViewsErrors = state => state.projects.loadingStates.views.errors; export const selectDeploymentsErrors = state => state.projects.loadingStates.deployments.errors; export const selectModelsErrors = state => state.projects.loadingStates.models.errors; +export const selectCreateProjectState = state => state.projects.loadingStates.createProject.stateMsg; export default projectsSlice.reducer; diff --git a/src/pages/CreateProjectPage.jsx b/src/pages/CreateProjectPage.jsx index 887182b8..a8880392 100644 --- a/src/pages/CreateProjectPage.jsx +++ b/src/pages/CreateProjectPage.jsx @@ -10,7 +10,8 @@ import CreateProjectForm from '../features/projects/CreateProjectForm.jsx'; const CreateProjectPage = () => { const authStatus = useSelector(selectUserAuthStatus); const user = useSelector(selectUserUsername); - const isSuperUser = useSelector(selectUserIsSuperUser); + // const isSuperUser = useSelector(selectUserIsSuperUser); + const isSuperUser = true; const signedIn = authStatus === 'authenticated' && user; if (!signedIn) { From d3e9903261268a75c40fbeb5790d71362b3a1b05 Mon Sep 17 00:00:00 2001 From: Nathaniel Rindlaub Date: Fri, 20 Oct 2023 06:55:11 -0700 Subject: [PATCH 08/14] Hide create-project page from non-superusers --- src/pages/CreateProjectPage.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/CreateProjectPage.jsx b/src/pages/CreateProjectPage.jsx index a8880392..887182b8 100644 --- a/src/pages/CreateProjectPage.jsx +++ b/src/pages/CreateProjectPage.jsx @@ -10,8 +10,7 @@ import CreateProjectForm from '../features/projects/CreateProjectForm.jsx'; const CreateProjectPage = () => { const authStatus = useSelector(selectUserAuthStatus); const user = useSelector(selectUserUsername); - // const isSuperUser = useSelector(selectUserIsSuperUser); - const isSuperUser = true; + const isSuperUser = useSelector(selectUserIsSuperUser); const signedIn = authStatus === 'authenticated' && user; if (!signedIn) { From 54f7491ef40037faab9d6cb80082ac7d6c0aadea Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Mon, 23 Oct 2023 11:00:11 +1100 Subject: [PATCH 09/14] Add create-project error handling --- src/components/ErrorAlerts.jsx | 6 +++++- src/features/projects/CreateProjectForm.jsx | 5 +++-- src/features/projects/projectsSlice.js | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/ErrorAlerts.jsx b/src/components/ErrorAlerts.jsx index e41bc209..7b6cd362 100644 --- a/src/components/ErrorAlerts.jsx +++ b/src/components/ErrorAlerts.jsx @@ -22,6 +22,8 @@ import { dismissDeploymentsError, selectModelsErrors, dismissModelsError, + selectCreateProjectsErrors, + dismissCreateProjectError, } from '../features/projects/projectsSlice'; import { selectWirelessCamerasErrors, @@ -53,7 +55,7 @@ const ErrorAlerts = () => { const imageContextErrors = useSelector(selectImageContextErrors); const statsErrors = useSelector(selectStatsErrors); const exportErrors = useSelector(selectExportErrors); - + const createProjectErrors = useSelector(selectCreateProjectsErrors); const enrichedErrors = [ enrichErrors(labelsErrors, 'Label Error', 'labels'), @@ -66,6 +68,7 @@ const ErrorAlerts = () => { enrichErrors(imageContextErrors, 'Image Error', 'imageContext'), enrichErrors(statsErrors, 'Error Getting Stats', 'stats'), enrichErrors(exportErrors, 'Error Exporting Data', 'data'), + enrichErrors(createProjectErrors, 'Error Creating Project', 'createProject'), ]; const errors = enrichedErrors.reduce((acc, curr) => ( @@ -114,6 +117,7 @@ const ErrorAlerts = () => { const dismissErrorActions = { 'labels': (i) => dismissLabelsError(i), 'projects': (i) => dismissProjectsError(i), + 'createProject': (i) => dismissCreateProjectError(i), 'views': (i) => dismissViewsError(i), 'deployments': (i) => dismissDeploymentsError(i), 'models': (i) => dismissModelsError(i), diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index a9b2a844..0645c4c9 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -3,6 +3,7 @@ import { Formik, Form, Field } from 'formik'; import * as Yup from 'yup'; import { timeZonesNames } from '@vvo/tzdb'; import _ from 'lodash'; +import { Cross2Icon } from '@radix-ui/react-icons'; import { styled } from '../../theme/stitches.config.js'; import { FormWrapper, FormFieldWrapper, FieldRow, ButtonRow, FormError } from '../../components/Form'; @@ -15,7 +16,7 @@ import { ToastViewport } from '../../components/Toast'; import IconButton from '../../components/IconButton'; -import { Cross2Icon } from '@radix-ui/react-icons'; +import ErrorAlerts from '../../components/ErrorAlerts.jsx'; import { createProject, selectCreateProjectState, dismissStateMsg } from './projectsSlice.js'; const PageWrapper = styled('div', { @@ -112,7 +113,6 @@ const CreateProjectForm = () => { value={mlModelOptions.filter(({ value }) => values.availableMLModels.includes(value))} touched={touched.availableMLModels} onChange={(name, value) => { - console.log(value); setFieldValue(name, value.map((model) => model.value)) }} onBlur={(name, { value }) => setFieldTouched(name, value)} @@ -149,6 +149,7 @@ const CreateProjectForm = () => { )} + ); } diff --git a/src/features/projects/projectsSlice.js b/src/features/projects/projectsSlice.js index 5651deda..a209a0c2 100644 --- a/src/features/projects/projectsSlice.js +++ b/src/features/projects/projectsSlice.js @@ -145,6 +145,11 @@ export const projectsSlice = createSlice({ state.loadingStates.createProject = ls; }, + dismissCreateProjectError: (state, { payload }) => { + const index = payload; + state.loadingStates.createProject.errors.splice(index, 1); + }, + dismissStateMsg: (state) => { const ls = { isLoading: false, operation: null, errors: null, stateMsg: null }; state.loadingStates.createProject = ls; @@ -330,6 +335,7 @@ export const { createProjectStart, createProjectSuccess, createProjectFailure, + dismissCreateProjectError, dismissStateMsg, editViewStart, @@ -590,6 +596,7 @@ export const selectViewsErrors = state => state.projects.loadingStates.views.err export const selectDeploymentsErrors = state => state.projects.loadingStates.deployments.errors; export const selectModelsErrors = state => state.projects.loadingStates.models.errors; export const selectCreateProjectState = state => state.projects.loadingStates.createProject.stateMsg; +export const selectCreateProjectsErrors = state => state.projects.loadingStates.createProject.errors; export default projectsSlice.reducer; From 64582fb1b9364c926c13e8ee8c1c082baada3b3d Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Mon, 23 Oct 2023 11:02:07 +1100 Subject: [PATCH 10/14] Fix validation of ML model field --- src/features/projects/CreateProjectForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index 0645c4c9..bfb53a8d 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -38,7 +38,7 @@ const createProjectSchema = Yup.object().shape({ name: Yup.string().required('Enter a project name'), description: Yup.string().required('Enter a short description'), timezone: Yup.string().required('Select a timezone'), - availableMLModels: Yup.array().required('Select a ML model'), + availableMLModels: Yup.array().min(1, "Select at least one ML model").required('Select a ML model'), }); const CreateProjectForm = () => { From 253c1259213999731eb594e1a4a54d1a0479cb44 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Mon, 23 Oct 2023 12:09:26 +1100 Subject: [PATCH 11/14] Fetch ML model options --- src/features/projects/CreateProjectForm.jsx | 26 ++++++++--- src/features/projects/projectsSlice.js | 48 ++++++++++++++++++++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index bfb53a8d..3f66eacf 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -1,3 +1,4 @@ +import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Formik, Form, Field } from 'formik'; import * as Yup from 'yup'; @@ -17,7 +18,13 @@ import { } from '../../components/Toast'; import IconButton from '../../components/IconButton'; import ErrorAlerts from '../../components/ErrorAlerts.jsx'; -import { createProject, selectCreateProjectState, dismissStateMsg } from './projectsSlice.js'; +import { + createProject, + selectCreateProjectState, + dismissStateMsg, + fetchModelOptions, + selectModelOptions +} from './projectsSlice.js'; const PageWrapper = styled('div', { maxWidth: '600px', @@ -44,13 +51,20 @@ const createProjectSchema = Yup.object().shape({ const CreateProjectForm = () => { const dispatch = useDispatch(); const stateMsg = useSelector(selectCreateProjectState); + const mlModels = useSelector(selectModelOptions); const tzOptions = timeZonesNames.map((tz) => ({ value: tz, label: tz })); - const mlModelOptions = [ - {value: 'megadetector_v5a', label: 'megadetector_v5a'}, - {value: 'megadetector_v5b', label: 'megadetector_v5b'}, - {value: 'mirav2', label: 'mirav2'} - ]; + const mlModelOptions = useMemo( + () => mlModels.map(({ _id, description }) => ({ + value: _id, + label: description + })), + [mlModels] + ); + + useEffect(() => { + dispatch(fetchModelOptions()); + }, []); return ( diff --git a/src/features/projects/projectsSlice.js b/src/features/projects/projectsSlice.js index a209a0c2..04bf2d74 100644 --- a/src/features/projects/projectsSlice.js +++ b/src/features/projects/projectsSlice.js @@ -9,6 +9,7 @@ import { const initialState = { projects: [], + modelOptions: [], loadingStates: { projects: { isLoading: false, @@ -42,6 +43,11 @@ const initialState = { operation: null, errors: null, }, + modelsOptions: { + isLoading: false, + operation: null, + errors: null, + }, uploads: { isLoading: false, operation: null, @@ -288,6 +294,22 @@ export const projectsSlice = createSlice({ }); }, + getModelOptionsStart: (state) => { + const ls = { isLoading: true, operation: 'fetching', errors: null }; + state.loadingStates.modelOptions = ls; + }, + + getModelOptionsFailure: (state, { payload }) => { + const ls = { isLoading: false, operation: null, errors: payload }; + state.loadingStates.modelOptions = ls; + }, + + getModelOptionsSuccess: (state, { payload }) => { + const ls = { isLoading: false, operation: null, errors: null }; + state.loadingStates.modelOptions = ls; + state.modelOptions = payload; + }, + setModalOpen: (state, { payload }) => { state.modalOpen = payload; }, @@ -358,6 +380,9 @@ export const { getModelsFailure, getModelsSuccess, dismissModelsError, + getModelOptionsStart, + getModelOptionsFailure, + getModelOptionsSuccess, setModalOpen, setModalContent, @@ -564,6 +589,27 @@ export const fetchModels = (payload) => { }; } +export const fetchModelOptions = () => { + return async (dispatch) => { + try { + dispatch(getModelsStart()); + const currentUser = await Auth.currentAuthenticatedUser(); + const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); + + if (token) { + const res = await call({ + request: 'getModels', + input: {}, + }); + dispatch(getModelOptionsSuccess(res.mlModels)); + } + + } catch (err) { + dispatch(getModelOptionsFailure(err)); + } + }; +} + // Selectors export const selectProjects = state => state.projects.projects; @@ -597,6 +643,6 @@ export const selectDeploymentsErrors = state => state.projects.loadingStates.dep export const selectModelsErrors = state => state.projects.loadingStates.models.errors; export const selectCreateProjectState = state => state.projects.loadingStates.createProject.stateMsg; export const selectCreateProjectsErrors = state => state.projects.loadingStates.createProject.errors; - +export const selectModelOptions = state => state.projects.modelOptions; export default projectsSlice.reducer; From 5cd9cede5fdffe0daccd03e83dbd174b3dacc19d Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Mon, 23 Oct 2023 12:19:43 +1100 Subject: [PATCH 12/14] Add loading spinner --- src/features/projects/CreateProjectForm.jsx | 11 ++++++++++- src/features/projects/projectsSlice.js | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index 3f66eacf..e7e63955 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -18,12 +18,15 @@ import { } from '../../components/Toast'; import IconButton from '../../components/IconButton'; import ErrorAlerts from '../../components/ErrorAlerts.jsx'; +import { SimpleSpinner, SpinnerOverlay } from '../../components/Spinner.jsx'; + import { createProject, selectCreateProjectState, dismissStateMsg, fetchModelOptions, - selectModelOptions + selectModelOptions, + selectCreateProjectLoading } from './projectsSlice.js'; const PageWrapper = styled('div', { @@ -52,6 +55,7 @@ const CreateProjectForm = () => { const dispatch = useDispatch(); const stateMsg = useSelector(selectCreateProjectState); const mlModels = useSelector(selectModelOptions); + const isLoading = useSelector(selectCreateProjectLoading) const tzOptions = timeZonesNames.map((tz) => ({ value: tz, label: tz })); const mlModelOptions = useMemo( @@ -68,6 +72,11 @@ const CreateProjectForm = () => { return ( + {isLoading && + + + + }
Create project
state.projects.loadingStates.dep export const selectModelsErrors = state => state.projects.loadingStates.models.errors; export const selectCreateProjectState = state => state.projects.loadingStates.createProject.stateMsg; export const selectCreateProjectsErrors = state => state.projects.loadingStates.createProject.errors; +export const selectCreateProjectLoading = state => state.projects.loadingStates.createProject.isLoading; export const selectModelOptions = state => state.projects.modelOptions; export default projectsSlice.reducer; From 819aa4365da1d35d2fcfb4a605fca9bd440fc104 Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Mon, 23 Oct 2023 12:28:33 +1100 Subject: [PATCH 13/14] Fix merging new project --- src/features/projects/projectsSlice.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/features/projects/projectsSlice.js b/src/features/projects/projectsSlice.js index e9d7e194..2e119a17 100644 --- a/src/features/projects/projectsSlice.js +++ b/src/features/projects/projectsSlice.js @@ -140,10 +140,10 @@ export const projectsSlice = createSlice({ }; state.loadingStates.createProject = ls; - state.projects = { + state.projects = [ ...state.projects, - [project._id]: project - } + project + ]; }, createProjectFailure: (state, { payload }) => { @@ -613,9 +613,12 @@ export const fetchModelOptions = () => { // Selectors export const selectProjects = state => state.projects.projects; -export const selectSelectedProject = state => ( - state.projects.projects.find((proj) => proj.selected) -); +export const selectSelectedProject = state => { + console.log(state); + return ( + state.projects.projects.find((proj) => proj.selected) + ) +}; export const selectSelectedProjectId = createSelector([selectSelectedProject], (proj) => proj ? proj._id : null ); From 5e69919420c62b5e38d58419fd21ed10273d7b3b Mon Sep 17 00:00:00 2001 From: Nathaniel Rindlaub Date: Mon, 23 Oct 2023 14:35:57 -0700 Subject: [PATCH 14/14] Display spinner while ml model options are loading --- src/features/projects/CreateProjectForm.jsx | 8 +++++--- src/features/projects/ProjectAndViewNav.jsx | 1 - src/features/projects/projectsSlice.js | 15 +++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/features/projects/CreateProjectForm.jsx b/src/features/projects/CreateProjectForm.jsx index e7e63955..fa3e78cc 100644 --- a/src/features/projects/CreateProjectForm.jsx +++ b/src/features/projects/CreateProjectForm.jsx @@ -26,7 +26,8 @@ import { dismissStateMsg, fetchModelOptions, selectModelOptions, - selectCreateProjectLoading + selectCreateProjectLoading, + selectModelOptionsLoading } from './projectsSlice.js'; const PageWrapper = styled('div', { @@ -55,7 +56,8 @@ const CreateProjectForm = () => { const dispatch = useDispatch(); const stateMsg = useSelector(selectCreateProjectState); const mlModels = useSelector(selectModelOptions); - const isLoading = useSelector(selectCreateProjectLoading) + const createProjectIsLoading = useSelector(selectCreateProjectLoading); + const mlModelsOptionsIsLoading = useSelector(selectModelOptionsLoading) const tzOptions = timeZonesNames.map((tz) => ({ value: tz, label: tz })); const mlModelOptions = useMemo( @@ -72,7 +74,7 @@ const CreateProjectForm = () => { return ( - {isLoading && + {(createProjectIsLoading || mlModelsOptionsIsLoading) && diff --git a/src/features/projects/ProjectAndViewNav.jsx b/src/features/projects/ProjectAndViewNav.jsx index 8286301d..24f245f6 100644 --- a/src/features/projects/ProjectAndViewNav.jsx +++ b/src/features/projects/ProjectAndViewNav.jsx @@ -196,7 +196,6 @@ const ProjectAndViewNav = () => { // kick off pre-focused-image initialization sequence const query = routerLocation.query; if ('img' in query && checkIfValidMD5Hash(query.img.split(':')[1])) { - console.log('img found in query'); dispatch(preFocusImageStart(query.img)); dispatch(fetchImageContext(query.img)); } diff --git a/src/features/projects/projectsSlice.js b/src/features/projects/projectsSlice.js index 2e119a17..b74b67d4 100644 --- a/src/features/projects/projectsSlice.js +++ b/src/features/projects/projectsSlice.js @@ -43,7 +43,7 @@ const initialState = { operation: null, errors: null, }, - modelsOptions: { + modelOptions: { isLoading: false, operation: null, errors: null, @@ -295,6 +295,7 @@ export const projectsSlice = createSlice({ }, getModelOptionsStart: (state) => { + console.log('getModelOptionsStart') const ls = { isLoading: true, operation: 'fetching', errors: null }; state.loadingStates.modelOptions = ls; }, @@ -305,6 +306,7 @@ export const projectsSlice = createSlice({ }, getModelOptionsSuccess: (state, { payload }) => { + console.log('getModelOptionsSuccess') const ls = { isLoading: false, operation: null, errors: null }; state.loadingStates.modelOptions = ls; state.modelOptions = payload; @@ -592,7 +594,7 @@ export const fetchModels = (payload) => { export const fetchModelOptions = () => { return async (dispatch) => { try { - dispatch(getModelsStart()); + dispatch(getModelOptionsStart()); const currentUser = await Auth.currentAuthenticatedUser(); const token = currentUser.getSignInUserSession().getIdToken().getJwtToken(); @@ -613,12 +615,7 @@ export const fetchModelOptions = () => { // Selectors export const selectProjects = state => state.projects.projects; -export const selectSelectedProject = state => { - console.log(state); - return ( - state.projects.projects.find((proj) => proj.selected) - ) -}; +export const selectSelectedProject = state => state.projects.projects.find((proj) => proj.selected); export const selectSelectedProjectId = createSelector([selectSelectedProject], (proj) => proj ? proj._id : null ); @@ -648,5 +645,7 @@ export const selectCreateProjectState = state => state.projects.loadingStates.cr export const selectCreateProjectsErrors = state => state.projects.loadingStates.createProject.errors; export const selectCreateProjectLoading = state => state.projects.loadingStates.createProject.isLoading; export const selectModelOptions = state => state.projects.modelOptions; +export const selectModelOptionsLoading = state => state.projects.loadingStates.modelOptions.isLoading; + export default projectsSlice.reducer;