Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/client/src/components/DatasetsView.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Accordion, AccordionSummary, Typography, Stack, AccordionDetails } from
import { Dataset } from '../graphql/graphql';
import { DatasetTable } from './DatasetTable.component';
import { ExpandMore } from '@mui/icons-material';
import { GridColDef } from '@mui/x-data-grid';

export interface DatasetsViewProps {
datasets: Dataset[];
additionalColumns?: GridColDef[];
}

// TODO: Implement lazy loading on accordion open to prevent loading all datasets at once
export const DatasetsView: React.FC<DatasetsViewProps> = ({ datasets }) => {
export const DatasetsView: React.FC<DatasetsViewProps> = ({ datasets, additionalColumns }) => {
return (
<>
{datasets.map((dataset: Dataset) => (
Expand All @@ -20,7 +22,7 @@ export const DatasetsView: React.FC<DatasetsViewProps> = ({ datasets }) => {
</Stack>
</AccordionSummary>
<AccordionDetails>
<DatasetTable dataset={dataset} />
<DatasetTable dataset={dataset} additionalColumns={additionalColumns} />
</AccordionDetails>
</Accordion>
))}
Expand Down
91 changes: 87 additions & 4 deletions packages/client/src/components/TagTraining.component.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,68 @@
import { DatasetsView } from './DatasetsView.component';
import { useState, useEffect } from 'react';
import { useState, useEffect, SetStateAction, Dispatch } from 'react';
import { useGetDatasetsQuery } from '../graphql/dataset/dataset';
import { Dataset } from '../graphql/graphql';
import { Dataset, Entry } from '../graphql/graphql';
import { GridColDef } from '@mui/x-data-grid';
import { Switch } from '@mui/material';

export const TagTrainingComponent = () => {
export interface TagTrainingComponentProps {
setTrainingSet: Dispatch<SetStateAction<string[]>>;
setTaggingSet: Dispatch<SetStateAction<string[]>>;
}

export const TagTrainingComponent: React.FC<TagTrainingComponentProps> = (props) => {
const [datasets, setDatasets] = useState<Dataset[]>([]);
const getDatasetsResults = useGetDatasetsQuery();

const trainingSet: Set<string> = new Set();
const fullSet: Set<string> = new Set();

const additionalColumns: GridColDef[] = [
{
field: 'training',
headerName: 'Training',
width: 200,
renderCell: (params) => (
<EditSetSwitch
startingValue={false}
onLoad={(_entry) => {}}
add={(entry) => {
trainingSet.add(entry._id);
props.setTrainingSet(Array.from(trainingSet));
}}
remove={(entry) => {
trainingSet.delete(entry._id);
props.setTrainingSet(Array.from(trainingSet));
}}
entry={params.row}
/>
)
},
{
field: 'full',
headerName: 'Available for Tagging',
width: 200,
renderCell: (params) => (
<EditSetSwitch
startingValue={true}
onLoad={(entry) => {
fullSet.add(entry._id);
props.setTaggingSet(Array.from(fullSet));
}}
add={(entry) => {
fullSet.add(entry._id);
props.setTaggingSet(Array.from(fullSet));
}}
remove={(entry) => {
fullSet.delete(entry._id);
props.setTaggingSet(Array.from(fullSet));
}}
entry={params.row}
/>
)
}
];

// TODO: In the future, the datasets retrieved should only be datasets
// accessible by the current project
useEffect(() => {
Expand All @@ -17,7 +73,34 @@ export const TagTrainingComponent = () => {

return (
<>
<DatasetsView datasets={datasets} />
<DatasetsView datasets={datasets} additionalColumns={additionalColumns} />
</>
);
};

interface EditSwitchProps {
startingValue: boolean;
add: (entry: Entry) => void;
remove: (entry: Entry) => void;
onLoad: (entry: Entry) => void;
entry: Entry;
}

const EditSetSwitch: React.FC<EditSwitchProps> = (props) => {
const [checked, setChecked] = useState(props.startingValue);

useEffect(() => {
props.onLoad(props.entry);
}, [props.entry]);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
if (event.target.checked) {
props.add(props.entry);
} else {
props.remove(props.entry);
}
};

return <Switch checked={checked} onChange={handleChange} inputProps={{ 'aria-label': 'controlled' }} />;
};
5 changes: 5 additions & 0 deletions packages/client/src/graphql/tag.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation createTags($study: ID!, $entries: [ID!]!) {
createTags(study: $study, entries: $entries) {
_id
}
}
50 changes: 50 additions & 0 deletions packages/client/src/graphql/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* Generated File DO NOT EDIT. */
/* tslint:disable */
import * as Types from './graphql';

import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type CreateTagsMutationVariables = Types.Exact<{
study: Types.Scalars['ID']['input'];
entries: Array<Types.Scalars['ID']['input']> | Types.Scalars['ID']['input'];
}>;


export type CreateTagsMutation = { __typename?: 'Mutation', createTags: Array<{ __typename?: 'Tag', _id: string }> };


export const CreateTagsDocument = gql`
mutation createTags($study: ID!, $entries: [ID!]!) {
createTags(study: $study, entries: $entries) {
_id
}
}
`;
export type CreateTagsMutationFn = Apollo.MutationFunction<CreateTagsMutation, CreateTagsMutationVariables>;

/**
* __useCreateTagsMutation__
*
* To run a mutation, you first call `useCreateTagsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateTagsMutation` 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 [createTagsMutation, { data, loading, error }] = useCreateTagsMutation({
* variables: {
* study: // value for 'study'
* entries: // value for 'entries'
* },
* });
*/
export function useCreateTagsMutation(baseOptions?: Apollo.MutationHookOptions<CreateTagsMutation, CreateTagsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateTagsMutation, CreateTagsMutationVariables>(CreateTagsDocument, options);
}
export type CreateTagsMutationHookResult = ReturnType<typeof useCreateTagsMutation>;
export type CreateTagsMutationResult = Apollo.MutationResult<CreateTagsMutation>;
export type CreateTagsMutationOptions = Apollo.BaseMutationOptions<CreateTagsMutation, CreateTagsMutationVariables>;
39 changes: 27 additions & 12 deletions packages/client/src/pages/studies/NewStudy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { TagTrainingComponent } from '../../components/TagTraining.component';
import { useState, useEffect } from 'react';
import { StudyCreate, TagSchema } from '../../graphql/graphql';
import { PartialStudyCreate } from '../../types/study';
import { useCreateStudyMutation } from '../../graphql/study/study';
import { CreateStudyDocument } from '../../graphql/study/study';
import { useProject } from '../../context/Project.context';
import { useStudy } from '../../context/Study.context';
import { useApolloClient } from '@apollo/client';
import { CreateTagsDocument } from '../../graphql/tag';

export const NewStudy: React.FC = () => {
const [activeStep, setActiveStep] = useState(0);
Expand All @@ -16,8 +18,9 @@ export const NewStudy: React.FC = () => {
const [tagSchema, setTagSchema] = useState<TagSchema | null>(null);
const { project } = useProject();
const { updateStudies } = useStudy();

const [createStudyMutation, createStudyResults] = useCreateStudyMutation();
const [_trainingSet, setTrainingSet] = useState<string[]>([]);
const [taggingSet, setTaggingSet] = useState<string[]>([]);
const apolloClient = useApolloClient();

// Handles mantaining which step the user is on and the step limit
useEffect(() => {
Expand All @@ -35,7 +38,7 @@ export const NewStudy: React.FC = () => {
setStepLimit(3);
}, [partialNewStudy, tagSchema]);

const handleNext = () => {
const handleNext = async () => {
if (activeStep === stepLimit) {
return;
}
Expand All @@ -50,21 +53,33 @@ export const NewStudy: React.FC = () => {
console.error('Reached submission with no project selected');
return;
}

const study: StudyCreate = {
...partialNewStudy,
project: project._id,
tagSchema: tagSchema
};
createStudyMutation({ variables: { study } });
}
setActiveStep((prevActiveStep: number) => prevActiveStep + 1);
};

useEffect(() => {
if (createStudyResults.data) {
// Make the new study
const result = await apolloClient.mutate({
mutation: CreateStudyDocument,
variables: { study: study }
});

if (result.errors || !result.data) {
console.error('Failed to create study');
return;
}

// Create the corresponding tags
await apolloClient.mutate({
mutation: CreateTagsDocument,
variables: { study: result.data.createStudy._id, entries: taggingSet }
});
updateStudies();
}
}, [createStudyResults.data]);
setActiveStep((prevActiveStep: number) => prevActiveStep + 1);
};

const handleBack = () => {
if (activeStep === 0) {
Expand All @@ -86,7 +101,7 @@ export const NewStudy: React.FC = () => {
case 1:
return <TagsDisplay tagSchema={tagSchema} setTagSchema={setTagSchema} />;
case 2:
return <TagTrainingComponent />;
return <TagTrainingComponent setTaggingSet={setTaggingSet} setTrainingSet={setTrainingSet} />;
default:
return null;
}
Expand Down
9 changes: 8 additions & 1 deletion packages/server/src/tag/tag.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import { StudyModule } from '../study/study.module';
import { EntryModule } from '../entry/entry.module';
import { TagPipe } from './pipes/tag.pipe';
import { SharedModule } from '../shared/shared.module';
import { PermissionModule } from '../permission/permission.module';

@Module({
imports: [MongooseModule.forFeature([{ name: Tag.name, schema: TagSchema }]), StudyModule, EntryModule, SharedModule],
imports: [
MongooseModule.forFeature([{ name: Tag.name, schema: TagSchema }]),
StudyModule,
EntryModule,
SharedModule,
PermissionModule
],
providers: [TagService, TagResolver, TagPipe]
})
export class TagModule {}
24 changes: 18 additions & 6 deletions packages/server/src/tag/tag.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import { EntriesPipe, EntryPipe } from '../entry/pipes/entry.pipe';
import { Entry } from '../entry/models/entry.model';
import { TagPipe } from './pipes/tag.pipe';
import JSON from 'graphql-type-json';
import { UseGuards } from '@nestjs/common';
import { Inject, UseGuards, UnauthorizedException } from '@nestjs/common';
import { JwtAuthGuard } from '../jwt/jwt.guard';
import { CASBIN_PROVIDER } from 'src/permission/casbin.provider';
import * as casbin from 'casbin';
import { TokenContext } from '../jwt/token.context';
import { TokenPayload } from '../jwt/token.dto';
import { StudyPermissions } from '../permission/permissions/study';

// TODO: Add permissioning
@UseGuards(JwtAuthGuard)
Expand All @@ -17,21 +22,28 @@ export class TagResolver {
constructor(
private readonly tagService: TagService,
private readonly entryPipe: EntryPipe,
private readonly studyPipe: StudyPipe
private readonly studyPipe: StudyPipe,
@Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer
) {}

@Mutation(() => [Tag])
async createTags(
@Args('study', { type: () => ID }, StudyPipe) study: Study,
@Args('entries', { type: () => [ID] }, EntriesPipe) entries: Entry[]
@Args('entries', { type: () => [ID] }, EntriesPipe) entries: Entry[],
@TokenContext() user: TokenPayload
) {
if (!(await this.enforcer.enforce(user.id, StudyPermissions.CREATE, study._id.toString()))) {
throw new UnauthorizedException('User cannot add tags to this study');
}
return this.tagService.createTags(study, entries);
}

@Mutation(() => Tag, { nullable: true })
async assignTag(@Args('study', { type: () => ID }, StudyPipe) study: Study): Promise<Tag | null> {
// TODO: Add user context
return this.tagService.assignTag(study, '1');
async assignTag(
@Args('study', { type: () => ID }, StudyPipe) study: Study,
@TokenContext() user: TokenPayload
): Promise<Tag | null> {
return this.tagService.assignTag(study, user.id);
}

@Mutation(() => Boolean)
Expand Down