diff --git a/.jshintrc b/.jshintrc index ce7eed7d7..897804370 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,11 +1,9 @@ { "browser": true, - "jquery": true, - "mootools": true, - "node": true, "undef": true, "sub": true, "newcap": false, + "esversion": 9, "indent": 4, "globals": { "alert": true, diff --git a/.travis.yml b/.travis.yml index 9905eb699..3bdb8f39d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,5 @@ jobs: - ./gradlew assemble - ./gradlew check - ./gradlew buildOfficial - - - language: node_js - node_js: 10 - script: - - cd tnoodle-ui - - npm install - - npm test -- --coverage after_script: - - COVERALLS_REPO_TOKEN=$coveralls_repo_token npm run coveralls + - COVERALLS_REPO_TOKEN=$coveralls_repo_token ./gradlew yarn_coveralls diff --git a/tnoodle-ui/build.gradle.kts b/tnoodle-ui/build.gradle.kts index e86d1a1ee..c1794f246 100644 --- a/tnoodle-ui/build.gradle.kts +++ b/tnoodle-ui/build.gradle.kts @@ -39,7 +39,7 @@ tasks.getByName("check") { } tasks.getByName("yarn_test") { - dependsOn("yarn_build") + dependsOn("yarn_install") } tasks.create("packageReactFrontend") { diff --git a/tnoodle-ui/package.json b/tnoodle-ui/package.json index 51047ba4c..4a6b957e7 100644 --- a/tnoodle-ui/package.json +++ b/tnoodle-ui/package.json @@ -3,7 +3,6 @@ "version": "0.1.0", "private": true, "homepage": "http://localhost:2014/scramble", - "proxy": "http://localhost:2014", "dependencies": { "bootstrap": "^4.4.1", "fetch-intercept": "^2.3.1", @@ -23,9 +22,9 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test --watchAll --watchAll=false", + "test": "react-scripts test --watchAll --watchAll=false --coverage", "eject": "react-scripts eject", - "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls" + "coveralls": "cat ./coverage/lcov.info | coveralls" }, "browserslist": { "production": [ @@ -43,13 +42,15 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", "@testing-library/user-event": "^12.1.4", - "coveralls": "^3.1.0" + "coveralls": "^3.1.0", + "http-proxy-middleware": "^1.0.5" }, "jest": { "collectCoverageFrom": [ "src/**/*.{js,jsx}", "!src/index.js", "!src/serviceWorker.js", + "!src/setupProxy.js", "!src/main/api/**.js", "!**/Interceptor.jsx" ] diff --git a/tnoodle-ui/src/main/api/tnoodle.api.js b/tnoodle-ui/src/main/api/tnoodle.api.js index 87cad7c4a..79d90918a 100644 --- a/tnoodle-ui/src/main/api/tnoodle.api.js +++ b/tnoodle-ui/src/main/api/tnoodle.api.js @@ -1,5 +1,5 @@ -let baseUrl = window.location.origin; -// let baseUrl = "http://localhost:2014"; +let backendUrl = new URL(window.location.origin); +export const tNoodleBackend = backendUrl.toString().replace(/\/$/g, ''); let zipEndpoint = "/wcif/zip"; let versionEndpoint = "/version"; @@ -9,7 +9,7 @@ let bestMbldAttemptEndpoint = "/frontend/mbld/best"; let wcaEventsEndpoint = "/frontend/data/events"; let formatsEndpoint = "/frontend/data/formats"; -export const fetchZip = (wcif, mbld, password, translations) => { +export const fetchZip = (scrambleClient, wcif, mbld, password, translations) => { let payload = { wcif, multiCubes: { requestedScrambles: mbld }, @@ -20,19 +20,26 @@ export const fetchZip = (wcif, mbld, password, translations) => { payload.zipPassword = password; } - return postToTnoodle(zipEndpoint, payload) - .then((response) => response.blob()) + return scrambleClient.loadScrambles(zipEndpoint, payload, wcif.id) + .then((result) => convertToBlob(result)) .catch((error) => console.error(error)); }; +const convertToBlob = async (result) => { + let {contentType, payload} = result; + let res = await fetch(`data:${contentType};base64,${payload}`); + + return await res.blob(); +}; + export const fetchWcaEvents = () => { - return fetch(baseUrl + wcaEventsEndpoint) + return fetch(tNoodleBackend + wcaEventsEndpoint) .then((response) => response.json()) .catch((error) => console.error(error)); }; export const fetchFormats = () => { - return fetch(baseUrl + formatsEndpoint) + return fetch(tNoodleBackend + formatsEndpoint) .then((response) => response.json()) .catch((error) => console.error(error)); }; @@ -50,13 +57,13 @@ export const fetchBestMbldAttempt = (wcif) => { }; export const fetchRunningVersion = () => { - return fetch(baseUrl + versionEndpoint) + return fetch(tNoodleBackend + versionEndpoint) .then((response) => response.json()) .catch((error) => console.error(error)); }; export const fetchAvailableFmcTranslations = () => { - return fetch(baseUrl + fmcTranslationsEndpoint) + return fetch(tNoodleBackend + fmcTranslationsEndpoint) .then((response) => response.json()) .catch((error) => console.error(error)); }; @@ -77,7 +84,7 @@ const fmcTranslationsHelper = (translations) => { }; const postToTnoodle = (endpoint, payload) => - fetch(baseUrl + endpoint, { + fetch(tNoodleBackend + endpoint, { method: "POST", headers: { Accept: "application/json", diff --git a/tnoodle-ui/src/main/api/tnoodle.socket.js b/tnoodle-ui/src/main/api/tnoodle.socket.js new file mode 100644 index 000000000..303eb4528 --- /dev/null +++ b/tnoodle-ui/src/main/api/tnoodle.socket.js @@ -0,0 +1,80 @@ +import { tNoodleBackend } from "./tnoodle.api"; + +export class ScrambleClient { + constructor(onHandshake, onProgress) { + this.onHandshake = onHandshake; + this.onProgress = onProgress; + + this.state = SCRAMBLING_STATES.IDLE; + + this.contentType = null; + this.resultPayload = null; + } + + loadScrambles(endpoint, payload, targetMarker) { + return new Promise((resolve, reject) => { + let ws = new WebSocket(BASE_URL + endpoint); + + ws.onopen = () => { + this.state = SCRAMBLING_STATES.INITIATE; + ws.send(JSON.stringify(payload)); + }; + + ws.onerror = (err) => { + reject(err); + }; + + ws.onclose = (cls) => { + if (this.state === SCRAMBLING_STATES.DONE && cls.wasClean) { + let resultObject = { + contentType: this.contentType, + payload: this.resultPayload + }; + + resolve(resultObject); + } else { + reject(cls); + } + }; + + ws.onmessage = (msg) => { + if (this.state === SCRAMBLING_STATES.INITIATE) { + this.state = SCRAMBLING_STATES.SCRAMBLING; + + let rawPayload = msg.data.toString(); + let targetPayload = JSON.parse(rawPayload); + + this.onHandshake(targetPayload); + } else if (this.state === SCRAMBLING_STATES.SCRAMBLING) { + if (msg.data === targetMarker) { + this.state = SCRAMBLING_STATES.COMPUTED_TYPE; + } else { + this.onProgress(msg.data); + } + } else if (this.state === SCRAMBLING_STATES.COMPUTED_TYPE) { + this.state = SCRAMBLING_STATES.COMPUTED_DATA; + + this.contentType = msg.data; + } else if (this.state === SCRAMBLING_STATES.COMPUTED_DATA) { + this.state = SCRAMBLING_STATES.DONE; + + this.resultPayload = msg.data; + } + }; + }); + } +} + +let wsTNoodleBackend = new URL(tNoodleBackend); +wsTNoodleBackend.protocol = "ws:"; + +const BASE_URL = wsTNoodleBackend.toString().replace(/\/$/g, ''); + +const SCRAMBLING_STATES = { + IDLE: "IDLE", + INITIATE: "INITIATE", + SCRAMBLING: "SCRAMBLING", + COMPUTED_TYPE: "COMPUTED_TYPE", + COMPUTED_DATA: "COMPUTED_DATA", + DONE: "DONE" +}; diff --git a/tnoodle-ui/src/main/components/EventPicker.jsx b/tnoodle-ui/src/main/components/EventPicker.jsx index ca53a08a3..e377c6f99 100644 --- a/tnoodle-ui/src/main/components/EventPicker.jsx +++ b/tnoodle-ui/src/main/components/EventPicker.jsx @@ -9,11 +9,16 @@ import { import MbldDetail from "./MbldDetail"; import FmcTranslationsDetail from "./FmcTranslationsDetail"; import "./EventPicker.css"; +import { ProgressBar } from "react-bootstrap"; const mapStateToProps = (store) => ({ editingDisabled: store.editingDisabled, wcif: store.wcif, wcaFormats: store.wcaFormats, + generatingScrambles: store.generatingScrambles, + scramblingProgressTarget: store.scramblingProgressTarget, + scramblingProgressCurrent: store.scramblingProgressCurrent, + fileZipBlob: store.fileZipBlob }); const mapDispatchToProps = { @@ -185,10 +190,38 @@ const EventPicker = connect( ); }; - render() { - let wcaEvent = this.props.wcif.events.find( - (event) => event.id === this.props.event.id + maybeShowProgressBar = (rounds) => { + let eventId = this.props.event.id; + + let current = this.props.scramblingProgressCurrent[eventId] || 0; + let target = this.props.scramblingProgressTarget[eventId]; + + if (rounds.length === 0 || !this.props.generatingScrambles || target === undefined) { + return; + } + + let progress = (current / target) * 100 + let miniThreshold = 2; + + if (progress === 0) { + progress = miniThreshold; + } + + return ( + ); + }; + + render() { + let wcaEvent = this.props.wcifEvent; let rounds = wcaEvent != null ? wcaEvent.rounds : []; return ( @@ -219,6 +252,7 @@ const EventPicker = connect(
{this.props.event.name}
+ {this.maybeShowProgressBar(rounds)} diff --git a/tnoodle-ui/src/main/components/Interceptor.jsx b/tnoodle-ui/src/main/components/Interceptor.jsx index 2504696c5..b4ec8ed0f 100644 --- a/tnoodle-ui/src/main/components/Interceptor.jsx +++ b/tnoodle-ui/src/main/components/Interceptor.jsx @@ -8,24 +8,23 @@ class Interceptor extends Component { constructor() { super(); - const that = this; - // http interceptor fetchIntercept.register({ - request: function (...request) { + request: (...request) => { // TODO set loading return request; }, - response: function (response) { + response: (response) => { if (!response.ok) { - that.handleHttpError(response); + this.handleHttpError(response); } + return response; }, - responseError: function (error) { - that.handleHttpError(error); + responseError: (error) => { + this.handleHttpError(error); return Promise.reject(error); }, }); diff --git a/tnoodle-ui/src/main/components/Main.jsx b/tnoodle-ui/src/main/components/Main.jsx index 257162750..c01b0e772 100644 --- a/tnoodle-ui/src/main/components/Main.jsx +++ b/tnoodle-ui/src/main/components/Main.jsx @@ -4,7 +4,14 @@ import EventPickerTable from "./EventPickerTable"; import Interceptor from "./Interceptor"; import VersionInfo from "./VersionInfo"; import { fetchZip } from "../api/tnoodle.api"; -import { updateFileZipBlob } from "../redux/ActionCreators"; +import { ScrambleClient } from "../api/tnoodle.socket"; +import { + updateFileZipBlob, + updateGeneratingScrambles, + updateScramblingProgressTarget, + updateScramblingProgressCurrentEvent, + resetScramblingProgressCurrent +} from "../redux/ActionCreators"; import { connect } from "react-redux"; import { isUsingStaging } from "../api/wca.api"; import "./Main.css"; @@ -17,10 +24,15 @@ const mapStateToProps = (store) => ({ officialZip: store.officialZip, fileZipBlob: store.fileZipBlob, translations: store.translations, + generatingScrambles: store.generatingScrambles }); const mapDispatchToProps = { updateFileZipBlob, + updateGeneratingScrambles, + updateScramblingProgressTarget, + updateScramblingProgressCurrentEvent, + resetScramblingProgressCurrent }; const Main = connect( @@ -32,14 +44,13 @@ const Main = connect( super(props); this.state = { - generatingScrambles: false, competitionNameFileZip: "", }; } onSubmit = (evt) => { evt.preventDefault(); - if (this.state.generatingScrambles) { + if (this.props.generatingScrambles) { return; } @@ -50,26 +61,25 @@ const Main = connect( } }; - setGeneratingScrambles = (flag) => { - this.setState({ ...this.state, generatingScrambles: flag }); - }; - generateZip = () => { // If user navigates during generation proccess, we still get the correct name this.setState({ ...this.state, competitionNameFileZip: this.props.wcif.name, - generatingScrambles: true, }); + let scrambleClient = new ScrambleClient(this.props.updateScramblingProgressTarget, this.props.updateScramblingProgressCurrentEvent); fetchZip( + scrambleClient, this.props.wcif, this.props.mbld, this.props.password, this.props.translations ).then((blob) => { this.props.updateFileZipBlob(blob); - this.setGeneratingScrambles(false); + this.props.updateGeneratingScrambles(false); + this.props.resetScramblingProgressCurrent(); }); + this.props.updateGeneratingScrambles(true); }; downloadZip = () => { @@ -100,7 +110,7 @@ const Main = connect( }; scrambleButton = () => { - if (this.state.generatingScrambles) { + if (this.props.generatingScrambles) { return (