[extension] Format
This commit is contained in:
parent
4a3ff56e7a
commit
fde6b0019b
@ -9,7 +9,9 @@
|
|||||||
"categories": [
|
"categories": [
|
||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"activationEvents": ["onStartupFinished"],
|
"activationEvents": [
|
||||||
|
"onStartupFinished"
|
||||||
|
],
|
||||||
"main": "./out/main.js",
|
"main": "./out/main.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"configuration": {
|
"configuration": {
|
||||||
|
@ -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>(
|
||||||
problem: p,
|
(p) => ({
|
||||||
overallState: calculateOverallState(contestTeamState.submissionsList.get(p.id) ?? []),
|
problem: p,
|
||||||
submissions: contestTeamState.submissionsList.get(p.id) ?? [],
|
overallState: calculateOverallState(contestTeamState.submissionsList.get(p.id) ?? []),
|
||||||
modified: changedProblemIds.has(p.id)
|
submissions: contestTeamState.submissionsList.get(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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
};
|
||||||
|
@ -2,27 +2,32 @@ 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;
|
||||||
|
|
||||||
export function getCachedContestTeamState(): ContestTeamState | null {
|
export function getCachedContestTeamState(): ContestTeamState | null {
|
||||||
return latestContestTeamState;
|
return latestContestTeamState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearCachedContestTeamState(): void {
|
export function clearCachedContestTeamState(): void {
|
||||||
latestContestTeamState = null;
|
latestContestTeamState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmissionsListChanged = new LiteEvent<SubmissionListStateChangedEventArgs>();
|
const onSubmissionsListChanged = new LiteEvent<SubmissionListStateChangedEventArgs>();
|
||||||
@ -30,141 +35,189 @@ export const submissionsListChanged = onSubmissionsListChanged.expose();
|
|||||||
|
|
||||||
let latestPollNum = 0;
|
let latestPollNum = 0;
|
||||||
export async function pollContestStatus(context: vscode.ExtensionContext) {
|
export async function pollContestStatus(context: vscode.ExtensionContext) {
|
||||||
const pollNum = ++latestPollNum;
|
const pollNum = ++latestPollNum;
|
||||||
outputPanelLog.trace(`Polling contest status, poll #${pollNum}`);
|
outputPanelLog.trace(`Polling contest status, poll #${pollNum}`);
|
||||||
|
|
||||||
const sessionToken = context.globalState.get('token');
|
const sessionToken = context.globalState.get('token');
|
||||||
if (!sessionToken) {
|
if (!sessionToken) {
|
||||||
outputPanelLog.trace(` Ending poll #${pollNum}: No sessionToken`);
|
outputPanelLog.trace(` Ending poll #${pollNum}: No sessionToken`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contestStateResponse = await fetch(urlJoin(extensionSettings().webUrl, `api/team/${sessionToken}/contestState`), {
|
const contestStateResponse = await fetch(
|
||||||
method: 'GET'
|
urlJoin(extensionSettings().webUrl, `api/team/${sessionToken}/contestState`),
|
||||||
});
|
{
|
||||||
|
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(
|
||||||
const changedProblemIds = new Set<number>();
|
contestState.problems,
|
||||||
|
fullState.submissions
|
||||||
|
);
|
||||||
|
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 =
|
||||||
const cachedSubmissionsForProblem = latestContestTeamState.submissionsList.get(problemId) ?? [];
|
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(
|
||||||
if (currentSubmission.state != previousSubmission.state) {
|
(s) => s.id == currentSubmission.id
|
||||||
anythingChanged = true;
|
)!;
|
||||||
changedProblemIds.add(problem.id);
|
if (currentSubmission.state != previousSubmission.state) {
|
||||||
outputPanelLog.trace(` Submission state for #${currentSubmission.id} changed from ${previousSubmission.state} (message '${previousSubmission.message}') to ${currentSubmission.state} (message '${currentSubmission.message}')`);
|
anythingChanged = true;
|
||||||
alertForNewState(problem, currentSubmission);
|
changedProblemIds.add(problem.id);
|
||||||
} else if (currentSubmission.message != previousSubmission.message) {
|
outputPanelLog.trace(
|
||||||
anythingChanged = true;
|
` Submission state for #${currentSubmission.id} changed from ${previousSubmission.state} (message '${previousSubmission.message}') to ${currentSubmission.state} (message '${currentSubmission.message}')`
|
||||||
changedProblemIds.add(problem.id);
|
);
|
||||||
outputPanelLog.trace(` Submission message changed (with same state) for #${currentSubmission.id} from ${previousSubmission.message} to ${currentSubmission.message}`);
|
alertForNewState(problem, currentSubmission);
|
||||||
}
|
} else if (currentSubmission.message != previousSubmission.message) {
|
||||||
}
|
anythingChanged = true;
|
||||||
|
changedProblemIds.add(problem.id);
|
||||||
|
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(
|
||||||
alertForNewState(problem, currentSubmission);
|
` Newly acknowledge submission #${currentSubmission.id} with state ${currentSubmission.state} and message ${currentSubmission.message}`
|
||||||
}
|
);
|
||||||
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProblemSubmissionsLookup(problems: ProblemNameForExtension[], submissions: SubmissionForExtension[]): Map<number, SubmissionForExtension[]> {
|
function createProblemSubmissionsLookup(
|
||||||
const orderedSubmissionsByProblemId = new Map<number, SubmissionForExtension[]>();
|
problems: ProblemNameForExtension[],
|
||||||
for (const problem of problems) {
|
submissions: SubmissionForExtension[]
|
||||||
orderedSubmissionsByProblemId.set(problem.id, []);
|
): Map<number, SubmissionForExtension[]> {
|
||||||
}
|
const orderedSubmissionsByProblemId = new Map<number, SubmissionForExtension[]>();
|
||||||
|
for (const problem of problems) {
|
||||||
|
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(
|
||||||
// Only alert on state changes team cares about
|
problem: ProblemNameForExtension,
|
||||||
if (currentSubmission.state === 'Correct') {
|
currentSubmission: SubmissionForExtension
|
||||||
vscode.window.showInformationMessage(`BWContest Judge: CORRECT Submission '${problem.friendlyName}'`);
|
) {
|
||||||
} else if (currentSubmission.state === 'Incorrect') {
|
// Only alert on state changes team cares about
|
||||||
const messageDisplayText = currentSubmission.message ? `Message: ${currentSubmission.message}` : '';
|
if (currentSubmission.state === 'Correct') {
|
||||||
vscode.window.showInformationMessage(`BWContest Judge: INCORRECT Submission '${problem.friendlyName}' ${messageDisplayText}`);
|
vscode.window.showInformationMessage(
|
||||||
}
|
`BWContest Judge: CORRECT Submission '${problem.friendlyName}'`
|
||||||
|
);
|
||||||
|
} else if (currentSubmission.state === 'Incorrect') {
|
||||||
|
const messageDisplayText = currentSubmission.message
|
||||||
|
? `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(
|
||||||
return;
|
' No locally cached submission list state, the normal polling cycle will update the list'
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const existingSubmissionListForProblem = latestContestTeamState.submissionsList.get(submission.problemId);
|
const existingSubmissionListForProblem = latestContestTeamState.submissionsList.get(
|
||||||
if (existingSubmissionListForProblem === undefined) {
|
submission.problemId
|
||||||
outputPanelLog.trace(` The cached submission list does not know about problemId #${submission.problemId}. Next polling cycle should fix consistency.`);
|
);
|
||||||
return;
|
if (existingSubmissionListForProblem === undefined) {
|
||||||
}
|
outputPanelLog.trace(
|
||||||
|
` The cached submission list does not know about problemId #${submission.problemId}. Next polling cycle should fix consistency.`
|
||||||
|
);
|
||||||
|
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(
|
||||||
return;
|
` The cached submission list already knows about submissionId #${submission.id}`
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
outputPanelLog.trace(` New submission #${submission.id} added to cache, triggering events`);
|
outputPanelLog.trace(` New submission #${submission.id} added to cache, triggering events`);
|
||||||
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])
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -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.`
|
||||||
|
);
|
||||||
}
|
}
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
@ -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({
|
||||||
|
@ -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';
|
||||||
@ -68,12 +74,12 @@ export const runCpp: IRunner<IRunnerParamsCpp> = async function (
|
|||||||
child.stdout.setEncoding('utf8');
|
child.stdout.setEncoding('utf8');
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
outputBuffer += data.toString();
|
outputBuffer += data.toString();
|
||||||
params.outputCallback?.(data.toString());
|
params.outputCallback?.(data.toString());
|
||||||
});
|
});
|
||||||
child.stderr.setEncoding('utf8');
|
child.stderr.setEncoding('utf8');
|
||||||
child.stderr.on('data', (data) => {
|
child.stderr.on('data', (data) => {
|
||||||
outputBuffer += data.toString();
|
outputBuffer += data.toString();
|
||||||
params.outputCallback?.(data.toString());
|
params.outputCallback?.(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
const runStartTime = performance.now();
|
const runStartTime = performance.now();
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
// 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;
|
||||||
remove(handler: { (data?: T): void }): void;
|
remove(handler: { (data?: T): void }): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,10 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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));
|
||||||
}
|
}
|
@ -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}
|
||||||
|
@ -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">
|
||||||
@ -127,7 +153,7 @@
|
|||||||
|
|
||||||
.problemHeaderSubmitCount {
|
.problemHeaderSubmitCount {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemHeader {
|
.problemHeader {
|
||||||
|
Loading…
Reference in New Issue
Block a user