From f7cbeb586c8a03cb72b35e9b0b7429579e72b604 Mon Sep 17 00:00:00 2001 From: Akosah Date: Fri, 19 Jan 2024 13:17:40 -0500 Subject: [PATCH 1/3] create dataset --- .../src/components/AddDataset.component.tsx | 60 ++++++++++++-- .../src/components/SideBar.component.tsx | 1 - .../src/graphql/dataset/dataset.graphql | 11 +++ .../client/src/graphql/dataset/dataset.ts | 83 ++++++++++++++++++- packages/client/src/graphql/graphql.ts | 6 ++ .../src/pages/datasets/DatasetControls.tsx | 14 +++- packages/server/schema.gql | 1 + .../server/src/dataset/dataset.resolver.ts | 9 ++ .../server/src/dataset/dataset.service.ts | 5 ++ 9 files changed, 176 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/AddDataset.component.tsx b/packages/client/src/components/AddDataset.component.tsx index 8ec4af1c..e69b223a 100644 --- a/packages/client/src/components/AddDataset.component.tsx +++ b/packages/client/src/components/AddDataset.component.tsx @@ -2,13 +2,16 @@ import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { JsonForms } from '@jsonforms/react'; import { materialRenderers, materialCells } from '@jsonforms/material-renderers'; +import { useCreateDatasetMutation, useDatasetExistsLazyQuery } from '../graphql/dataset/dataset'; +import { Button } from '@mui/material'; +import { ErrorObject } from 'ajv'; interface ShowProps { show: boolean; - toggleModal: () => void; + toggleModal: (newDatasetCreated: boolean) => void; } const schema = { @@ -16,6 +19,7 @@ const schema = { properties: { name: { type: 'string', + pattern: '^[a-zA-Z 0-9]*$', description: 'Please enter new dataset name' }, description: { @@ -44,16 +48,46 @@ const uischema = { export const AddDataset: React.FC = (props: ShowProps) => { const [error, setError] = useState(true); + const [additionalErrors, setAdditionalErrors] = useState([]); + const [datasetExistsQuery, datasetExistsResults] = useDatasetExistsLazyQuery(); + const initialData = { name: '', description: '' }; const [data, setData] = useState(initialData); + const [createDataset, { data: createDatasetResults, loading }] = useCreateDatasetMutation({ + variables: { dataset: data } + }); + + useEffect(() => { + if (datasetExistsResults.data?.datasetExists) { + setAdditionalErrors([ + { + instancePath: '/name', + keyword: 'uniqueProjectName', + message: 'A project with this name already exists', + schemaPath: '#/properties/name/name', + params: { keyword: 'uniqueProjectName' } + } + ]); + } else { + setAdditionalErrors([]); + } + }, [datasetExistsResults.data]); + + useEffect(() => { + if (createDatasetResults?.createDataset) { + props.toggleModal(true); + } + //TODO handle creation server error with snackbar + }, [createDatasetResults]); - const handleChange = (data: any) => { + const handleChange = (data: any, errors: ErrorObject[] | undefined) => { setData(data); - if (data.name.length > 1 && data.description.length > 1) { + if (!errors || errors.length === 0) { + datasetExistsQuery({ variables: { name: data.name } }); setError(false); } else { setError(true); @@ -71,16 +105,24 @@ export const AddDataset: React.FC = (props: ShowProps) => { data={data} renderers={materialRenderers} cells={materialCells} - onChange={({ data }) => handleChange(data)} + onChange={({ data, errors }) => handleChange(data, errors)} + additionalErrors={additionalErrors} /> - - + + diff --git a/packages/client/src/components/SideBar.component.tsx b/packages/client/src/components/SideBar.component.tsx index 25f98c6c..cd025dd7 100644 --- a/packages/client/src/components/SideBar.component.tsx +++ b/packages/client/src/components/SideBar.component.tsx @@ -51,7 +51,6 @@ export const SideBar: FC = ({ open, drawerWidth }) => { { name: 'New Study', action: () => navigate('/study/new'), visible: (p) => p!.projectAdmin }, { name: 'Study Control', action: () => navigate('/study/controls'), visible: (p) => p!.projectAdmin }, { name: 'User Permissions', action: () => navigate('/study/permissions'), visible: (p) => p!.studyAdmin }, - { name: 'Entry Controls', action: () => navigate('/study/controls'), visible: (p) => p!.studyAdmin }, { name: 'Entry Controls', action: () => navigate('/study/entries'), visible: (p) => p!.studyAdmin }, { name: 'Download Tags', action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin } ] diff --git a/packages/client/src/graphql/dataset/dataset.graphql b/packages/client/src/graphql/dataset/dataset.graphql index f57196a0..61f8bd7c 100644 --- a/packages/client/src/graphql/dataset/dataset.graphql +++ b/packages/client/src/graphql/dataset/dataset.graphql @@ -6,6 +6,10 @@ query getDatasets { } } +query datasetExists($name: String!) { + datasetExists(name: $name) +} + query getDatasetsByProject($project: ID!) { getDatasetsByProject(project: $project) { _id @@ -13,3 +17,10 @@ query getDatasetsByProject($project: ID!) { description } } + +mutation createDataset($dataset: DatasetCreate!) { + createDataset(dataset: $dataset) { + name + description + } +} diff --git a/packages/client/src/graphql/dataset/dataset.ts b/packages/client/src/graphql/dataset/dataset.ts index 8081d99d..5721b522 100644 --- a/packages/client/src/graphql/dataset/dataset.ts +++ b/packages/client/src/graphql/dataset/dataset.ts @@ -10,6 +10,13 @@ export type GetDatasetsQueryVariables = Types.Exact<{ [key: string]: never; }>; export type GetDatasetsQuery = { __typename?: 'Query', getDatasets: Array<{ __typename?: 'Dataset', _id: string, name: string, description: string }> }; +export type DatasetExistsQueryVariables = Types.Exact<{ + name: Types.Scalars['String']['input']; +}>; + + +export type DatasetExistsQuery = { __typename?: 'Query', datasetExists: boolean }; + export type GetDatasetsByProjectQueryVariables = Types.Exact<{ project: Types.Scalars['ID']['input']; }>; @@ -17,6 +24,13 @@ export type GetDatasetsByProjectQueryVariables = Types.Exact<{ export type GetDatasetsByProjectQuery = { __typename?: 'Query', getDatasetsByProject: Array<{ __typename?: 'Dataset', _id: string, name: string, description: string }> }; +export type CreateDatasetMutationVariables = Types.Exact<{ + dataset: Types.DatasetCreate; +}>; + + +export type CreateDatasetMutation = { __typename?: 'Mutation', createDataset: { __typename?: 'Dataset', name: string, description: string } }; + export const GetDatasetsDocument = gql` query getDatasets { @@ -54,6 +68,39 @@ export function useGetDatasetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOption export type GetDatasetsQueryHookResult = ReturnType; export type GetDatasetsLazyQueryHookResult = ReturnType; export type GetDatasetsQueryResult = Apollo.QueryResult; +export const DatasetExistsDocument = gql` + query datasetExists($name: String!) { + datasetExists(name: $name) +} + `; + +/** + * __useDatasetExistsQuery__ + * + * To run a query within a React component, call `useDatasetExistsQuery` and pass it any options that fit your needs. + * When your component renders, `useDatasetExistsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useDatasetExistsQuery({ + * variables: { + * name: // value for 'name' + * }, + * }); + */ +export function useDatasetExistsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(DatasetExistsDocument, options); + } +export function useDatasetExistsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(DatasetExistsDocument, options); + } +export type DatasetExistsQueryHookResult = ReturnType; +export type DatasetExistsLazyQueryHookResult = ReturnType; +export type DatasetExistsQueryResult = Apollo.QueryResult; export const GetDatasetsByProjectDocument = gql` query getDatasetsByProject($project: ID!) { getDatasetsByProject(project: $project) { @@ -90,4 +137,38 @@ export function useGetDatasetsByProjectLazyQuery(baseOptions?: Apollo.LazyQueryH } export type GetDatasetsByProjectQueryHookResult = ReturnType; export type GetDatasetsByProjectLazyQueryHookResult = ReturnType; -export type GetDatasetsByProjectQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetDatasetsByProjectQueryResult = Apollo.QueryResult; +export const CreateDatasetDocument = gql` + mutation createDataset($dataset: DatasetCreate!) { + createDataset(dataset: $dataset) { + name + description + } +} + `; +export type CreateDatasetMutationFn = Apollo.MutationFunction; + +/** + * __useCreateDatasetMutation__ + * + * To run a mutation, you first call `useCreateDatasetMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateDatasetMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createDatasetMutation, { data, loading, error }] = useCreateDatasetMutation({ + * variables: { + * dataset: // value for 'dataset' + * }, + * }); + */ +export function useCreateDatasetMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateDatasetDocument, options); + } +export type CreateDatasetMutationHookResult = ReturnType; +export type CreateDatasetMutationResult = Apollo.MutationResult; +export type CreateDatasetMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 5cd2f0fe..b3eb8eef 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -552,6 +552,7 @@ export type ProjectSettingsModel = { export type Query = { __typename?: 'Query'; + datasetExists: Scalars['Boolean']['output']; entryForDataset: Array; exists: Scalars['Boolean']['output']; findStudies: Array; @@ -585,6 +586,11 @@ export type Query = { }; +export type QueryDatasetExistsArgs = { + name: Scalars['String']['input']; +}; + + export type QueryEntryForDatasetArgs = { dataset: Scalars['ID']['input']; }; diff --git a/packages/client/src/pages/datasets/DatasetControls.tsx b/packages/client/src/pages/datasets/DatasetControls.tsx index ce0ce7da..44164ab5 100644 --- a/packages/client/src/pages/datasets/DatasetControls.tsx +++ b/packages/client/src/pages/datasets/DatasetControls.tsx @@ -4,7 +4,7 @@ import { AddDataset } from '../../components/AddDataset.component'; import { useEffect, useState } from 'react'; import { UploadEntries } from '../../components/UploadEntries.component'; import { Dataset } from '../../graphql/graphql'; -import { useGetDatasetsQuery } from '../../graphql/dataset/dataset'; +import { useGetDatasetsLazyQuery } from '../../graphql/dataset/dataset'; import { DatasetsView } from '../../components/DatasetsView.component'; import { GridColDef, GridActionsCellItem, GridRowId } from '@mui/x-data-grid'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; @@ -15,10 +15,15 @@ export const DatasetControls: React.FC = () => { const [add, setAdd] = useState(false); const [upload, setUpload] = useState(false); const [datasets, setDatasets] = useState([]); - const getDatasetsResults = useGetDatasetsQuery(); + const [getDatasets, getDatasetsResults] = useGetDatasetsLazyQuery(); const [deleteEntryMutation] = useDeleteEntryMutation(); const confirmation = useConfirmation(); + + useEffect(() => { + getDatasets(); + }, []); + useEffect(() => { if (getDatasetsResults.data) { setDatasets(getDatasetsResults.data.getDatasets); @@ -33,8 +38,11 @@ export const DatasetControls: React.FC = () => { } }; - const toggleAdd = () => { + const toggleAdd = (newDatasetCreated: boolean) => { setAdd((add) => !add); + if (newDatasetCreated) { + getDatasets({ fetchPolicy: 'network-only' }); + } }; const toggleUpload = () => { diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 569e693b..00e8bab4 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -141,6 +141,7 @@ type Query { getOrganizations: [Organization!]! exists(name: String!): Boolean! getDatasets: [Dataset!]! + datasetExists(name: String!): Boolean! getDatasetsByProject(project: ID!): [Dataset!]! getProjectPermissions(project: ID!): [ProjectPermissionModel!]! getStudyPermissions(study: ID!): [StudyPermissionModel!]! diff --git a/packages/server/src/dataset/dataset.resolver.ts b/packages/server/src/dataset/dataset.resolver.ts index 0d0cedf7..c0a7cd51 100644 --- a/packages/server/src/dataset/dataset.resolver.ts +++ b/packages/server/src/dataset/dataset.resolver.ts @@ -32,6 +32,15 @@ export class DatasetResolver { return this.datasetService.findAll(organization._id); } + @Query(() => Boolean) + async datasetExists( + @Args('name') name: string, + @OrganizationContext() organization: Organization, + @TokenContext() _user: TokenPayload + ): Promise { + return this.datasetService.exists(name, organization._id); + } + @Mutation(() => Dataset) async createDataset( @Args('dataset') dataset: DatasetCreate, diff --git a/packages/server/src/dataset/dataset.service.ts b/packages/server/src/dataset/dataset.service.ts index b0d3245f..d21e126f 100644 --- a/packages/server/src/dataset/dataset.service.ts +++ b/packages/server/src/dataset/dataset.service.ts @@ -30,6 +30,11 @@ export class DatasetService { return this.datasetModel.findOne({ organization, name }); } + async exists(name: string, organization: string): Promise { + const dataset = await this.datasetModel.findOne({ name, organization }); + return !!dataset; + } + async create(organization: string, datasetCreate: DatasetCreate): Promise { // Create the dataset const dataset = await this.datasetModel.create({ ...datasetCreate, organization }); From 6eb28db39f38cc5be4631af8ed608fce9f215d9f Mon Sep 17 00:00:00 2001 From: Akosah Date: Fri, 19 Jan 2024 14:40:06 -0500 Subject: [PATCH 2/3] minor change --- .../src/components/AddDataset.component.tsx | 22 +++++++++---------- .../server/src/dataset/dataset.resolver.ts | 5 ++++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/client/src/components/AddDataset.component.tsx b/packages/client/src/components/AddDataset.component.tsx index e69b223a..163d6b57 100644 --- a/packages/client/src/components/AddDataset.component.tsx +++ b/packages/client/src/components/AddDataset.component.tsx @@ -19,7 +19,6 @@ const schema = { properties: { name: { type: 'string', - pattern: '^[a-zA-Z 0-9]*$', description: 'Please enter new dataset name' }, description: { @@ -51,15 +50,10 @@ export const AddDataset: React.FC = (props: ShowProps) => { const [additionalErrors, setAdditionalErrors] = useState([]); const [datasetExistsQuery, datasetExistsResults] = useDatasetExistsLazyQuery(); - const initialData = { - name: '', - description: '' - }; + const initialData = {} as { name: string; description: string }; const [data, setData] = useState(initialData); - const [createDataset, { data: createDatasetResults, loading }] = useCreateDatasetMutation({ - variables: { dataset: data } - }); + const [createDataset, { data: createDatasetResults, loading }] = useCreateDatasetMutation(); useEffect(() => { if (datasetExistsResults.data?.datasetExists) { @@ -67,7 +61,7 @@ export const AddDataset: React.FC = (props: ShowProps) => { { instancePath: '/name', keyword: 'uniqueProjectName', - message: 'A project with this name already exists', + message: 'A dataset with this name already exists', schemaPath: '#/properties/name/name', params: { keyword: 'uniqueProjectName' } } @@ -85,15 +79,21 @@ export const AddDataset: React.FC = (props: ShowProps) => { }, [createDatasetResults]); const handleChange = (data: any, errors: ErrorObject[] | undefined) => { + console.log('data', data); setData(data); if (!errors || errors.length === 0) { datasetExistsQuery({ variables: { name: data.name } }); + console.log('setting error to false'); setError(false); } else { setError(true); } }; + const onCreate = () => { + createDataset({ variables: { dataset: data } }); + }; + return (
@@ -116,9 +116,7 @@ export const AddDataset: React.FC = (props: ShowProps) => {