[extension] Format

This commit is contained in:
orosmatthew 2024-03-05 19:31:11 -05:00
parent 4a3ff56e7a
commit fde6b0019b
17 changed files with 458 additions and 288 deletions

View File

@ -9,7 +9,9 @@
"categories": [ "categories": [
"Other" "Other"
], ],
"activationEvents": ["onStartupFinished"], "activationEvents": [
"onStartupFinished"
],
"main": "./out/main.js", "main": "./out/main.js",
"contributes": { "contributes": {
"configuration": { "configuration": {

View File

@ -4,15 +4,25 @@ import { cloneAndOpenRepo } from './extension';
import { BWPanel } from './problemPanel'; import { BWPanel } from './problemPanel';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
import outputPanelLog from './outputPanelLog'; import outputPanelLog from './outputPanelLog';
import { ContestStateForExtension, ProblemNameForExtension, SubmissionForExtension, SubmissionStateForExtension } from './contestMonitor/contestMonitorSharedTypes'; import {
ContestStateForExtension,
ProblemNameForExtension,
SubmissionForExtension,
SubmissionStateForExtension
} from './contestMonitor/contestMonitorSharedTypes';
import { TeamData } from './sharedTypes'; import { TeamData } from './sharedTypes';
import { ContestTeamState, getCachedContestTeamState, clearCachedContestTeamState, submissionsListChanged } from './contestMonitor/contestStateSyncManager'; import {
ContestTeamState,
getCachedContestTeamState,
clearCachedContestTeamState,
submissionsListChanged
} from './contestMonitor/contestStateSyncManager';
import { startTeamStatusPolling, stopTeamStatusPolling } from './contestMonitor/pollingService'; import { startTeamStatusPolling, stopTeamStatusPolling } from './contestMonitor/pollingService';
export type WebviewMessageType = export type WebviewMessageType =
{ msg: 'onLogin'; data: TeamData } | | { msg: 'onLogin'; data: TeamData }
{ msg: 'onLogout' } | | { msg: 'onLogout' }
{ msg: 'teamStatusUpdated'; data: SidebarTeamStatus | null }; | { msg: 'teamStatusUpdated'; data: SidebarTeamStatus | null };
export type MessageType = export type MessageType =
| { msg: 'onTestAndSubmit' } | { msg: 'onTestAndSubmit' }
@ -27,14 +37,14 @@ export type SidebarTeamStatus = {
processingProblems: SidebarProblemWithSubmissions[]; processingProblems: SidebarProblemWithSubmissions[];
incorrectProblems: SidebarProblemWithSubmissions[]; incorrectProblems: SidebarProblemWithSubmissions[];
notStartedProblems: SidebarProblemWithSubmissions[]; notStartedProblems: SidebarProblemWithSubmissions[];
} };
export type SidebarProblemWithSubmissions = { export type SidebarProblemWithSubmissions = {
problem: ProblemNameForExtension; problem: ProblemNameForExtension;
overallState: SubmissionStateForExtension | null; overallState: SubmissionStateForExtension | null;
submissions: SubmissionForExtension[]; submissions: SubmissionForExtension[];
modified: boolean; modified: boolean;
} };
export class SidebarProvider implements vscode.WebviewViewProvider { export class SidebarProvider implements vscode.WebviewViewProvider {
private webview: vscode.Webview | null = null; private webview: vscode.Webview | null = null;
@ -44,14 +54,17 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
private readonly context: vscode.ExtensionContext, private readonly context: vscode.ExtensionContext,
private readonly webUrl: string private readonly webUrl: string
) { ) {
outputPanelLog.info("Constructing SidebarProvider"); outputPanelLog.info('Constructing SidebarProvider');
const currentSubmissionsList = getCachedContestTeamState(); const currentSubmissionsList = getCachedContestTeamState();
outputPanelLog.info("When SidebarProvider constructed, cached submission list is: " + JSON.stringify(currentSubmissionsList)); outputPanelLog.info(
'When SidebarProvider constructed, cached submission list is: ' +
JSON.stringify(currentSubmissionsList)
);
this.updateTeamStatus(currentSubmissionsList); this.updateTeamStatus(currentSubmissionsList);
submissionsListChanged.add(submissionsChangedEventArgs => { submissionsListChanged.add((submissionsChangedEventArgs) => {
outputPanelLog.trace("Sidebar submission list updating from submissionsListChanged event"); outputPanelLog.trace('Sidebar submission list updating from submissionsListChanged event');
if (!submissionsChangedEventArgs) { if (!submissionsChangedEventArgs) {
return; return;
@ -60,7 +73,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
this.updateTeamStatus( this.updateTeamStatus(
submissionsChangedEventArgs.contestTeamState, submissionsChangedEventArgs.contestTeamState,
submissionsChangedEventArgs.changedProblemIds submissionsChangedEventArgs.changedProblemIds
)}); );
});
} }
private async handleLogin( private async handleLogin(
@ -84,7 +98,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
} }
if (resData.success !== true) { if (resData.success !== true) {
outputPanelLog.error('Invalid Login attempt with message: ' + (resData.message ?? "<none>")); outputPanelLog.error('Invalid Login attempt with message: ' + (resData.message ?? '<none>'));
vscode.window.showErrorMessage('BWContest: Invalid Login'); vscode.window.showErrorMessage('BWContest: Invalid Login');
return; return;
} }
@ -95,7 +109,9 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}); });
const data2 = await teamRes.json(); const data2 = await teamRes.json();
if (!data2.success) { if (!data2.success) {
outputPanelLog.error('Login attempt retrieved token but not team details. Staying logged out.'); outputPanelLog.error(
'Login attempt retrieved token but not team details. Staying logged out.'
);
vscode.window.showErrorMessage('BWContest: Invalid Login'); vscode.window.showErrorMessage('BWContest: Invalid Login');
return; return;
} }
@ -109,21 +125,27 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
webviewPostMessage({ msg: 'onLogin', data: data2.data }); webviewPostMessage({ msg: 'onLogin', data: data2.data });
const currentSubmissionsList = getCachedContestTeamState(); const currentSubmissionsList = getCachedContestTeamState();
outputPanelLog.info("After login, cached submission list is: " + JSON.stringify(currentSubmissionsList)); outputPanelLog.info(
'After login, cached submission list is: ' + JSON.stringify(currentSubmissionsList)
);
this.updateTeamStatus(currentSubmissionsList); this.updateTeamStatus(currentSubmissionsList);
} }
private async handleLogout(webviewPostMessage: (m: WebviewMessageType) => void) { private async handleLogout(webviewPostMessage: (m: WebviewMessageType) => void) {
const sessionToken = this.context.globalState.get<string>('token'); const sessionToken = this.context.globalState.get<string>('token');
if (sessionToken === undefined) { if (sessionToken === undefined) {
outputPanelLog.error("Team requested logout, but no token was stored locally. Switching to logged out state."); outputPanelLog.error(
'Team requested logout, but no token was stored locally. Switching to logged out state.'
);
this.clearLocalTeamDataAndFinishLogout(webviewPostMessage); this.clearLocalTeamDataAndFinishLogout(webviewPostMessage);
return; return;
} }
const teamData = this.context.globalState.get<TeamData>('teamData'); const teamData = this.context.globalState.get<TeamData>('teamData');
if (teamData === undefined) { if (teamData === undefined) {
outputPanelLog.error("Team requested logout with a locally stored token but no teamData. Switching to logged out state."); outputPanelLog.error(
'Team requested logout with a locally stored token but no teamData. Switching to logged out state.'
);
this.clearLocalTeamDataAndFinishLogout(webviewPostMessage); this.clearLocalTeamDataAndFinishLogout(webviewPostMessage);
return; return;
} }
@ -138,16 +160,21 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}); });
if (res.status !== 200) { if (res.status !== 200) {
outputPanelLog.error(`Team requested logout, failed with status code ${res.status}. Not modifying local state.`); outputPanelLog.error(
`Team requested logout, failed with status code ${res.status}. Not modifying local state.`
);
vscode.window.showErrorMessage(`BWContest: Logout failed with code ${res.status}`); vscode.window.showErrorMessage(`BWContest: Logout failed with code ${res.status}`);
return; return;
}; }
const data2 = await res.json(); const data2 = await res.json();
const responseMessage = data2.message ? `Message: ${data2.message}` : ''; const responseMessage = data2.message ? `Message: ${data2.message}` : '';
if (data2.success !== true) { if (data2.success !== true) {
outputPanelLog.error(`Team requested logout, failed with normal status code. Not modifying local state. ` + responseMessage); outputPanelLog.error(
`Team requested logout, failed with normal status code. Not modifying local state. ` +
responseMessage
);
vscode.window.showErrorMessage(`BWContest: Logout failed.`); vscode.window.showErrorMessage(`BWContest: Logout failed.`);
return; return;
} }
@ -166,44 +193,51 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
this.context.globalState.update('teamData', undefined); this.context.globalState.update('teamData', undefined);
} }
public updateTeamStatus(contestTeamState : ContestTeamState | null, changedProblemIds = new Set<number>) { public updateTeamStatus(
contestTeamState: ContestTeamState | null,
changedProblemIds = new Set<number>()
) {
if (contestTeamState == null) { if (contestTeamState == null) {
outputPanelLog.trace("Not updating sidebar submission list because provided state is null"); outputPanelLog.trace('Not updating sidebar submission list because provided state is null');
return; return;
} }
if (this.webview == null) { if (this.webview == null) {
outputPanelLog.trace("Not updating sidebar submission list because webview is null"); outputPanelLog.trace('Not updating sidebar submission list because webview is null');
return; return;
} }
const contestState = contestTeamState.contestState; const contestState = contestTeamState.contestState;
const problemsWithSubmissions = contestState.problems.map<SidebarProblemWithSubmissions>(p => ({ const problemsWithSubmissions = contestState.problems.map<SidebarProblemWithSubmissions>(
(p) => ({
problem: p, problem: p,
overallState: calculateOverallState(contestTeamState.submissionsList.get(p.id) ?? []), overallState: calculateOverallState(contestTeamState.submissionsList.get(p.id) ?? []),
submissions: contestTeamState.submissionsList.get(p.id) ?? [], submissions: contestTeamState.submissionsList.get(p.id) ?? [],
modified: changedProblemIds.has(p.id) modified: changedProblemIds.has(p.id)
})); })
);
const teamStatus: SidebarTeamStatus = { const teamStatus: SidebarTeamStatus = {
contestState, contestState,
correctProblems: problemsWithSubmissions.filter(p => p.overallState === 'Correct'), correctProblems: problemsWithSubmissions.filter((p) => p.overallState === 'Correct'),
processingProblems: problemsWithSubmissions.filter(p => p.overallState === 'Processing'), processingProblems: problemsWithSubmissions.filter((p) => p.overallState === 'Processing'),
incorrectProblems: problemsWithSubmissions.filter(p => p.overallState === 'Incorrect'), incorrectProblems: problemsWithSubmissions.filter((p) => p.overallState === 'Incorrect'),
notStartedProblems: problemsWithSubmissions.filter(p => p.overallState === null), notStartedProblems: problemsWithSubmissions.filter((p) => p.overallState === null)
} };
const message: WebviewMessageType = { const message: WebviewMessageType = {
msg: 'teamStatusUpdated', msg: 'teamStatusUpdated',
data: teamStatus data: teamStatus
}; };
outputPanelLog.trace("Posting teamStatusUpdated to webview with message: " + JSON.stringify(message)); outputPanelLog.trace(
'Posting teamStatusUpdated to webview with message: ' + JSON.stringify(message)
);
this.webview.postMessage(message); this.webview.postMessage(message);
} }
public resolveWebviewView(webviewView: vscode.WebviewView) { public resolveWebviewView(webviewView: vscode.WebviewView) {
outputPanelLog.trace("SidebarProvider resolveWebviewView"); outputPanelLog.trace('SidebarProvider resolveWebviewView');
const webview = webviewView.webview; const webview = webviewView.webview;
this.webview = webview; this.webview = webview;
webview.options = { webview.options = {
@ -225,7 +259,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
break; break;
} }
case 'onUIMount': { case 'onUIMount': {
outputPanelLog.trace("SidebarProvider onUIMount"); outputPanelLog.trace('SidebarProvider onUIMount');
const token = this.context.globalState.get<string>('token'); const token = this.context.globalState.get<string>('token');
const teamData = this.context.globalState.get<TeamData>('teamData'); const teamData = this.context.globalState.get<TeamData>('teamData');
if (token !== undefined && teamData !== undefined) { if (token !== undefined && teamData !== undefined) {
@ -235,7 +269,9 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}); });
const currentSubmissionsList = getCachedContestTeamState(); const currentSubmissionsList = getCachedContestTeamState();
outputPanelLog.trace("onUIMount, currentSubmissionsList is " + JSON.stringify(currentSubmissionsList)); outputPanelLog.trace(
'onUIMount, currentSubmissionsList is ' + JSON.stringify(currentSubmissionsList)
);
this.updateTeamStatus(currentSubmissionsList); this.updateTeamStatus(currentSubmissionsList);
} }
break; break;
@ -297,17 +333,16 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
</html>`; </html>`;
} }
} }
function calculateOverallState(submissions: SubmissionForExtension[]): SubmissionStateForExtension | null { function calculateOverallState(
if (submissions.find(s => s.state === 'Correct')) { submissions: SubmissionForExtension[]
): SubmissionStateForExtension | null {
if (submissions.find((s) => s.state === 'Correct')) {
return 'Correct'; return 'Correct';
} } else if (submissions.find((s) => s.state === 'Processing')) {
else if (submissions.find(s => s.state === 'Processing')) {
return 'Processing'; return 'Processing';
} } else if (submissions.find((s) => s.state === 'Incorrect')) {
else if (submissions.find(s => s.state === 'Incorrect')) {
return 'Incorrect'; return 'Incorrect';
} } else {
else {
return null; return null;
} }
} }

