diff --git a/codegen/gql/fragments/Definition.graphql b/codegen/gql/fragments/Definition.graphql index 6d935972cbeb084ff2652235e3be923d337ab197..fa99b9a8f565803c98ad46ab64f65be910db2ba1 100644 --- a/codegen/gql/fragments/Definition.graphql +++ b/codegen/gql/fragments/Definition.graphql @@ -5,4 +5,7 @@ fragment Definition on DefinitionType { channels { ...Channel } + roles { + ...Role + } } diff --git a/codegen/gql/fragments/Role.graphql b/codegen/gql/fragments/Role.graphql new file mode 100644 index 0000000000000000000000000000000000000000..a7e5e8c7bbc3a5306190587cf6b9da8037a31c43 --- /dev/null +++ b/codegen/gql/fragments/Role.graphql @@ -0,0 +1,4 @@ +fragment Role on DefinitionRoleType { + id + name +} diff --git a/frontend/src/analyst/NavigationBar/index.tsx b/frontend/src/analyst/NavigationBar/index.tsx index e86faf67d5675b73c951a78f36d4dc241339bb3f..f6cabfbfbc89c2af58812717777552cd313bd9ab 100644 --- a/frontend/src/analyst/NavigationBar/index.tsx +++ b/frontend/src/analyst/NavigationBar/index.tsx @@ -2,6 +2,7 @@ import { relativeTime, useRelativeTime, } from '@/clientsettings/vars/relativeTime' +import ExitButton from '@/components/ExitButton' import type { Section } from '@/components/Sidebar' import Sidebar from '@/components/Sidebar' import useHideButton from '@/components/Sidebar/useHideButton' @@ -109,9 +110,9 @@ const NavigationBar: FC<PropsWithChildren> = ({ children }) => { text: !hide && 'Exercise selector', }} /> - {/* if the exercise hasn't started yet, the time should be shown relative to the start of time */} {exerciseId && exercise.exerciseStart && <TimeSwitch />} + <ExitButton /> </> ), }, diff --git a/frontend/src/components/ExerciseList/ExerciseCard.tsx b/frontend/src/components/ExerciseList/ExerciseCard.tsx index aa81d1b91503662dd231f403a29adfae166cc896..07c6c68f7fccb563a5ae4b266d8b8e596b3f57b7 100644 --- a/frontend/src/components/ExerciseList/ExerciseCard.tsx +++ b/frontend/src/components/ExerciseList/ExerciseCard.tsx @@ -47,7 +47,7 @@ const ExerciseCard: FC<ExerciseCardProps> = ({ <> {!exercise.running && !exercise.finished && ( <Button icon='play' {...details.startButtonProps}> - Start + {exercise.exerciseStart ? 'Resume' : 'Start'} </Button> )} {exercise.running && ( @@ -60,7 +60,7 @@ const ExerciseCard: FC<ExerciseCardProps> = ({ Remove </Button> )} - {(exercise.running || exercise.finished) && ( + {exercise.exerciseStart && ( <a href={downloadLogUrl(host || '', exercise.id)} target='_blank' @@ -73,6 +73,7 @@ const ExerciseCard: FC<ExerciseCardProps> = ({ ), [ details, + exercise.exerciseStart, exercise.finished, exercise.id, exercise.running, diff --git a/frontend/src/pages/instructor/+exerciseCreator.tsx b/frontend/src/pages/instructor/+exerciseCreator.tsx index be896a7148827a063245ecf9c3112408fd49e47c..0f6d6380ff8a85a232ef646cbe5f872d900f940f 100644 --- a/frontend/src/pages/instructor/+exerciseCreator.tsx +++ b/frontend/src/pages/instructor/+exerciseCreator.tsx @@ -11,14 +11,14 @@ import { Label, NumericInput, } from '@blueprintjs/core' +import type { Definition } from '@inject/graphql/fragments/Definition.generated' import { useCreateExercises } from '@inject/graphql/mutations/CreateExercise.generated' import { useGetDefinitions } from '@inject/graphql/queries/GetDefinitions.generated' import { GetExercisesDocument } from '@inject/graphql/queries/GetExercises.generated' import Box from '@inject/shared/components/Box' import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' -const TEAMS_MIN = 1 const TEAMS_MAX = 20 const ExerciseCreator = () => { @@ -32,7 +32,18 @@ const ExerciseCreator = () => { }) const { notify } = useNotifyContext() const [definitionId, setDefinitionId] = useState<string>() - const [count, setCount] = useState<undefined | number>(undefined) + const [definition, setDefinition] = useState<Definition>() + const [count, setCount] = useState<undefined | number>() + + useEffect(() => { + const definition = definitionData?.definitions?.find( + definition => definition?.id === definitionId + ) + setCount(definition?.roles.length || 1) + if (definition) { + setDefinition(definition) + } + }, [definitionData?.definitions, definitionId]) const handleSubmit = useCallback(() => { addExercise({ @@ -104,17 +115,35 @@ const ExerciseCreator = () => { </Label> <Label style={{ width: '100%' }}> - Team count + Number of teams + {definition?.roles.length ? ( + <> + {' '} + <span + className={Classes.TEXT_MUTED} + >{`(multiple of the number of roles - ${definition.roles.length})`}</span> + </> + ) : ( + '' + )} <NumericInput fill value={count} + disabled={!definition} + stepSize={definition?.roles.length || 1} + minorStepSize={null} + majorStepSize={null} onValueChange={value => { - if (value >= TEAMS_MIN && value <= TEAMS_MAX) { + if ( + value >= (definition?.roles.length || 1) && + value <= Math.max(TEAMS_MAX, definition?.roles.length || 1) && + value % (definition?.roles.length || 1) === 0 + ) { setCount(value) } }} - min={TEAMS_MIN} - max={TEAMS_MAX} + min={definition?.roles.length || 1} + max={Math.max(TEAMS_MAX, definition?.roles.length || 1)} title='Number of teams' placeholder='Number of teams' style={{ margin: 0 }} diff --git a/graphql/fragments/Definition.generated.ts b/graphql/fragments/Definition.generated.ts index 1d4614871138e052134c2047f816019a1a03ee20..102d2fac2660e475dffa1820e14209c0b4dd5307 100644 --- a/graphql/fragments/Definition.generated.ts +++ b/graphql/fragments/Definition.generated.ts @@ -3,6 +3,6 @@ import type * as _Types from '../types'; import type { DocumentNode } from 'graphql'; -export type Definition = { id: string, name: string, version: string, channels: Array<{ id: string, name: string, type: _Types.ChannelType, readReceipt: Array<{ readReceipt: string | null, teamId: string }> }> }; +export type Definition = { id: string, name: string, version: string, channels: Array<{ id: string, name: string, type: _Types.ChannelType, readReceipt: Array<{ readReceipt: string | null, teamId: string }> }>, roles: Array<{ id: string, name: string }> }; -export const Definition = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Definition"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"channels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Channel"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Channel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionChannelType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"readReceipt"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readReceipt"}},{"kind":"Field","name":{"kind":"Name","value":"teamId"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const Definition = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Definition"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"channels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Channel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Role"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Channel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionChannelType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"readReceipt"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readReceipt"}},{"kind":"Field","name":{"kind":"Name","value":"teamId"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Role"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionRoleType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/graphql/fragments/Role.generated.ts b/graphql/fragments/Role.generated.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d937e977113cc89acc106e95d00a1196babaaaa --- /dev/null +++ b/graphql/fragments/Role.generated.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +//@ts-nocheck +import type * as _Types from '../types'; + +import type { DocumentNode } from 'graphql'; +export type Role = { id: string, name: string }; + +export const Role = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Role"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionRoleType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/graphql/queries/GetDefinition.generated.ts b/graphql/queries/GetDefinition.generated.ts index 5c08ebdd728ffdfd4ca1f910bdd173b6d9c0aed5..9e6034bc2bea302fca63d0779b44ef8ecf7ba452 100644 --- a/graphql/queries/GetDefinition.generated.ts +++ b/graphql/queries/GetDefinition.generated.ts @@ -10,10 +10,10 @@ export type GetDefinitionVariables = _Types.Exact<{ }>; -export type GetDefinition = { definition: { id: string, name: string, version: string, channels: Array<{ id: string, name: string, type: _Types.ChannelType, readReceipt: Array<{ readReceipt: string | null, teamId: string }> }> } | null }; +export type GetDefinition = { definition: { id: string, name: string, version: string, channels: Array<{ id: string, name: string, type: _Types.ChannelType, readReceipt: Array<{ readReceipt: string | null, teamId: string }> }>, roles: Array<{ id: string, name: string }> } | null }; -export const GetDefinitionDocument = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDefinition"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"definitionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"definition"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"definitionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"definitionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Definition"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Definition"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"channels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Channel"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Channel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionChannelType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"readReceipt"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readReceipt"}},{"kind":"Field","name":{"kind":"Name","value":"teamId"}}]}}]}}]} as unknown as DocumentNode; +export const GetDefinitionDocument = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDefinition"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"definitionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"definition"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"definitionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"definitionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Definition"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Definition"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"channels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Channel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Role"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Channel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionChannelType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"readReceipt"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readReceipt"}},{"kind":"Field","name":{"kind":"Name","value":"teamId"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Role"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionRoleType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; /** * __useGetDefinition__ diff --git a/graphql/queries/GetDefinitions.generated.ts b/graphql/queries/GetDefinitions.generated.ts index 2a6921a6029a99961cb44a267b478cbf58f88274..6a82dd44fdc0574d0746bfd4376fe15f2293166b 100644 --- a/graphql/queries/GetDefinitions.generated.ts +++ b/graphql/queries/GetDefinitions.generated.ts @@ -8,10 +8,10 @@ const defaultOptions = {} as const; export type GetDefinitionsVariables = _Types.Exact<{ [key: string]: never; }>; -export type GetDefinitions = { definitions: Array<{ id: string, name: string, version: string, channels: Array<{ id: string, name: string, type: _Types.ChannelType, readReceipt: Array<{ readReceipt: string | null, teamId: string }> }> } | null> | null }; +export type GetDefinitions = { definitions: Array<{ id: string, name: string, version: string, channels: Array<{ id: string, name: string, type: _Types.ChannelType, readReceipt: Array<{ readReceipt: string | null, teamId: string }> }>, roles: Array<{ id: string, name: string }> } | null> | null }; -export const GetDefinitionsDocument = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"definitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Definition"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Definition"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"channels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Channel"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Channel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionChannelType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"readReceipt"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readReceipt"}},{"kind":"Field","name":{"kind":"Name","value":"teamId"}}]}}]}}]} as unknown as DocumentNode; +export const GetDefinitionsDocument = /*#__PURE__*/ {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"definitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Definition"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Definition"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"channels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Channel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Role"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Channel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionChannelType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"readReceipt"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"client"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readReceipt"}},{"kind":"Field","name":{"kind":"Name","value":"teamId"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Role"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DefinitionRoleType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; /** * __useGetDefinitions__