bw-hspc-contest-env/extension/bwcontest/webviews/components/SidebarProblemStatus.svelte
David Poeschl 4a3ff56e7a
VSCode Extension: Sidebar UI showing team's submissions, automatically updating and showing alerts as submissions are judged (#14)
* Add an Output Panel channel named "BWContest Log"

* Allow client logout when no contest

And make login/logout error messages clearer

* Show contest name & team name in Code extension side panel

* submission icons for sidebar panel

* Start VSCode extension "onStartupFinished"

instead of waiting for Sidebar to be opened

* VSCode: Sidebar UI for up-to-date problem/submissions status

- VSCode: poll API every 30 seconds to get contest metadata and all submission metadata for the logged in team
- The Sidebar now shows all problems in the contest, along with their submissions and overall status, which automatically updates as submissions are submitted & judged
- Web: "contestState" API to get all info for an activeTeam via their token
- Update submit API to return the submission id, allowing the VSCode UI to immediately render it as Pending without waiting for a polling cycle
-

* Add "Compilation Failed" message to submissions that fail to build

* Contest Import - Option to create repos & immediately activate the imported contest

Useful for testing with old contests (including the submissions)

* Test/Submit panel, use fixed-width font in input/output areas

* Fix build error for 'pluralize'

* Clear all state & halt polling loops on logout, restart them on login

* Improve the debug fastPolling option

- Toggleable via package.json config
- Setting the option changes the initial state as well as ability to toggle states

* Web project 'npm run format'
2024-03-05 17:50:16 -05:00

221 lines
5.9 KiB
Svelte

