diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e5e6be76..920965c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,92 +3,13 @@ name: ci on: push: branches: - - '*' + - "*" pull_request: branches: - master jobs: - test: - runs-on: ubuntu-22.04 - services: - rabbitmq: - image: rabbitmq - ports: - - 5672:5672 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 50 - - - id: changed-files - name: Check changed files - run: python3 ci/check-changed-files.py | bash - - - name: Set up JDK - if: steps.changed-files.outputs.gradle == '1' - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '11' - cache: 'gradle' - - - name: Test judgels-commons - if: steps.changed-files.outputs.judgels-commons == '1' - run: ./judgels-backends/gradlew -p judgels-backends/judgels-commons check - - - name: Test judgels-grader-api - if: steps.changed-files.outputs.judgels-grader == '1' - run: ./judgels-backends/gradlew -p judgels-backends/judgels-grader-api check - - - name: Test judgels-grader-engines - if: steps.changed-files.outputs.judgels-grader == '1' - run: ./judgels-backends/gradlew -p judgels-backends/judgels-grader-engines check - - - name: Test judgels-grader-app - if: steps.changed-files.outputs.judgels-grader == '1' - run: ./judgels-backends/gradlew -p judgels-backends/judgels-grader-app check - - - name: Test judgels-server-api - if: steps.changed-files.outputs.judgels-server == '1' - run: ./judgels-backends/gradlew -p judgels-backends/judgels-server-api check - - - name: Test judgels-server-app - if: steps.changed-files.outputs.judgels-server == '1' - run: ./judgels-backends/gradlew -p judgels-backends/judgels-server-app check - - - name: Set up node - if: steps.changed-files.outputs.yarn == '1' - uses: actions/setup-node@v3 - with: - node-version: '16' - - - name: Get yarn cache directory path - if: steps.changed-files.outputs.yarn == '1' - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - - name: Use yarn cache - if: steps.changed-files.outputs.yarn == '1' - uses: actions/cache@v3 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Test judgels-client - if: steps.changed-files.outputs.judgels-client == '1' - run: | - cd judgels-client - export NODE_OPTIONS="--max-old-space-size=4096" - yarn install - yarn ci - build: - needs: test - if: github.ref == 'refs/heads/master' && github.repository_owner == 'ia-toki' runs-on: ubuntu-22.04 env: CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} @@ -111,15 +32,15 @@ jobs: if: steps.changed-files.outputs.gradle == '1' uses: actions/setup-java@v3 with: - distribution: 'zulu' - java-version: '11' - cache: 'gradle' + distribution: "zulu" + java-version: "11" + cache: "gradle" - name: Set up node if: steps.changed-files.outputs.yarn == '1' uses: actions/setup-node@v3 with: - node-version: '16' + node-version: "16" - name: Get yarn cache directory path if: steps.changed-files.outputs.yarn == '1' @@ -136,61 +57,12 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - - name: Build judgels-server - if: steps.changed-files.outputs.judgels-server == '1' - run: ./deployment/scripts/build_judgels_server.sh - - name: Build judgels-client if: steps.changed-files.outputs.judgels-client == '1' run: ./deployment/scripts/build_judgels_client.sh - - name: Build judgels-grader - if: steps.changed-files.outputs.judgels-grader == '1' - run: ./deployment/scripts/build_judgels_grader.sh - - deploy-web: - if: github.ref == 'refs/heads/master' && github.repository_owner == 'ia-toki' - runs-on: ubuntu-22.04 - env: - WEB_HOST: ${{ secrets.WEB_HOST }} - WEB_KNOWN_HOSTS: ${{ secrets.WEB_KNOWN_HOSTS }} - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 50 - - - id: changed-files - name: Check changed files - run: python3 ci/check-changed-files.py | bash - - - name: Set up node - if: steps.changed-files.outputs.web == '1' - uses: actions/setup-node@v3 - with: - node-version: '16' - - - name: Build web - if: steps.changed-files.outputs.web == '1' - run: | - cd web - yarn install - yarn build - - - name: Install SSH key - if: steps.changed-files.outputs.web == '1' - uses: shimataro/ssh-key-action@v2 - with: - key: ${{ secrets.WEB_PRIVATE_KEY }} - known_hosts: ${{ secrets.WEB_KNOWN_HOSTS }} - - - name: Rsync web - if: steps.changed-files.outputs.web == '1' - run: rsync -avzh web/build/ root@${WEB_HOST}:/var/www/html/judgels --delete - deploy-tlx-staging: needs: build - if: github.ref == 'refs/heads/master' && github.repository_owner == 'ia-toki' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -211,23 +83,9 @@ jobs: name: Check changed files run: python3 ci/check-changed-files.py | bash - - name: Deploy judgels-server - if: steps.changed-files.outputs.judgels-server == '1' - run: | - eval "$(ssh-agent -s)" - ssh-add deployment/ansible/env/deployment_rsa - ./deployment/scripts/deploy_judgels_server.sh - - name: Deploy judgels-client if: steps.changed-files.outputs.judgels-client == '1' run: | eval "$(ssh-agent -s)" ssh-add deployment/ansible/env/deployment_rsa ./deployment/scripts/deploy_judgels_client.sh - - - name: Deploy judgels-grader - if: steps.changed-files.outputs.judgels-grader == '1' - run: | - eval "$(ssh-agent -s)" - ssh-add deployment/ansible/env/deployment_rsa - ./deployment/scripts/deploy_judgels_grader.sh diff --git a/deployment/ansible/roles/judgels-client-build/tasks/main.yml b/deployment/ansible/roles/judgels-client-build/tasks/main.yml index 158c95849..e14f140f8 100644 --- a/deployment/ansible/roles/judgels-client-build/tasks/main.yml +++ b/deployment/ansible/roles/judgels-client-build/tasks/main.yml @@ -3,7 +3,7 @@ docker_image: source: build force_source: yes - name: ghcr.io/ia-toki/judgels/client + name: "ghcr.io/ia-toki/judgels/client:{{ app_version | default('latest', true) }}" push: yes build: path: "{{ playbook_dir }}/../../../judgels-client/dist" diff --git a/deployment/ansible/roles/judgels-server-build/tasks/main.yml b/deployment/ansible/roles/judgels-server-build/tasks/main.yml index 135683d6b..b2910a067 100644 --- a/deployment/ansible/roles/judgels-server-build/tasks/main.yml +++ b/deployment/ansible/roles/judgels-server-build/tasks/main.yml @@ -3,7 +3,7 @@ docker_image: source: build force_source: yes - name: ghcr.io/ia-toki/judgels/server + name: "ghcr.io/ia-toki/judgels/server:{{ app_version | default('latest', true) }}" push: yes build: path: "{{ playbook_dir }}/../../../judgels-backends/judgels-server-app" diff --git a/deployment/scripts/build_judgels_client.sh b/deployment/scripts/build_judgels_client.sh index 7bc8a931a..7a5d63f42 100755 --- a/deployment/scripts/build_judgels_client.sh +++ b/deployment/scripts/build_judgels_client.sh @@ -12,4 +12,4 @@ rm -rf dist/build && mv build dist/ cd ../deployment/ansible ansible --version 0 1; @@ -146,6 +148,13 @@ export function SubmissionDetails({ )); }; + const renderLoader = () => { + if (showLoaderWhenPending && latestGrading.verdict.code === VerdictCode.PND) { + return ; + } + return null; + }; + const renderSampleTestDataResults = () => { const details = latestGrading.details; if (details.testDataResults.length < 1) { @@ -302,9 +311,11 @@ export function SubmissionDetails({ const sourceFiles = Object.keys(submissionFiles).map(key => ( -
- {key === DEFAULT_SOURCE_KEY ? '' : key + ': '} {submissionFiles[key].name} -
+ {!hideSourceFilename && ( +
+ {key === DEFAULT_SOURCE_KEY ? '' : key + ': '} {submissionFiles[key].name} +
+ )} {decodeBase64(submissionFiles[key].content)} @@ -343,7 +354,7 @@ export function SubmissionDetails({ const renderSourceFilesHeading = () => { if (!onDownload) { - return

Source Files

; + return null; } return (
@@ -359,6 +370,7 @@ export function SubmissionDetails({ return (
{renderGeneralInfo()} + {renderLoader()} {renderDetails()} {renderSourceFiles()}
diff --git a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss index bdb446728..b5da5b6fb 100644 --- a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss +++ b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss @@ -58,6 +58,10 @@ float: left; } + .pending-loader { + margin-bottom: 15px; + } + .submission-details-image { overflow: auto; } diff --git a/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js b/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js index 56ab472bf..92a64205f 100644 --- a/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js +++ b/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js @@ -5,6 +5,7 @@ import storage from 'redux-persist/es/storage'; import courseReducer from '../../routes/courses/courses/modules/courseReducer'; import courseChapterReducer from '../../routes/courses/courses/single/chapters/modules/courseChapterReducer'; import courseChaptersReducer from '../../routes/courses/courses/single/chapters/modules/courseChaptersReducer'; +import chapterProblemReducer from '../../routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer'; import problemSetReducer from '../../routes/problems/problemsets/modules/problemSetReducer'; import problemSetProblemReducer from '../../routes/problems/problemsets/single/problems/modules/problemSetProblemReducer'; @@ -12,6 +13,7 @@ export default combineReducers({ course: persistReducer({ key: 'jerahmeelCourse', storage }, courseReducer), courseChapter: persistReducer({ key: 'jerahmeelCourseChapter', storage }, courseChapterReducer), courseChapters: courseChaptersReducer, + chapterProblem: chapterProblemReducer, problemSet: persistReducer({ key: 'jerahmeelProblemSet', storage }, problemSetReducer), problemSetProblem: persistReducer({ key: 'jerahmeelProblemSetProblem', storage }, problemSetProblemReducer), }); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx index 668ee23f4..245c3f4b4 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx @@ -14,6 +14,7 @@ import { ProblemType } from '../../../../../../../../../modules/api/sandalphon/p import { selectCourse } from '../../../../../../modules/courseSelectors'; import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; import { selectCourseChapters } from '../../../../modules/courseChaptersSelectors'; +import { selectChapterProblemKey } from '../modules/chapterProblemSelectors'; import { selectStatementLanguage } from '../../../../../../../../../modules/webPrefs/webPrefsSelectors'; import * as chapterProblemActions from '../../modules/chapterProblemActions'; import * as breadcrumbsActions from '../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; @@ -25,33 +26,17 @@ export class ChapterProblemPage extends Component { response: undefined, }; - async componentDidMount() { - const response = await this.props.onGetProblemWorksheet( - this.props.chapter.jid, - this.props.match.params.problemAlias, - this.props.statementLanguage - ); - - this.setState({ - response, - }); - - this.props.onPushBreadcrumb(this.props.match.url, response.problem.alias); - - sendGAEvent({ category: 'Courses', action: 'View course problem', label: this.props.course.name }); - sendGAEvent({ category: 'Courses', action: 'View chapter problem', label: this.props.chapter.name }); - sendGAEvent({ - category: 'Courses', - action: 'View problem', - label: this.props.chapterName + ': ' + this.props.match.params.problemAlias, - }); + componentDidMount() { + this.refreshProblem(); } async componentDidUpdate(prevProps) { - if (this.props.statementLanguage !== prevProps.statementLanguage) { - await this.componentDidMount(); - } else if (this.props.match.params.problemAlias !== prevProps.match.params.problemAlias) { - await this.componentDidMount(); + if ( + this.props.statementLanguage !== prevProps.statementLanguage || + this.props.chapterProblemKey !== prevProps.chapterProblemKey || + this.props.match.params.problemAlias !== prevProps.match.params.problemAlias + ) { + await this.refreshProblem(); } } @@ -69,6 +54,28 @@ export class ChapterProblemPage extends Component { ); } + refreshProblem = async () => { + const response = await this.props.onGetProblemWorksheet( + this.props.chapter.jid, + this.props.match.params.problemAlias, + this.props.statementLanguage + ); + + this.setState({ + response, + }); + + this.props.onPushBreadcrumb(this.props.match.url, response.problem.alias); + + sendGAEvent({ category: 'Courses', action: 'View course problem', label: this.props.course.name }); + sendGAEvent({ category: 'Courses', action: 'View chapter problem', label: this.props.chapter.name }); + sendGAEvent({ + category: 'Courses', + action: 'View problem', + label: this.props.chapterName + ': ' + this.props.match.params.problemAlias, + }); + }; + renderHeader = () => { const { course, chapter, match } = this.props; @@ -148,6 +155,7 @@ const mapStateToProps = state => ({ course: selectCourse(state), chapter: selectCourseChapter(state), chapters: selectCourseChapters(state), + chapterProblemKey: selectChapterProblemKey(state), statementLanguage: selectStatementLanguage(state), }); const mapDispatchToProps = { diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js index 9a281d540..c0bfde420 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js @@ -34,12 +34,20 @@ export function createSubmission(courseSlug, chapterJid, chapterAlias, problemJi sourceFiles['sourceFiles.' + key] = data.sourceFiles[key]; }); - await submissionProgrammingAPI.createSubmission(token, chapterJid, problemJid, data.gradingLanguage, sourceFiles); + const submission = await submissionProgrammingAPI.createSubmission( + token, + chapterJid, + problemJid, + data.gradingLanguage, + sourceFiles + ); toastActions.showSuccessToast('Solution submitted.'); window.scrollTo(0, 0); - dispatch(push(`/courses/${courseSlug}/chapters/${chapterAlias}/problems/${problemAlias}/submissions`)); + dispatch( + push(`/courses/${courseSlug}/chapters/${chapterAlias}/problems/${problemAlias}/submissions/${submission.id}`) + ); }; } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx index 4a86382ec..daf9e60f7 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx @@ -10,6 +10,8 @@ import { SubmissionDetails } from '../../../../../../../../../../../../component import { selectStatementLanguage } from '../../../../../../../../../../../../modules/webPrefs/webPrefsSelectors'; import { selectCourse } from '../../../../../../../../../modules/courseSelectors'; import { selectCourseChapter } from '../../../../../../../modules/courseChapterSelectors'; +import { RefreshChapterProblem } from '../../../../modules/chapterProblemReducer'; +import { VerdictCode } from '../../../../../../../../../../../../modules/api/gabriel/verdict'; import * as breadcrumbsActions from '../../../../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; import * as chapterProblemSubmissionActions from '../../modules/chapterProblemSubmissionActions'; @@ -22,20 +24,10 @@ export class ChapterProblemSubmissionPage extends Component { containerName: undefined, }; - async componentDidMount() { - const { data, profile, problemName, containerName } = await this.props.onGetSubmissionWithSource( - +this.props.match.params.submissionId, - this.props.statementLanguage - ); - const sourceImageUrl = data.source ? undefined : await this.props.onGetSubmissionSourceImage(data.submission.jid); - this.props.onPushBreadcrumb(this.props.match.url, '#' + data.submission.id); - this.setState({ - submissionWithSource: data, - sourceImageUrl, - profile, - problemName, - containerName, - }); + currentTimeout; + + componentDidMount() { + this.refreshSubmission(); } async componentWillUnmount() { @@ -63,6 +55,36 @@ export class ChapterProblemSubmissionPage extends Component { ); } + refreshSubmission = async () => { + const { data, profile, problemName, containerName } = await this.props.onGetSubmissionWithSource( + +this.props.match.params.submissionId, + this.props.statementLanguage + ); + const sourceImageUrl = data.source ? undefined : await this.props.onGetSubmissionSourceImage(data.submission.jid); + this.props.onPushBreadcrumb(this.props.match.url, '#' + data.submission.id); + this.setState({ + submissionWithSource: data, + sourceImageUrl, + profile, + problemName, + containerName, + }); + + if (sourceImageUrl) { + return; + } + + const verdictCode = data.submission.latestGrading && data.submission.latestGrading.verdict.code; + if (verdictCode === VerdictCode.PND) { + this.currentTimeout = setTimeout(this.refreshSubmission, 2000); + } else if (verdictCode === VerdictCode.AC) { + if (this.currentTimeout) { + clearTimeout(this.currentTimeout); + this.props.onRefreshChapterProblem(new Date()); + } + } + }; + renderSubmission = () => { const { submissionWithSource, profile, sourceImageUrl } = this.state; const { course, chapter } = this.props; @@ -79,6 +101,8 @@ export class ChapterProblemSubmissionPage extends Component { sourceImageUrl={sourceImageUrl} profile={profile} problemUrl={`/courses/${course.slug}/chapters/${chapter.alias}/problems/${problemAlias}`} + hideSourceFilename + showLoaderWhenPending /> ); }; @@ -91,6 +115,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = { + onRefreshChapterProblem: RefreshChapterProblem, onGetSubmissionWithSource: chapterProblemSubmissionActions.getSubmissionWithSource, onGetSubmissionSourceImage: chapterProblemSubmissionActions.getSubmissionSourceImage, onPushBreadcrumb: breadcrumbsActions.pushBreadcrumb, diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer.js new file mode 100644 index 000000000..5f4ae30c2 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer.js @@ -0,0 +1,19 @@ +export const initialState = { + value: undefined, +}; + +export function RefreshChapterProblem(key) { + return { + type: 'jerahmeel/chapterProblem/PUT', + payload: key, + }; +} + +export default function chapterProblemReducer(state = initialState, action) { + switch (action.type) { + case 'jerahmeel/chapterProblem/PUT': + return { ...state, value: action.payload }; + default: + return state; + } +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemSelectors.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemSelectors.js new file mode 100644 index 000000000..2c1a6ee7f --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemSelectors.js @@ -0,0 +1,3 @@ +export function selectChapterProblemKey(state) { + return state.jerahmeel.chapterProblem.value; +}