View File

@ -1,29 +1,29 @@
export type FullStateForExtension = { export type FullStateForExtension = {
contestState: ContestStateForExtension, contestState: ContestStateForExtension;
submissions: SubmissionForExtension[] submissions: SubmissionForExtension[];
} };
export type ProblemNameForExtension = { export type ProblemNameForExtension = {
id: number, id: number;
friendlyName: string, friendlyName: string;
} };
export type ContestStateForExtension = { export type ContestStateForExtension = {
startTime: Date | null, startTime: Date | null;
endTime: Date | null, endTime: Date | null;
problems: ProblemNameForExtension[], problems: ProblemNameForExtension[];
isActive: boolean, isActive: boolean;
isScoreboardFrozen: boolean, isScoreboardFrozen: boolean;
} };
export type SubmissionStateForExtension = 'Processing' | 'Correct' | 'Incorrect'; export type SubmissionStateForExtension = 'Processing' | 'Correct' | 'Incorrect';
export type SubmissionForExtension = { export type SubmissionForExtension = {
id: number, id: number;
contestId: number, contestId: number;
teamId: number, teamId: number;
problemId: number, problemId: number;
createdAt: Date, createdAt: Date;
state: SubmissionStateForExtension state: SubmissionStateForExtension;
message: string | null message: string | null;
} };