<script lang="ts" context="module">
export const watSubmissionsImageUrl =
new URL('../../media/SubmissionIcons/TeamPanel/none.png', import.meta.url).href;
export const correctSubmissionImageUrl =
new URL('../../media/SubmissionIcons/TeamPanel/correct.png', import.meta.url).href;
export const incorrectSubmissionImageUrl =
new URL('../../media/SubmissionIcons/TeamPanel/incorrect.png', import.meta.url).href;
export const pendingSubmissionImageUrl =
new URL('../../media/SubmissionIcons/TeamPanel/unknown.png', import.meta.url).href;
export const noSubmissionsImageUrl =
new URL('../../media/SubmissionIcons/TeamPanel/none.png', import.meta.url).href;
</script>
<script lang="ts">
import type { SidebarProblemWithSubmissions } from '../../src/SidebarProvider';
import type { ContestStateForExtension, SubmissionForExtension, SubmissionStateForExtension } from '../../src/contestMonitor/contestMonitorSharedTypes';
export let contestState: ContestStateForExtension;
export let problem: SidebarProblemWithSubmissions;
const sortedSubmissions = problem.submissions
? problem.submissions.sort((s1, s2) => Date.parse(s1.createdAt.toString()) - Date.parse(s2.createdAt.toString()))
: [];
const highlightClasses = `${(problem.modified ? "highlight" : "")} ${problem.overallState?.toLowerCase()}`;
function getStatusImageUrl(overallState: SubmissionStateForExtension | null): string {
switch (overallState) {
case 'Correct':
return correctSubmissionImageUrl;
case 'Incorrect':
return incorrectSubmissionImageUrl;
case 'Processing':
return pendingSubmissionImageUrl;
default:
return watSubmissionsImageUrl;
}
}
function getContestOffsetDisplay(submission: SubmissionForExtension): string {
if (!contestState.startTime) {
return '?';
}
try {
const contestStartAbsoluteMillis = Date.parse(contestState.startTime.toString());
const submissionTimeAbsoluteMillis = Date.parse(submission.createdAt.toString());
const submissionRelativeMillis = submissionTimeAbsoluteMillis - contestStartAbsoluteMillis;
const minutesFromContestStart = Math.ceil(submissionRelativeMillis / 1000 / 60);
return `${minutesFromContestStart} min`;
} catch (error) {
return '???';
}
}
function pluralize(num: number, singular: string, plural: string) {
return num === 1 ? singular : plural;
}
</script>
<div class={"problemStatusDiv " + highlightClasses}>
<div class={"problemHeaderDiv"}>
<img
class="overallStatusImage"
src={getStatusImageUrl(problem.overallState)}
alt={problem.overallState}
/>
<div class="problemHeader">
{#if problem.submissions.length == 0}
<span class="problemHeaderName">{problem.problem.friendlyName}</span>
{:else}
<span class="problemHeaderName">{problem.problem.friendlyName}</span>
<span class="problemHeaderSubmitCount">
{problem.submissions.length}
{pluralize(problem.submissions.length, 'attempt', 'attempts')}</span>
{#if problem.submissions.filter((s) => s.state === 'Processing').length > 0}
<span>({problem.submissions.filter((s) => s.state === 'Processing').length} pending...)</span>
{/if}
{#if problem.overallState === "Correct"}
<span class="individualSubmissionAttemptTime"> @ {getContestOffsetDisplay(problem.submissions.filter(s => s.state === "Correct")[0])}</span>
{/if}
{/if}
</div>
</div>
{#if problem.overallState !== "Correct"}
{#each sortedSubmissions as submission, i}
<div class="individualSubmissionDiv">
<span class="individualSubmissionAttemptNumber">Submit #{i + 1}: </span>
<img
class="individualSubmissionStatusImage"
src={getStatusImageUrl(submission.state)}
alt={submission.state}/>
<span class="individualSubmissionResult {submission.state.toLowerCase()}">
{submission.state}
</span>
<span class="individualSubmissionAttemptTime"> @ {getContestOffsetDisplay(submission)}</span>
</div>
{#if submission.message}
<div class="individualSubmissionMessageWrapper">
Judge: <span class="individualSubmissionMessage">{submission.message}</span>
</div>
{/if}
{/each}
{/if}
</div>
<style>
.problemStatusDiv {
padding-top: 8px;
padding-left: 16px;
padding-bottom: 6px;
}
.problemHeaderDiv {
display: flex;
align-items: center;
}
.problemHeaderName {
font-size: 15px;
font-weight: bold;
}
.problemHeaderSubmitCount {
margin-left: 4px;
font-style: italic;
}
.problemHeader {
margin-left: 6px;
}
.overallStatusImage {
height: 18px;
width: 18px;
}
.individualSubmissionStatusImage {
height: 12px;
width: 12px;
margin-left: 6px;
}
.individualSubmissionDiv {
margin-left: 38px;
display: flex;
align-items: center;
padding-top: 4px;
}
.individualSubmissionAttemptNumber {
font-weight: bold;
margin-left: 6px;
}
.individualSubmissionAttemptTime {
margin-left: 4px;
color: var(--vscode-descriptionForeground);
}
.individualSubmissionResult {
padding-left: 4px;
}
.individualSubmissionResult.processing {
color: var(--vscode-charts-yellow);
}
.individualSubmissionResult.correct {
color: var(--vscode-charts-green);
}
.individualSubmissionResult.incorrect {
color: var(--vscode-charts-red);
}
.individualSubmissionMessageWrapper {
padding-left: 64px;
padding-top: 2px;
color: var(--vscode-charts-blue);
}
.individualSubmissionMessage {
font-style: italic;
}
.highlight.correct {
animation: highlightAnimationCorrect 2s ease;
}
.highlight.incorrect {
animation: highlightAnimationIncorrect 2s ease;
}
.highlight.processing {
animation: highlightAnimationProcessing 2s ease;
}
@keyframes highlightAnimationCorrect {
from {
background-color: var(--vscode-charts-green);
}
}
@keyframes highlightAnimationIncorrect {
from {
background-color: var(--vscode-charts-red);
}
}
@keyframes highlightAnimationProcessing {
from {
background-color: var(--vscode-charts-yellow);
}
}
</style>