Skip to content

Commit

Permalink
Added Timeline (#188)
Browse files Browse the repository at this point in the history
* start timeline

* added problems letters

* timeline

* clean up

* added websocket support

* fix api

* fix timeline design

* add constants

* ignore run after accepted verdict

* delete RUNS_URL

* refactor backend

* ioi support

* backend rework

* design improvements

* refactor

* refactor

* fix const

* fix admin linter

* fix api

* some refactor

* move to typescript
  • Loading branch information
Mond1c authored Jul 30, 2024
1 parent 32a33d2 commit 2ab162a
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 3 deletions.
24 changes: 24 additions & 0 deletions schemas/advanced.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,27 @@
],
"title": "TaskStatus"
},
"TimeLine": {
"type": "object",
"properties": {
"type": {
"const": "TimeLine",
"default": "TimeLine"
},
"teamId": {
"type": "string"
},
"isMedia": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"type",
"teamId"
],
"title": "TimeLine"
},
"Video": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -204,6 +225,9 @@
{
"$ref": "#/$defs/TaskStatus"
},
{
"$ref": "#/$defs/TimeLine"
},
{
"$ref": "#/$defs/Video"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ data class ExternalTeamViewSettings(
val mediaTypes: List<TeamMediaType> = emptyList(),
val showTaskStatus: Boolean = true,
val showAchievement: Boolean = false,
val showTimeLine: Boolean = false,
val position: TeamViewPosition = TeamViewPosition.SINGLE_TOP_RIGHT,
) : ObjectSettings

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class TeamViewController(manager: Manager<TeamViewWidget>, val position: TeamVie
if (settings.showTaskStatus) {
settings.teamId?.let { teamId -> content.add(MediaType.TaskStatus(teamId)) }
}
if (settings.showTimeLine) {
settings.teamId?.let { teamId -> content.add(MediaType.TimeLine(teamId)) }
}
if (settings.showAchievement) {
teamInfo?.medias?.get(TeamMediaType.ACHIEVEMENT)?.let { content.add(it.noMedia()) }
}
Expand Down
32 changes: 30 additions & 2 deletions src/backend/src/main/kotlin/org/icpclive/overlay/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.flow.*
import org.icpclive.Config
import org.icpclive.admin.getExternalRun
import org.icpclive.cds.RunUpdate
import org.icpclive.cds.api.*
import org.icpclive.data.DataBus
import org.icpclive.data.currentContestInfoFlow
import org.icpclive.util.sendJsonFlow
import kotlin.time.Duration

inline fun <reified T: Any> Route.flowEndpoint(name: String, crossinline dataProvider: suspend () -> Flow<T>) {
inline fun <reified T : Any> Route.flowEndpoint(name: String, crossinline dataProvider: suspend () -> Flow<T>) {
webSocket(name) { sendJsonFlow(dataProvider()) }
get(name) { call.respond(dataProvider().first()) }
}
Expand All @@ -28,13 +32,37 @@ fun Route.configureOverlayRouting() {
flowEndpoint("/mainScreen") { DataBus.mainScreenFlow.await() }
flowEndpoint("/contestInfo") { DataBus.currentContestInfoFlow() }
flowEndpoint("/runs") { DataBus.contestStateFlow.await().map { it.runsAfterEvent.values.sortedBy { it.time } } }
webSocket("/teamRuns/{id}") {
val teamIdStr = call.parameters["id"]
if (teamIdStr.isNullOrBlank()) {
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid team id"))
return@webSocket
}
val teamId = teamIdStr.toTeamId()
val acceptedProblems = mutableSetOf<ProblemId>()
val allRuns = mutableMapOf<RunId, RunInfo>()
DataBus.contestStateFlow.await().first().runsAfterEvent.values
.filter { teamId == it.teamId && it.time != Duration.ZERO }
.forEach { allRuns[it.id] = it }
sendJsonFlow(DataBus.contestStateFlow.await()
.mapNotNull { (it.lastEvent as? RunUpdate)?.newInfo }
.runningFold(allRuns.toPersistentMap()) { acc, it ->
if (it.teamId == teamId) acc.put(it.id, it) else if (it.id in acc) acc.remove(it.id) else acc
}
.distinctUntilChanged { a, b -> a === b }
.map { runs -> runs.values.sortedBy { it.time } }
.map { runs ->
acceptedProblems.clear()
runs.mapNotNull { info -> TimeLineRunInfo.fromRunInfo(info, acceptedProblems) }
})
}
flowEndpoint("/queue") { DataBus.queueFlow.await() }
flowEndpoint("/statistics") { DataBus.statisticFlow.await() }
flowEndpoint("/ticker") { DataBus.tickerFlow.await() }
route("/scoreboard") {
setUpScoreboard { getScoreboardDiffs(it) }
}
route("/svgAchievement"){
route("/svgAchievement") {
configureSvgAchievementRouting(Config.mediaDirectory)
}
get("/visualConfig.json") { call.respond(DataBus.visualConfigFlow.await().value) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ private fun MediaType.toClicsMedia() = when (this) {
is MediaType.Object -> null
is MediaType.Image -> File("image", Url(url))
is MediaType.TaskStatus -> null
is MediaType.TimeLine -> null
is MediaType.Video -> File("video", Url(url))
is MediaType.M2tsVideo -> File("video/m2ts", Url(url))
is MediaType.HLSVideo -> File("application/vnd.apple.mpegurl", Url(url))
Expand Down
132 changes: 132 additions & 0 deletions src/cds/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,34 @@ public final class org/icpclive/cds/api/MediaType$TaskStatus$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/MediaType$TimeLine : org/icpclive/cds/api/MediaType {
public static final field Companion Lorg/icpclive/cds/api/MediaType$TimeLine$Companion;
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-ed2mA_4 ()Ljava/lang/String;
public final fun copy-nUE0bHc (Ljava/lang/String;)Lorg/icpclive/cds/api/MediaType$TimeLine;
public static synthetic fun copy-nUE0bHc$default (Lorg/icpclive/cds/api/MediaType$TimeLine;Ljava/lang/String;ILjava/lang/Object;)Lorg/icpclive/cds/api/MediaType$TimeLine;
public fun equals (Ljava/lang/Object;)Z
public final fun getTeamId-ed2mA_4 ()Ljava/lang/String;
public fun hashCode ()I
public fun isMedia ()Z
public fun toString ()Ljava/lang/String;
}

public synthetic class org/icpclive/cds/api/MediaType$TimeLine$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lorg/icpclive/cds/api/MediaType$TimeLine$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/MediaType$TimeLine;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/MediaType$TimeLine;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/MediaType$TimeLine$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/MediaType$Video : org/icpclive/cds/api/MediaType {
public static final field Companion Lorg/icpclive/cds/api/MediaType$Video$Companion;
public fun <init> (Ljava/lang/String;Z)V
Expand Down Expand Up @@ -1644,6 +1672,110 @@ public final class org/icpclive/cds/api/TeamMediaType$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public abstract class org/icpclive/cds/api/TimeLineRunInfo {
public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$Companion;
public synthetic fun <init> (ILkotlinx/serialization/internal/SerializationConstructorMarker;)V
public static final synthetic fun write$Self (Lorg/icpclive/cds/api/TimeLineRunInfo;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}

public final class org/icpclive/cds/api/TimeLineRunInfo$Companion {
public final fun fromRunInfo (Lorg/icpclive/cds/api/RunInfo;Ljava/util/Set;)Lorg/icpclive/cds/api/TimeLineRunInfo;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/TimeLineRunInfo$ICPC : org/icpclive/cds/api/TimeLineRunInfo {
public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC$Companion;
public synthetic fun <init> (JLjava/lang/String;ZLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-UwyO8pc ()J
public final fun component2-Xzdl60o ()Ljava/lang/String;
public final fun component3 ()Z
public final fun component4 ()Ljava/lang/String;
public final fun copy-4joZhgo (JLjava/lang/String;ZLjava/lang/String;)Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;
public static synthetic fun copy-4joZhgo$default (Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;JLjava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;
public fun equals (Ljava/lang/Object;)Z
public final fun getProblemId-Xzdl60o ()Ljava/lang/String;
public final fun getShortName ()Ljava/lang/String;
public final fun getTime-UwyO8pc ()J
public fun hashCode ()I
public final fun isAccepted ()Z
public fun toString ()Ljava/lang/String;
}

public synthetic class org/icpclive/cds/api/TimeLineRunInfo$ICPC$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/TimeLineRunInfo$ICPC;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/TimeLineRunInfo$ICPC$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/TimeLineRunInfo$IOI : org/icpclive/cds/api/TimeLineRunInfo {
public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$IOI$Companion;
public synthetic fun <init> (JLjava/lang/String;DLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-UwyO8pc ()J
public final fun component2-Xzdl60o ()Ljava/lang/String;
public final fun component3 ()D
public final fun copy-nveW3u0 (JLjava/lang/String;D)Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;
public static synthetic fun copy-nveW3u0$default (Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;JLjava/lang/String;DILjava/lang/Object;)Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;
public fun equals (Ljava/lang/Object;)Z
public final fun getProblemId-Xzdl60o ()Ljava/lang/String;
public final fun getScore ()D
public final fun getTime-UwyO8pc ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public synthetic class org/icpclive/cds/api/TimeLineRunInfo$IOI$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lorg/icpclive/cds/api/TimeLineRunInfo$IOI$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/TimeLineRunInfo$IOI;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/TimeLineRunInfo$IOI$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/TimeLineRunInfo$InProgress : org/icpclive/cds/api/TimeLineRunInfo {
public static final field Companion Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress$Companion;
public synthetic fun <init> (JLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-UwyO8pc ()J
public final fun component2-Xzdl60o ()Ljava/lang/String;
public final fun copy-15fPPug (JLjava/lang/String;)Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;
public static synthetic fun copy-15fPPug$default (Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;JLjava/lang/String;ILjava/lang/Object;)Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;
public fun equals (Ljava/lang/Object;)Z
public final fun getProblemId-Xzdl60o ()Ljava/lang/String;
public final fun getTime-UwyO8pc ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public synthetic class org/icpclive/cds/api/TimeLineRunInfo$InProgress$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/TimeLineRunInfo$InProgress;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class org/icpclive/cds/api/TimeLineRunInfo$InProgress$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public abstract class org/icpclive/cds/api/Verdict {
public static final field Companion Lorg/icpclive/cds/api/Verdict$Companion;
public synthetic fun <init> (Ljava/lang/String;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public sealed class MediaType {
override val isMedia: Boolean = false
}

@Serializable
@SerialName("TimeLine")
public data class TimeLine(val teamId: TeamId) : MediaType() {
override val isMedia: Boolean = false
}

public fun noMedia(): MediaType = when (this) {
is Image -> copy(isMedia = false)
is Video -> copy(isMedia = false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.icpclive.cds.api

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.icpclive.cds.util.serializers.DurationInMillisecondsSerializer
import kotlin.time.Duration

@Serializable
public sealed class TimeLineRunInfo {
@Serializable
@SerialName("ICPC")
public data class ICPC(
@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration,
val problemId: ProblemId,
val isAccepted: Boolean,
val shortName: String) : TimeLineRunInfo()

@Serializable
@SerialName("IOI")
public data class IOI(@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, val problemId: ProblemId, val score: Double) : TimeLineRunInfo()

@Serializable
@SerialName("IN_PROGRESS")
public data class InProgress(@Serializable(with = DurationInMillisecondsSerializer::class) val time: Duration, val problemId: ProblemId) : TimeLineRunInfo()

public companion object {
public fun fromRunInfo(info: RunInfo, acceptedProblems: MutableSet<ProblemId>): TimeLineRunInfo? {
return when (info.result) {
is RunResult.ICPC -> {
val icpcResult = info.result
if (!acceptedProblems.contains(info.problemId)) {
if (icpcResult.verdict.isAccepted) {
acceptedProblems.add(info.problemId)
}
ICPC(info.time, info.problemId, icpcResult.verdict.isAccepted, icpcResult.verdict.shortName)
} else {
null
}
}

is RunResult.IOI -> {
val ioiResult = info.result
if (ioiResult.difference > 0) {
IOI(info.time, info.problemId, ioiResult.scoreAfter)
} else {
null
}
}

else -> {
InProgress(info.time, info.problemId)
}
}
}
}
}
18 changes: 17 additions & 1 deletion src/frontend/admin/src/components/TeamView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ const TeamViewManager = ({ singleService, pvpService, splitService }) => {
const [mediaType2, setMediaType2] = useState(undefined);
const [statusShown, setStatusShown] = useState(true);
const [achievementShown, setAchievementShown] = useState(false);
const [timeLineShown, setTimeLineShown] = useState(false);

const [allowedMediaTypes, disableMediaTypes] = useMemo(() => [
DEFAULT_MEDIA_TYPES.filter(m => m && (selectedTeam?.id ? selectedTeam.medias[m] : teamsAvailableMedias.includes(m))),
Expand Down Expand Up @@ -371,6 +372,7 @@ const TeamViewManager = ({ singleService, pvpService, splitService }) => {
teamId: selectedTeamId,
showTaskStatus: statusShown,
showAchievement: achievementShown && variant === "single",
showTimeLine: timeLineShown,
};
if (isMultipleMode) {
currentService.editPreset(selectedInstance, settings);
Expand All @@ -380,7 +382,7 @@ const TeamViewManager = ({ singleService, pvpService, splitService }) => {
setSelectedInstance(undefined);
setSelectedTeamId(undefined);
}, [selectedInstance, currentService, isMultipleMode, mediaType1,
mediaType2, selectedTeamId, statusShown, achievementShown, variant]);
mediaType2, selectedTeamId, statusShown, achievementShown, variant, timeLineShown]);

const onInstanceSelect = useCallback((instance) => () => {
if (instance === selectedInstance) {
Expand Down Expand Up @@ -492,6 +494,20 @@ const TeamViewManager = ({ singleService, pvpService, splitService }) => {
</Grid>
</>
)}
{variant === "single" && (
<>
<Grid item xs={10} sm={4}>
<FormLabel component="legend">TimeLine</FormLabel>
</Grid>
<Grid item xs={2} sm={8}>
<ShowPresetButton
checked={timeLineShown}
onClick={(v) => setTimeLineShown(v)}
sx={{ justifyContent: "flex-start" }}
/>
</Grid>
</>
)}
</Grid>
<Button
color="primary"
Expand Down
Loading

0 comments on commit 2ab162a

Please sign in to comment.