View File

@ -2,18 +2,23 @@ import * as vscode from 'vscode';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
import outputPanelLog from '../outputPanelLog'; import outputPanelLog from '../outputPanelLog';
import { extensionSettings } from '../extension'; import { extensionSettings } from '../extension';
import { ContestStateForExtension, ProblemNameForExtension, FullStateForExtension, SubmissionForExtension } from './contestMonitorSharedTypes'; import {
ContestStateForExtension,
ProblemNameForExtension,
FullStateForExtension,
SubmissionForExtension
} from './contestMonitorSharedTypes';
import { LiteEvent } from '../utilities/LiteEvent'; import { LiteEvent } from '../utilities/LiteEvent';
export type ContestTeamState = { export type ContestTeamState = {
contestState: ContestStateForExtension, contestState: ContestStateForExtension;
submissionsList: Map<number, SubmissionForExtension[]> submissionsList: Map<number, SubmissionForExtension[]>;
} };
export type SubmissionListStateChangedEventArgs = { export type SubmissionListStateChangedEventArgs = {
contestTeamState: ContestTeamState, contestTeamState: ContestTeamState;
changedProblemIds: Set<number> changedProblemIds: Set<number>;
} };
let latestContestTeamState: ContestTeamState | null = null; let latestContestTeamState: ContestTeamState | null = null;
@ -39,79 +44,107 @@ export async function pollContestStatus(context: vscode.ExtensionContext) {
return; return;
} }
const contestStateResponse = await fetch(urlJoin(extensionSettings().webUrl, `api/team/${sessionToken}/contestState`), { const contestStateResponse = await fetch(
urlJoin(extensionSettings().webUrl, `api/team/${sessionToken}/contestState`),
{
method: 'GET' method: 'GET'
}); }
);
if (contestStateResponse.status != 200) { if (contestStateResponse.status != 200) {
outputPanelLog.trace(` Ending poll #${pollNum}: Status check API returned http status ${contestStateResponse.status}`); outputPanelLog.trace(
` Ending poll #${pollNum}: Status check API returned http status ${contestStateResponse.status}`
);
return; return;
} }
const data = await contestStateResponse.json(); const data = await contestStateResponse.json();
if (!data.success) { if (!data.success) {
outputPanelLog.trace(` Ending poll #${pollNum}: Status check returned OK but was not successful`); outputPanelLog.trace(
` Ending poll #${pollNum}: Status check returned OK but was not successful`
);
return; return;
} }
const fullState: FullStateForExtension = data.data; const fullState: FullStateForExtension = data.data;
outputPanelLog.trace(` Poll #${pollNum} succeeded. Submission count: ${fullState.submissions.length}. Diffing...`); outputPanelLog.trace(
` Poll #${pollNum} succeeded. Submission count: ${fullState.submissions.length}. Diffing...`
);
diffAndUpdateContestState(fullState); diffAndUpdateContestState(fullState);
} }
function diffAndUpdateContestState(fullState: FullStateForExtension) { function diffAndUpdateContestState(fullState: FullStateForExtension) {
const contestState = fullState.contestState; const contestState = fullState.contestState;
const currentSubmissionsList = createProblemSubmissionsLookup(contestState.problems, fullState.submissions); const currentSubmissionsList = createProblemSubmissionsLookup(
contestState.problems,
fullState.submissions
);
const changedProblemIds = new Set<number>(); const changedProblemIds = new Set<number>();
let anythingChanged = false; let anythingChanged = false;
if (latestContestTeamState == null) { if (latestContestTeamState == null) {
outputPanelLog.trace(` No previously cached data to diff`); outputPanelLog.trace(` No previously cached data to diff`);
anythingChanged = true; anythingChanged = true;
} } else {
else {
for (const problem of contestState.problems) { for (const problem of contestState.problems) {
const problemId = problem.id; const problemId = problem.id;
const currentSubmissionsForProblem = currentSubmissionsList.get(problemId) ?? []; const currentSubmissionsForProblem = currentSubmissionsList.get(problemId) ?? [];
const cachedSubmissionsForProblem = latestContestTeamState.submissionsList.get(problemId) ?? []; const cachedSubmissionsForProblem =
latestContestTeamState.submissionsList.get(problemId) ?? [];
const currentSubmissionsAlreadyInCache = currentSubmissionsForProblem!.filter(s => cachedSubmissionsForProblem.find(ss => ss.id == s.id)); const currentSubmissionsAlreadyInCache = currentSubmissionsForProblem!.filter((s) =>
const currentSubmissionsNotInCache = currentSubmissionsForProblem!.filter(s => !cachedSubmissionsForProblem.find(ss => ss.id == s.id)); cachedSubmissionsForProblem.find((ss) => ss.id == s.id)
const cachedSubmissionsNotInCurrent = cachedSubmissionsForProblem.filter(s => !currentSubmissionsForProblem!.find(ss => ss.id == s.id)); );
const currentSubmissionsNotInCache = currentSubmissionsForProblem!.filter(
(s) => !cachedSubmissionsForProblem.find((ss) => ss.id == s.id)
);
const cachedSubmissionsNotInCurrent = cachedSubmissionsForProblem.filter(
(s) => !currentSubmissionsForProblem!.find((ss) => ss.id == s.id)
);
for (const currentSubmission of currentSubmissionsAlreadyInCache ) { for (const currentSubmission of currentSubmissionsAlreadyInCache) {
const previousSubmission = cachedSubmissionsForProblem.find(s => s.id == currentSubmission.id)!; const previousSubmission = cachedSubmissionsForProblem.find(
(s) => s.id == currentSubmission.id
)!;
if (currentSubmission.state != previousSubmission.state) { if (currentSubmission.state != previousSubmission.state) {
anythingChanged = true; anythingChanged = true;
changedProblemIds.add(problem.id); changedProblemIds.add(problem.id);
outputPanelLog.trace(` Submission state for #${currentSubmission.id} changed from ${previousSubmission.state} (message '${previousSubmission.message}') to ${currentSubmission.state} (message '${currentSubmission.message}')`); outputPanelLog.trace(
` Submission state for #${currentSubmission.id} changed from ${previousSubmission.state} (message '${previousSubmission.message}') to ${currentSubmission.state} (message '${currentSubmission.message}')`
);
alertForNewState(problem, currentSubmission); alertForNewState(problem, currentSubmission);
} else if (currentSubmission.message != previousSubmission.message) { } else if (currentSubmission.message != previousSubmission.message) {
anythingChanged = true; anythingChanged = true;
changedProblemIds.add(problem.id); changedProblemIds.add(problem.id);
outputPanelLog.trace(` Submission message changed (with same state) for #${currentSubmission.id} from ${previousSubmission.message} to ${currentSubmission.message}`); outputPanelLog.trace(
` Submission message changed (with same state) for #${currentSubmission.id} from ${previousSubmission.message} to ${currentSubmission.message}`
);
} }
} }
for (const currentSubmission of currentSubmissionsNotInCache ) { for (const currentSubmission of currentSubmissionsNotInCache) {
anythingChanged = true; anythingChanged = true;
changedProblemIds.add(problem.id); changedProblemIds.add(problem.id);
outputPanelLog.trace(` Newly acknowledge submission #${currentSubmission.id} with state ${currentSubmission.state} and message ${currentSubmission.message}`); outputPanelLog.trace(
` Newly acknowledge submission #${currentSubmission.id} with state ${currentSubmission.state} and message ${currentSubmission.message}`
);
alertForNewState(problem, currentSubmission); alertForNewState(problem, currentSubmission);
} }
for (const previousSubmission of cachedSubmissionsNotInCurrent ) { for (const previousSubmission of cachedSubmissionsNotInCurrent) {
anythingChanged = true; anythingChanged = true;
outputPanelLog.trace(` Deleted submission #${previousSubmission.id}`); outputPanelLog.trace(` Deleted submission #${previousSubmission.id}`);
} }
} }
} }
outputPanelLog.trace(anythingChanged ? " Diff has changes, triggering events" : " No changes found"); outputPanelLog.trace(
anythingChanged ? ' Diff has changes, triggering events' : ' No changes found'
);
if (anythingChanged) { if (anythingChanged) {
latestContestTeamState = { contestState, submissionsList: currentSubmissionsList}; latestContestTeamState = { contestState, submissionsList: currentSubmissionsList };
onSubmissionsListChanged.trigger({ onSubmissionsListChanged.trigger({
contestTeamState: latestContestTeamState, contestTeamState: latestContestTeamState,
changedProblemIds changedProblemIds
@ -119,45 +152,65 @@ function diffAndUpdateContestState(fullState: FullStateForExtension) {
} }
} }
function createProblemSubmissionsLookup(problems: ProblemNameForExtension[], submissions: SubmissionForExtension[]): Map<number, SubmissionForExtension[]> { function createProblemSubmissionsLookup(
problems: ProblemNameForExtension[],
submissions: SubmissionForExtension[]
): Map<number, SubmissionForExtension[]> {
const orderedSubmissionsByProblemId = new Map<number, SubmissionForExtension[]>(); const orderedSubmissionsByProblemId = new Map<number, SubmissionForExtension[]>();
for (const problem of problems) { for (const problem of problems) {
orderedSubmissionsByProblemId.set(problem.id, []); orderedSubmissionsByProblemId.set(problem.id, []);
} }
for (const submission of submissions.sort(s => s.id)) { for (const submission of submissions.sort((s) => s.id)) {
orderedSubmissionsByProblemId.get(submission.problemId)!.push(submission); orderedSubmissionsByProblemId.get(submission.problemId)!.push(submission);
} }
return orderedSubmissionsByProblemId; return orderedSubmissionsByProblemId;
} }
function alertForNewState(problem: ProblemNameForExtension, currentSubmission: SubmissionForExtension) { function alertForNewState(
problem: ProblemNameForExtension,
currentSubmission: SubmissionForExtension
) {
// Only alert on state changes team cares about // Only alert on state changes team cares about
if (currentSubmission.state === 'Correct') { if (currentSubmission.state === 'Correct') {
vscode.window.showInformationMessage(`BWContest Judge: CORRECT Submission '${problem.friendlyName}'`); vscode.window.showInformationMessage(
`BWContest Judge: CORRECT Submission '${problem.friendlyName}'`
);
} else if (currentSubmission.state === 'Incorrect') { } else if (currentSubmission.state === 'Incorrect') {
const messageDisplayText = currentSubmission.message ? `Message: ${currentSubmission.message}` : ''; const messageDisplayText = currentSubmission.message
vscode.window.showInformationMessage(`BWContest Judge: INCORRECT Submission '${problem.friendlyName}' ${messageDisplayText}`); ? `Message: ${currentSubmission.message}`
: '';
vscode.window.showInformationMessage(
`BWContest Judge: INCORRECT Submission '${problem.friendlyName}' ${messageDisplayText}`
);
} }
} }
export function recordInitialSubmission(submission: SubmissionForExtension): void { export function recordInitialSubmission(submission: SubmissionForExtension): void {
outputPanelLog.trace("Server received new submission, #" + submission.id); outputPanelLog.trace('Server received new submission, #' + submission.id);
if (!latestContestTeamState) { if (!latestContestTeamState) {
outputPanelLog.trace(" No locally cached submission list state, the normal polling cycle will update the list"); outputPanelLog.trace(
' No locally cached submission list state, the normal polling cycle will update the list'
);
return; return;
} }
const existingSubmissionListForProblem = latestContestTeamState.submissionsList.get(submission.problemId); const existingSubmissionListForProblem = latestContestTeamState.submissionsList.get(
submission.problemId
);
if (existingSubmissionListForProblem === undefined) { if (existingSubmissionListForProblem === undefined) {
outputPanelLog.trace(` The cached submission list does not know about problemId #${submission.problemId}. Next polling cycle should fix consistency.`); outputPanelLog.trace(
` The cached submission list does not know about problemId #${submission.problemId}. Next polling cycle should fix consistency.`
);
return; return;
} }
if (existingSubmissionListForProblem.find(s => s.id == submission.id)) { if (existingSubmissionListForProblem.find((s) => s.id == submission.id)) {
outputPanelLog.trace(` The cached submission list already knows about submissionId #${submission.id}`); outputPanelLog.trace(
` The cached submission list already knows about submissionId #${submission.id}`
);
return; return;
} }
@ -165,6 +218,6 @@ export function recordInitialSubmission(submission: SubmissionForExtension): voi
existingSubmissionListForProblem.push(submission); existingSubmissionListForProblem.push(submission);
onSubmissionsListChanged.trigger({ onSubmissionsListChanged.trigger({
contestTeamState: latestContestTeamState, contestTeamState: latestContestTeamState,
changedProblemIds: new Set<number>([submission.problemId]), changedProblemIds: new Set<number>([submission.problemId])
}); });
} }

View File

@ -25,9 +25,8 @@ export async function startTeamStatusPolling() {
if (currentlyPolling) { if (currentlyPolling) {
outputPanelLog.trace("Tried to start team status polling, but it's already running."); outputPanelLog.trace("Tried to start team status polling, but it's already running.");
return; return;
} } else if (!extensionContext.globalState.get('token')) {
else if (!extensionContext.globalState.get('token')) { outputPanelLog.info('Tried to start team status polling, but team is not logged in.');
outputPanelLog.info("Tried to start team status polling, but team is not logged in.");
return; return;
} }
@ -38,14 +37,15 @@ export async function startTeamStatusPolling() {
async function startPollingWorker(cancellationToken: SimpleCancellationToken) { async function startPollingWorker(cancellationToken: SimpleCancellationToken) {
const pollingLoopNum = ++debugPollingLoopNum; const pollingLoopNum = ++debugPollingLoopNum;
outputPanelLog.trace(`Starting polling loop #${pollingLoopNum}, checking contest/team status every ${pollingIntervalSeconds} seconds`); outputPanelLog.trace(
`Starting polling loop #${pollingLoopNum}, checking contest/team status every ${pollingIntervalSeconds} seconds`
);
while (!cancellationToken.isCancelled) { while (!cancellationToken.isCancelled) {
try { try {
await pollContestStatus(extensionContext); await pollContestStatus(extensionContext);
} } catch (error) {
catch (error) { outputPanelLog.error('Polling contest status failed: ' + (error ?? '<unknown error>'));
outputPanelLog.error("Polling contest status failed: " + (error ?? "<unknown error>"));
} }
await sleep(pollingIntervalSeconds * 1000); await sleep(pollingIntervalSeconds * 1000);
@ -55,12 +55,16 @@ async function startPollingWorker(cancellationToken: SimpleCancellationToken) {
} }
export function stopTeamStatusPolling() { export function stopTeamStatusPolling() {
outputPanelLog.trace("Stopping team status polling"); outputPanelLog.trace('Stopping team status polling');
currentPollingCancellationToken?.cancel(); currentPollingCancellationToken?.cancel();
currentlyPolling = false; currentlyPolling = false;
} }
export function useFastPolling(enabled: boolean): void { export function useFastPolling(enabled: boolean): void {
pollingIntervalSeconds = enabled ? developerFastPollingIntervalSeconds : defaultPollingIntervalSeconds; pollingIntervalSeconds = enabled
outputPanelLog.info(`Changed polling interval to ${pollingIntervalSeconds} seconds. Takes effect after current delay.`); ? developerFastPollingIntervalSeconds
: defaultPollingIntervalSeconds;
outputPanelLog.info(
`Changed polling interval to ${pollingIntervalSeconds} seconds. Takes effect after current delay.`
);
} }

View File

@ -6,7 +6,11 @@ import git from 'isomorphic-git';
import path = require('path'); import path = require('path');
import http from 'isomorphic-git/http/node'; import http from 'isomorphic-git/http/node';
import outputPanelLog from './outputPanelLog'; import outputPanelLog from './outputPanelLog';
import { startTeamStatusPollingOnActivation, stopTeamStatusPolling, useFastPolling } from './contestMonitor/pollingService'; import {
startTeamStatusPollingOnActivation,
stopTeamStatusPolling,
useFastPolling
} from './contestMonitor/pollingService';
export interface BWContestSettings { export interface BWContestSettings {
repoBaseUrl: string; repoBaseUrl: string;
@ -83,13 +87,14 @@ export async function cloneAndOpenRepo(contestId: number, teamId: number) {
outputPanelLog.info(`Running 'git clone' to directory: ${dir}`); outputPanelLog.info(`Running 'git clone' to directory: ${dir}`);
try { try {
await git.clone({ fs, http, dir, url: repoUrl }); await git.clone({ fs, http, dir, url: repoUrl });
} } catch (error) {
catch (error) { outputPanelLog.error(
outputPanelLog.error("Failed to 'git clone'. The git server might be incorrectly configured. Error: " + error); "Failed to 'git clone'. The git server might be incorrectly configured. Error: " + error
);
throw error; throw error;
} }
outputPanelLog.info("Closing workspaces..."); outputPanelLog.info('Closing workspaces...');
closeAllWorkspaces(); closeAllWorkspaces();
const addedFolder = vscode.workspace.updateWorkspaceFolders( const addedFolder = vscode.workspace.updateWorkspaceFolders(
@ -107,7 +112,7 @@ export async function cloneAndOpenRepo(contestId: number, teamId: number) {
} }
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
outputPanelLog.info("BWContest Extension Activated"); outputPanelLog.info('BWContest Extension Activated');
const sidebarProvider = new SidebarProvider( const sidebarProvider = new SidebarProvider(
context.extensionUri, context.extensionUri,
@ -122,7 +127,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.registerWebviewViewProvider('bwcontest-sidebar', sidebarProvider), vscode.window.registerWebviewViewProvider('bwcontest-sidebar', sidebarProvider),
vscode.commands.registerCommand('bwcontest.toggleFastPolling', () => { vscode.commands.registerCommand('bwcontest.toggleFastPolling', () => {
if (!extensionSettings().debugFastPolling) { if (!extensionSettings().debugFastPolling) {
outputPanelLog.trace("Tried to toggle fast polling, but not allowed."); outputPanelLog.trace('Tried to toggle fast polling, but not allowed.');
return; return;
} }
@ -135,7 +140,6 @@ export function activate(context: vscode.ExtensionContext) {
} }
export function deactivate() { export function deactivate() {
outputPanelLog.info("BWContest Extension Deactivated"); outputPanelLog.info('BWContest Extension Deactivated');
stopTeamStatusPolling(); stopTeamStatusPolling();
} }

View File

@ -1,7 +1,7 @@
import { window } from "vscode"; import { window } from 'vscode';
/** Logs to the Output panel of a team's VS Code instance. Useful for diagnosing issues. /** Logs to the Output panel of a team's VS Code instance. Useful for diagnosing issues.
* *
* Do NOT output anything secret here. */ * Do NOT output anything secret here. */
const outputPanelLog = window.createOutputChannel('BWContest Log', {log: true}); const outputPanelLog = window.createOutputChannel('BWContest Log', { log: true });
export default outputPanelLog; export default outputPanelLog;

View File

@ -56,7 +56,7 @@ export class BWPanel {
} }
public static show(context: vscode.ExtensionContext, webUrl: string) { public static show(context: vscode.ExtensionContext, webUrl: string) {
outputPanelLog.info("Showing BWPanel"); outputPanelLog.info('Showing BWPanel');
const column = vscode.window.activeTextEditor const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn ? vscode.window.activeTextEditor.viewColumn
: undefined; : undefined;
@ -129,15 +129,21 @@ export class BWPanel {
} }
try { try {
const submissionResult = await submitProblem(sessionToken, teamData.contestId, teamData.teamId, problemId); const submissionResult = await submitProblem(
sessionToken,
teamData.contestId,
teamData.teamId,
problemId
);
if (submissionResult.success === true) { if (submissionResult.success === true) {
recordInitialSubmission(submissionResult.submission); recordInitialSubmission(submissionResult.submission);
vscode.window.showInformationMessage(`Submitted '${problem.name}'!`); vscode.window.showInformationMessage(`Submitted '${problem.name}'!`);
} else { } else {
vscode.window.showErrorMessage(`Error submitting '${problem.name}': ${submissionResult.message}`); vscode.window.showErrorMessage(
`Error submitting '${problem.name}': ${submissionResult.message}`
);
} }
} } catch (error) {
catch (error) {
vscode.window.showErrorMessage(`Web error submitting '${problem.name}'`); vscode.window.showErrorMessage(`Web error submitting '${problem.name}'`);
outputPanelLog.error(`Web error submitting '${problem.name}': ${error}`); outputPanelLog.error(`Web error submitting '${problem.name}': ${error}`);
} }
@ -195,7 +201,7 @@ export class BWPanel {
res.runResult.then(() => { res.runResult.then(() => {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ msg: 'onRunningDone' }); this.webviewPostMessage({ msg: 'onRunningDone' });
}) });
} else { } else {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ this.webviewPostMessage({
@ -218,13 +224,13 @@ export class BWPanel {
outputBuffer.push(data); outputBuffer.push(data);
this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') });
} }
}) });
if (res.success === true) { if (res.success === true) {
killFunc = res.killFunc; killFunc = res.killFunc;
res.runResult.then(() => { res.runResult.then(() => {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ msg: 'onRunningDone' }); this.webviewPostMessage({ msg: 'onRunningDone' });
}) });
} else { } else {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ this.webviewPostMessage({
@ -238,18 +244,23 @@ export class BWPanel {
input, input,
cppPlatform: process.platform === 'win32' ? 'VisualStudio' : 'GCC', cppPlatform: process.platform === 'win32' ? 'VisualStudio' : 'GCC',
problemName: problem.pascalName, problemName: problem.pascalName,
srcDir: join(repoDir, 'BWContest', teamData.contestId.toString(), teamData.teamId.toString()), srcDir: join(
repoDir,
'BWContest',
teamData.contestId.toString(),
teamData.teamId.toString()
),
outputCallback: (data) => { outputCallback: (data) => {
outputBuffer.push(data); outputBuffer.push(data);
this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') });
} }
}) });
if (res.success === true) { if (res.success === true) {
killFunc = res.killFunc; killFunc = res.killFunc;
res.runResult.then(() => { res.runResult.then(() => {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ msg: 'onRunningDone' }); this.webviewPostMessage({ msg: 'onRunningDone' });
}) });
} else { } else {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ this.webviewPostMessage({

View File

@ -1,6 +1,12 @@
import { join } from 'path'; import { join } from 'path';
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import { timeoutSeconds, type IRunner, type IRunnerParams, type IRunnerReturn, type RunResult } from './types'; import {
timeoutSeconds,
type IRunner,
type IRunnerParams,
type IRunnerReturn,
type RunResult
} from './types';
import kill = require('tree-kill'); import kill = require('tree-kill');
import * as os from 'os'; import * as os from 'os';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';

View File

@ -1,7 +1,13 @@
import { join } from 'path'; import { join } from 'path';
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import * as util from 'util'; import * as util from 'util';
import { timeoutSeconds, type IRunner, type IRunnerParams, type IRunnerReturn, type RunResult } from './types'; import {
timeoutSeconds,
type IRunner,
type IRunnerParams,
type IRunnerReturn,
type RunResult
} from './types';
import kill = require('tree-kill'); import kill = require('tree-kill');
const execPromise = util.promisify(exec); const execPromise = util.promisify(exec);

View File

@ -12,7 +12,9 @@ export async function submitProblem(
contestId: number, contestId: number,
teamId: number, teamId: number,
problemId: number problemId: number
): Promise<{ success: true; submission: SubmissionForExtension } | { success: false; message: string }> { ): Promise<
{ success: true; submission: SubmissionForExtension } | { success: false; message: string }
> {
outputPanelLog.info(`Submitting problem id #{${problemId}}...`); outputPanelLog.info(`Submitting problem id #{${problemId}}...`);
let hash: string; let hash: string;
@ -30,9 +32,8 @@ export async function submitProblem(
author: { name: `Team ${teamId}` }, author: { name: `Team ${teamId}` },
message: `Submit problem ${problemId}` message: `Submit problem ${problemId}`
}); });
} } catch (error) {
catch (error) { outputPanelLog.error('Fail to make commit for submission: ' + JSON.stringify(error));
outputPanelLog.error("Fail to make commit for submission: " + JSON.stringify(error));
throw error; throw error;
} }

View File

@ -1,6 +1,6 @@
// Modified from JasonKleban @ https://gist.github.com/JasonKleban/50cee44960c225ac1993c922563aa540 // Modified from JasonKleban @ https://gist.github.com/JasonKleban/50cee44960c225ac1993c922563aa540
export { ILiteEvent, LiteEvent } export { ILiteEvent, LiteEvent };
interface ILiteEvent<T> { interface ILiteEvent<T> {
add(handler: { (data?: T): void }): void; add(handler: { (data?: T): void }): void;
@ -8,7 +8,7 @@ interface ILiteEvent<T> {
} }
class LiteEvent<T> implements ILiteEvent<T> { class LiteEvent<T> implements ILiteEvent<T> {
protected handlers: { (data?: T): void; }[] = []; protected handlers: { (data?: T): void }[] = [];
public add(handler: { (data?: T): void }): void { public add(handler: { (data?: T): void }): void {
this.handlers.push(handler); this.handlers.push(handler);
@ -16,12 +16,12 @@ class LiteEvent<T> implements ILiteEvent<T> {
public remove(handler: { (data?: T): void }): boolean { public remove(handler: { (data?: T): void }): boolean {
const countBefore = this.handlers.length; const countBefore = this.handlers.length;
this.handlers = this.handlers.filter(h => h !== handler); this.handlers = this.handlers.filter((h) => h !== handler);
return countBefore != this.handlers.length; return countBefore != this.handlers.length;
} }
public trigger(data?: T) { public trigger(data?: T) {
this.handlers.slice(0).forEach(h => h(data)); this.handlers.slice(0).forEach((h) => h(data));
} }
public expose(): ILiteEvent<T> { public expose(): ILiteEvent<T> {

View File

@ -1,6 +1,8 @@
export class SimpleCancellationToken { export class SimpleCancellationToken {
private _isCancelled: boolean = false; private _isCancelled: boolean = false;
get isCancelled() { return this._isCancelled; } get isCancelled() {
return this._isCancelled;
}
cancel(): void { cancel(): void {
this._isCancelled = true; this._isCancelled = true;

View File

@ -1,3 +1,3 @@
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }

View File

@ -2,7 +2,11 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import SidebarProblemStatus from './SidebarProblemStatus.svelte'; import SidebarProblemStatus from './SidebarProblemStatus.svelte';
import type { TeamData } from '../../src/sharedTypes'; import type { TeamData } from '../../src/sharedTypes';
import type { WebviewMessageType, MessageType, SidebarTeamStatus } from '../../src/SidebarProvider'; import type {
WebviewMessageType,
MessageType,
SidebarTeamStatus
} from '../../src/SidebarProvider';
let teamname: string; let teamname: string;
let password: string; let password: string;
@ -122,7 +126,10 @@
</div> </div>
{#if teamStatus.processingProblems.length > 0} {#if teamStatus.processingProblems.length > 0}
{#each teamStatus.processingProblems as inProgressProblem (JSON.stringify(inProgressProblem))} {#each teamStatus.processingProblems as inProgressProblem (JSON.stringify(inProgressProblem))}
<SidebarProblemStatus problem={inProgressProblem} contestState={teamStatus.contestState} /> <SidebarProblemStatus
problem={inProgressProblem}
contestState={teamStatus.contestState}
/>
{/each} {/each}
{:else} {:else}
<div class="problemSectionExplanation">No pending submissions</div> <div class="problemSectionExplanation">No pending submissions</div>
@ -131,11 +138,16 @@
<div class="problemResultsSection"> <div class="problemResultsSection">
<div> <div>
<span class="problemResultsSectionHeader correct">Correct </span> <span class="problemResultsSectionHeader correct">Correct </span>
<span class="problemResultsSectionCount">{teamStatus.correctProblems.length} of {totalProblems}</span> <span class="problemResultsSectionCount"
>{teamStatus.correctProblems.length} of {totalProblems}</span
>
</div> </div>
{#if teamStatus.correctProblems.length > 0} {#if teamStatus.correctProblems.length > 0}
{#each teamStatus.correctProblems as correctProblem (JSON.stringify(correctProblem))} {#each teamStatus.correctProblems as correctProblem (JSON.stringify(correctProblem))}
<SidebarProblemStatus problem={correctProblem} contestState={teamStatus.contestState} /> <SidebarProblemStatus
problem={correctProblem}
contestState={teamStatus.contestState}
/>
{/each} {/each}
{:else} {:else}
<div class="problemSectionExplanation">Solved problems appear here</div> <div class="problemSectionExplanation">Solved problems appear here</div>
@ -144,26 +156,34 @@
<div class="problemResultsSection"> <div class="problemResultsSection">
<div> <div>
<span class="problemResultsSectionHeader incorrect">Incorrect </span> <span class="problemResultsSectionHeader incorrect">Incorrect </span>
<span class="problemResultsSectionCount">{teamStatus.incorrectProblems.length} of {totalProblems}</span> <span class="problemResultsSectionCount"
>{teamStatus.incorrectProblems.length} of {totalProblems}</span
>
</div> </div>
{#if teamStatus.incorrectProblems.length > 0} {#if teamStatus.incorrectProblems.length > 0}
{#each teamStatus.incorrectProblems as incorrectProblem (JSON.stringify(incorrectProblem))} {#each teamStatus.incorrectProblems as incorrectProblem (JSON.stringify(incorrectProblem))}
<SidebarProblemStatus problem={incorrectProblem} contestState={teamStatus.contestState} /> <SidebarProblemStatus
problem={incorrectProblem}
contestState={teamStatus.contestState}
/>
{/each} {/each}
{:else} {:else}
<div class="problemSectionExplanation"> <div class="problemSectionExplanation">Attempted problems appear here until solved</div>
Attempted problems appear here until solved
</div>
{/if} {/if}
</div> </div>
{#if teamStatus.notStartedProblems.length > 0} {#if teamStatus.notStartedProblems.length > 0}
<div class="problemResultsSection"> <div class="problemResultsSection">
<div> <div>
<span class="problemResultsSectionHeader notAttempted">Not Attempted </span> <span class="problemResultsSectionHeader notAttempted">Not Attempted </span>
<span class="problemResultsSectionCount">{teamStatus.notStartedProblems.length} of {totalProblems}</span> <span class="problemResultsSectionCount"
>{teamStatus.notStartedProblems.length} of {totalProblems}</span
>
</div> </div>
{#each teamStatus.notStartedProblems as notStartedProblem (JSON.stringify(notStartedProblem))} {#each teamStatus.notStartedProblems as notStartedProblem (JSON.stringify(notStartedProblem))}
<SidebarProblemStatus problem={notStartedProblem} contestState={teamStatus.contestState} /> <SidebarProblemStatus
problem={notStartedProblem}
contestState={teamStatus.contestState}
/>
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -1,32 +1,48 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export const watSubmissionsImageUrl = export const watSubmissionsImageUrl = new URL(
new URL('../../media/SubmissionIcons/TeamPanel/none.png', import.meta.url).href; '../../media/SubmissionIcons/TeamPanel/none.png',
import.meta.url
).href;
export const correctSubmissionImageUrl = export const correctSubmissionImageUrl = new URL(
new URL('../../media/SubmissionIcons/TeamPanel/correct.png', import.meta.url).href; '../../media/SubmissionIcons/TeamPanel/correct.png',
import.meta.url
).href;
export const incorrectSubmissionImageUrl = export const incorrectSubmissionImageUrl = new URL(
new URL('../../media/SubmissionIcons/TeamPanel/incorrect.png', import.meta.url).href; '../../media/SubmissionIcons/TeamPanel/incorrect.png',
import.meta.url
).href;
export const pendingSubmissionImageUrl = export const pendingSubmissionImageUrl = new URL(
new URL('../../media/SubmissionIcons/TeamPanel/unknown.png', import.meta.url).href; '../../media/SubmissionIcons/TeamPanel/unknown.png',
import.meta.url
).href;
export const noSubmissionsImageUrl = export const noSubmissionsImageUrl = new URL(
new URL('../../media/SubmissionIcons/TeamPanel/none.png', import.meta.url).href; '../../media/SubmissionIcons/TeamPanel/none.png',
import.meta.url
).href;
</script> </script>
<script lang="ts"> <script lang="ts">
import type { SidebarProblemWithSubmissions } from '../../src/SidebarProvider'; import type { SidebarProblemWithSubmissions } from '../../src/SidebarProvider';
import type { ContestStateForExtension, SubmissionForExtension, SubmissionStateForExtension } from '../../src/contestMonitor/contestMonitorSharedTypes'; import type {
ContestStateForExtension,
SubmissionForExtension,
SubmissionStateForExtension
} from '../../src/contestMonitor/contestMonitorSharedTypes';
export let contestState: ContestStateForExtension; export let contestState: ContestStateForExtension;
export let problem: SidebarProblemWithSubmissions; export let problem: SidebarProblemWithSubmissions;
const sortedSubmissions = problem.submissions const sortedSubmissions = problem.submissions
? problem.submissions.sort((s1, s2) => Date.parse(s1.createdAt.toString()) - Date.parse(s2.createdAt.toString())) ? problem.submissions.sort(
(s1, s2) => Date.parse(s1.createdAt.toString()) - Date.parse(s2.createdAt.toString())
)
: []; : [];
const highlightClasses = `${(problem.modified ? "highlight" : "")} ${problem.overallState?.toLowerCase()}`; const highlightClasses = `${problem.modified ? 'highlight' : ''} ${problem.overallState?.toLowerCase()}`;
function getStatusImageUrl(overallState: SubmissionStateForExtension | null): string { function getStatusImageUrl(overallState: SubmissionStateForExtension | null): string {
switch (overallState) { switch (overallState) {
@ -62,8 +78,8 @@
} }
</script> </script>
<div class={"problemStatusDiv " + highlightClasses}> <div class={'problemStatusDiv ' + highlightClasses}>
<div class={"problemHeaderDiv"}> <div class={'problemHeaderDiv'}>
<img <img
class="overallStatusImage" class="overallStatusImage"
src={getStatusImageUrl(problem.overallState)} src={getStatusImageUrl(problem.overallState)}
@ -76,28 +92,38 @@
<span class="problemHeaderName">{problem.problem.friendlyName}</span> <span class="problemHeaderName">{problem.problem.friendlyName}</span>
<span class="problemHeaderSubmitCount"> <span class="problemHeaderSubmitCount">
{problem.submissions.length} {problem.submissions.length}
{pluralize(problem.submissions.length, 'attempt', 'attempts')}</span> {pluralize(problem.submissions.length, 'attempt', 'attempts')}</span
>
{#if problem.submissions.filter((s) => s.state === 'Processing').length > 0} {#if problem.submissions.filter((s) => s.state === 'Processing').length > 0}
<span>({problem.submissions.filter((s) => s.state === 'Processing').length} pending...)</span> <span
>({problem.submissions.filter((s) => s.state === 'Processing').length} pending...)</span
>
{/if} {/if}
{#if problem.overallState === "Correct"} {#if problem.overallState === 'Correct'}
<span class="individualSubmissionAttemptTime"> @ {getContestOffsetDisplay(problem.submissions.filter(s => s.state === "Correct")[0])}</span> <span class="individualSubmissionAttemptTime">
@ {getContestOffsetDisplay(
problem.submissions.filter((s) => s.state === 'Correct')[0]
)}</span
>
{/if} {/if}
{/if} {/if}
</div> </div>
</div> </div>
{#if problem.overallState !== "Correct"} {#if problem.overallState !== 'Correct'}
{#each sortedSubmissions as submission, i} {#each sortedSubmissions as submission, i}
<div class="individualSubmissionDiv"> <div class="individualSubmissionDiv">
<span class="individualSubmissionAttemptNumber">Submit #{i + 1}: </span> <span class="individualSubmissionAttemptNumber">Submit #{i + 1}: </span>
<img <img
class="individualSubmissionStatusImage" class="individualSubmissionStatusImage"
src={getStatusImageUrl(submission.state)} src={getStatusImageUrl(submission.state)}
alt={submission.state}/> alt={submission.state}
/>
<span class="individualSubmissionResult {submission.state.toLowerCase()}"> <span class="individualSubmissionResult {submission.state.toLowerCase()}">
{submission.state} {submission.state}
</span> </span>
<span class="individualSubmissionAttemptTime"> @ {getContestOffsetDisplay(submission)}</span> <span class="individualSubmissionAttemptTime">
@ {getContestOffsetDisplay(submission)}</span
>
</div> </div>
{#if submission.message} {#if submission.message}
<div class="individualSubmissionMessageWrapper"> <div class="individualSubmissionMessageWrapper">