Skip to content
Snippets Groups Projects
Commit 8ba8e822 authored by Marek Veselý's avatar Marek Veselý
Browse files

Merge remote-tracking branch 'origin/main' into 300-forwarding-emails

parents b7b1ace1 1481a353
No related branches found
No related tags found
No related merge requests found
https://github.com/heroku/heroku-buildpack-nodejs.git#v236
https://github.com/dokku/heroku-buildpack-nginx.git#v25
\ No newline at end of file
......@@ -10,6 +10,14 @@ variables:
CI_REGISTRY: gitlab.fi.muni.cz:5050/inject/container-registry/$CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_REGISTRY:$VER
IMAGE_LATEST: $CI_REGISTRY:latest
# using "docker" as the host is only possible if you alias the service below
DOCKER_HOST: tcp://docker:2375
# could be wrong here but although Docker defaults to overlay2,
# Docker-in-Docker (DIND) does not according to the following GitLab doc:
# https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-the-overlayfs-driver
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
default:
tags:
......@@ -45,7 +53,9 @@ tsc-test-job:
create-image:
image: docker:20.10.16
services:
- docker:20.10.16-dind
- name: docker:20.10.16-dind
alias: docker
command: ["--tls=false"]
stage: buildimage
only:
refs:
......
2024-05-16 - add emails forwarding
2024-05-15 - fix draft persistance
2024-05-15 - add a title to the toolbar and improve the section titles
2024-05-14 - improve new email highlighting
2024-05-14 - fix draft persistance
2024-05-07 - add the option to add not team-visible addresses to the recipients list
2024-05-07 - add exercise name, rework exercise panel add dialogs
2024-05-07 - add learning objectives page to the instructor view
......
import type { SpinnerProps } from '@blueprintjs/core'
import { Spinner } from '@blueprintjs/core'
import { useApolloNetworkStatus } from '@inject/graphql/client'
import useApolloClient from '@inject/graphql/client/useApolloClient'
import { GetExerciseDocument } from '@inject/graphql/queries/GetExercise.generated'
import { GetExerciseLoopStatusDocument } from '@inject/graphql/queries/GetExerciseLoopStatus.generated'
import { useGetExerciseTimeLeft } from '@inject/graphql/queries/GetExerciseTimeLeft.generated'
import { useEffect, useState } from 'react'
import { HEALTH_CHECK_INTERVAL } from '../utilities'
const HealthCheck = () => {
const [wasError, setWasError] = useState(false)
const { loading, error } = useGetExerciseTimeLeft({
pollInterval: HEALTH_CHECK_INTERVAL,
})
const { numPendingQueries, numPendingMutations } = useApolloNetworkStatus()
const client = useApolloClient()
/**
* During a backend server outage, the exercise loop of a running exercise
* will be stopped, but the exercise will still be marked as running
*
* This inconsistency is resolved in
* /frontend/graphql/utils/useExerciseSubscriptionStatus.ts by restarting
* the exercise and refetching the exercise loop status
*
* To be able to detect this inconsistency, the relevant queries need to be
* refetched after the backend server outage
*/
useEffect(() => {
if (error) {
setWasError(true)
} else {
if (wasError) {
setWasError(false)
client.refetchQueries({
include: [GetExerciseLoopStatusDocument, GetExerciseDocument],
})
}
}
}, [client, error, wasError])
let spinnerProps: SpinnerProps
if (loading) {
......
......@@ -25,6 +25,12 @@ const sectionName = css`
font-size: 1rem;
`
const sidebarTitle = css`
margin: 0 0 0.5rem 0.5rem;
font-size: 1.2rem;
text-align: center;
`
export interface Section {
name?: string
node: ReactNode
......@@ -36,6 +42,7 @@ interface SidebarProps {
width?: number
sections: Section[]
showLogo?: boolean
title?: string
}
const Sidebar: FC<SidebarProps> = ({
......@@ -44,11 +51,19 @@ const Sidebar: FC<SidebarProps> = ({
showLogo,
hideNames,
width,
title,
}) => (
<div className={container(position, width)}>
{position === 'right' && <Divider />}
<div className={sidebar}>
{title && (
<div>
<h2 className={sidebarTitle}>{title}</h2>
<Divider />
</div>
)}
{showLogo && (
<>
<InjectLogo
......
......@@ -23,24 +23,28 @@ const Toolbar: FC<ToolbarProps> = ({ teamId, exerciseId }) => {
})
const process = (data?.teamTools ?? []).filter(notEmpty)
const groups = useMemo(() => {
const map = new Map<string, Tool[]>()
const groups: Map<string | undefined, Tool[]> = useMemo(() => {
const uncategorized: Tool[] = []
process.forEach(x => {
let prefix = x.name.split('_')[0]!
const groupsMap = new Map<string | undefined, Tool[]>()
process.forEach(tool => {
const prefix = tool.name.split('_').at(0)
if (prefix === x.name) {
prefix = 'Uncategorized'
if (!prefix || prefix === tool.name) {
uncategorized.push(tool)
return
}
if (map.has(prefix)) {
map.get(prefix)?.push(x)
if (groupsMap.has(prefix)) {
groupsMap.get(prefix)?.push(tool)
} else {
map.set(prefix, [x])
groupsMap.set(prefix, [tool])
}
})
return map
groupsMap.set(groupsMap.size === 0 ? undefined : 'Other', uncategorized)
return groupsMap
}, [process])
const sections: Section[] = [...groups.entries()].map(([key, value]) => ({
......@@ -58,7 +62,14 @@ const Toolbar: FC<ToolbarProps> = ({ teamId, exerciseId }) => {
),
}))
return <Sidebar position='right' sections={sections} width={300} />
return (
<Sidebar
position='right'
sections={sections}
width={300}
title='Available tools'
/>
)
}
export default memo(Toolbar)
import { useEffect } from 'react'
import { useStartExercise } from '../mutations/StartExercise.generated'
import { useStopExercise } from '../mutations/StopExercise.generated'
import { useGetExercise } from '../queries/GetExercise.generated'
import { useGetExerciseLoopStatus } from '../queries/GetExerciseLoopStatus.generated'
import type { ExerciseLoopRunningSubscriptionResult } from '../subscriptions/ExerciseLoopStatus.generated'
import { ExerciseLoopRunningDocument } from '../subscriptions/ExerciseLoopStatus.generated'
const useExerciseSubscriptionStatus = (exerciseId?: string) => {
const { data: dataExerciseLoop, subscribeToMore } = useGetExerciseLoopStatus({
const {
data: dataExerciseLoop,
subscribeToMore,
refetch: refetchExerciseLoop,
} = useGetExerciseLoopStatus({
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-first',
variables: {
......@@ -12,6 +19,13 @@ const useExerciseSubscriptionStatus = (exerciseId?: string) => {
},
skip: !exerciseId,
})
const { data: dataExercise } = useGetExercise({
variables: { exerciseId: exerciseId || '' },
skip: !exerciseId,
})
const [stopExercise] = useStopExercise()
const [startExercise] = useStartExercise()
useEffect(() => {
if (exerciseId) {
return subscribeToMore({
......@@ -29,6 +43,52 @@ const useExerciseSubscriptionStatus = (exerciseId?: string) => {
})
}
}, [subscribeToMore, exerciseId])
/**
* During a backend server outage, the exercise loop of a running exercise
* will be stopped, but the exercise will still be marked as running
*
* This useEffect resolves this inconsistency by restarting the exercise and
* refetching the exercise loop status; only one client can do this at a time
*
* Note that the issue will persist if the exercise is not subscribed to at
* the moment of the backend server failure
*/
useEffect(() => {
const asyncEffect = async () => {
while (
dataExerciseLoop?.exerciseLoopRunning === false &&
dataExercise?.exerciseId?.running === true &&
exerciseId
) {
const lock = localStorage.getItem('backendOutageFixLock')
if (lock) {
await new Promise(resolve => setTimeout(resolve, 1000))
await refetchExerciseLoop()
return
}
localStorage.setItem('backendOutageFixLock', 'true')
stopExercise({
variables: { id: exerciseId },
onCompleted: () => {
startExercise({
variables: { id: exerciseId },
onCompleted: () => {
localStorage.removeItem('backendOutageFixLock')
refetchExerciseLoop()
},
})
},
})
break
}
}
asyncEffect()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataExercise?.exerciseId?.running, dataExerciseLoop?.exerciseLoopRunning])
return dataExerciseLoop?.exerciseLoopRunning
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment