diff --git a/package.json b/package.json index c9f6d3e7..a19873d2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "async": "^2.6.1", "bfx-svc-test-helper": "git+https://github.com/bitfinexcom/bfx-svc-test-helper.git", "bittorrent-dht": "^8.4.0", + "cron-validate": "^1.4.2", "ed25519-supercop": "^2.0.1", + "electron-alert": "^0.1.11", "electron-serve": "^1.0.0", "find-free-port": "^2.0.0", "grenache-grape": "^0.9.8", diff --git a/scripts/build-ui.sh b/scripts/build-ui.sh index f5866948..d6f0fac1 100755 --- a/scripts/build-ui.sh +++ b/scripts/build-ui.sh @@ -2,6 +2,8 @@ set -x +export CI_ENVIRONMENT_NAME=production + ROOT="$PWD" frontendFolder="$ROOT/bfx-report-ui" pathToTriggerElectronLoad="$frontendFolder/src/utils/triggerElectronLoad.js" @@ -58,34 +60,43 @@ if ! [ -s "$frontendFolder/package.json" ]; then exit 1 fi -cp -f "$frontendFolder/.env.example" "$frontendFolder/.env" -sed -i -e \ - "s/REACT_APP_ELECTRON=.*/REACT_APP_ELECTRON=true/g" \ - $frontendFolder/.env - -npm i --no-audit - sed -i -e \ "s/API_URL: .*,/API_URL: \'http:\/\/localhost:34343\/api\',/g" \ - $frontendFolder/src/var/config.js + $frontendFolder/src/config.js sed -i -e \ "s/WS_ADDRESS: .*,/WS_ADDRESS: \'ws:\/\/localhost:34343\/ws\',/g" \ - $frontendFolder/src/var/config.js + $frontendFolder/src/config.js if [ $isDevEnv != 0 ]; then + export CI_ENVIRONMENT_NAME=development + sed -i -e \ - "s/KEY_URL: .*,/KEY_URL: \'https:\/\/test.bitfinex.com\/api\',/g" \ - $frontendFolder/src/var/config.js + "s/KEY_URL: .*,/KEY_URL: \'https:\/\/api.staging.bitfinex.com\/api\',/g" \ + $frontendFolder/src/config.js fi sed -i -e \ - "s/showAuthPage: .*,/showAuthPage: true,/g" \ - $frontendFolder/src/var/config.js + "s/localExport: false/localExport: true/g" \ + $frontendFolder/src/config.js +sed -i -e \ + "s/showAuthPage: false/showAuthPage: true/g" \ + $frontendFolder/src/config.js sed -i -e \ - "s/showFrameworkMode: .*,/showFrameworkMode: true,/g" \ - $frontendFolder/src/var/config.js + "s/isElectronApp: false/isElectronApp: true/g" \ + $frontendFolder/src/config.js +sed -i -e \ + "s/showFrameworkMode: false/showFrameworkMode: true/g" \ + $frontendFolder/src/config.js + +rm -f "$ROOT/.eslintrc" + +npm i --no-audit npm run build +if ! [ -s "$frontendFolder/build/index.html" ]; then + exit 1 +fi + mv -f $frontendFolder/build/* $uiBuildFolder cp $pathToTriggerElectronLoad $uiBuildFolder/triggerElectronLoad.js touch $uiReadyFile diff --git a/server.js b/server.js index 13d4cd66..466b7e1d 100644 --- a/server.js +++ b/server.js @@ -40,6 +40,7 @@ let isMigrationsError = false try { const pathToUserData = process.env.PATH_TO_USER_DATA const pathToUserCsv = process.env.PATH_TO_USER_CSV + const schedulerRule = process.env.SCHEDULER_RULE const secretKey = process.env.SECRET_KEY if (!secretKey) { @@ -120,7 +121,8 @@ let isMigrationsError = false `--logsFolder=${pathToUserData}/logs`, `--dbFolder=${pathToUserData}`, `--grape=${grape}`, - `--secretKey=${secretKey}` + `--secretKey=${secretKey}`, + `--schedulerRule=${schedulerRule || ''}` ], { env, cwd: process.cwd(), diff --git a/src/change-sync-frequency.js b/src/change-sync-frequency.js new file mode 100644 index 00000000..4d8071af --- /dev/null +++ b/src/change-sync-frequency.js @@ -0,0 +1,245 @@ +'use strict' + +const electron = require('electron') +const Alert = require('electron-alert') +const cronValidate = require('cron-validate') +const path = require('path') +const fs = require('fs') + +const modalDialogStyle = fs.readFileSync(path.join( + __dirname, 'modal-dialog-src/modal-dialog.css' +)) +const modalDialogScript = fs.readFileSync(path.join( + __dirname, 'modal-dialog-src/modal-dialog.js' +)) + +const { + SyncFrequencyChangingError +} = require('./errors') +const showErrorModalDialog = require('./show-error-modal-dialog') +const pauseApp = require('./pause-app') +const relaunch = require('./relaunch') +const { getConfigsKeeperByName } = require('./configs-keeper') + +const _getSchedulerRule = (timeFormat, alertRes) => { + if (timeFormat.value === 'days') { + return `0 0 */${alertRes.value} * *` + } + if (timeFormat.value === 'hours') { + return `0 */${alertRes.value} * * *` + } + + return `*/${alertRes.value} * * * *` +} + +const _testTime = (time) => { + return ( + time && + typeof time === 'string' && + /^\*\/\d{1,2}$/i.test(time) + ) +} + +const _getTime = (timeFormat, time) => { + return { + timeFormat, + value: time.replace('*/', '') + } +} + +const _getTimeDataFromRule = (rule) => { + const cronResult = cronValidate(rule) + + if (!cronResult.isValid()) { + return { timeFormat: 'hours', value: 2 } + } + + const value = cronResult.getValue() + + if (_testTime(value.daysOfMonth)) { + return _getTime('days', value.daysOfMonth) + } + if (_testTime(value.hours)) { + return _getTime('hours', value.hours) + } + if (_testTime(value.minutes)) { + return _getTime('mins', value.minutes) + } + + return { timeFormat: 'hours', value: 2 } +} + +const style = `` +const script = `` + +module.exports = () => { + const configsKeeper = getConfigsKeeperByName('main') + const timeFormatAlert = new Alert([style]) + const alert = new Alert([style, script]) + + const closeTimeFormatAlert = () => { + if (!timeFormatAlert.browserWindow) return + + timeFormatAlert.browserWindow.close() + } + const closeAlert = () => { + if (!alert.browserWindow) return + + alert.browserWindow.close() + } + + const timeFormatAlertOptions = { + title: 'Set time format', + type: 'question', + background: '#172d3e', + customClass: { + title: 'titleColor', + content: 'textColor', + input: 'textColor radioInput' + }, + focusConfirm: true, + showCancelButton: true, + progressSteps: [1, 2], + currentProgressStep: 0, + input: 'radio', + inputValue: 'hours', + inputOptions: { + mins: 'Mins', + hours: 'Hours', + days: 'Days' + }, + onBeforeOpen: () => { + if (!timeFormatAlert.browserWindow) return + + timeFormatAlert.browserWindow.once('blur', closeTimeFormatAlert) + } + } + const alertOptions = { + title: 'Set sync frequency', + type: 'question', + background: '#172d3e', + customClass: { + title: 'titleColor', + content: 'textColor', + input: 'textColor rangeInput' + }, + focusConfirm: true, + showCancelButton: true, + progressSteps: [1, 2], + currentProgressStep: 1, + input: 'range', + onBeforeOpen: () => { + if (!alert.browserWindow) return + + alert.browserWindow.once('blur', closeAlert) + } + } + const sound = { freq: 'F2', type: 'triange', duration: 1.5 } + + const getAlertOpts = (timeFormat, timeData) => { + const { inputOptions } = timeFormatAlertOptions + const text = inputOptions[timeFormat.value] + + if (timeFormat.value === 'days') { + return { + ...alertOptions, + text, + inputValue: timeFormat.value === timeData.timeFormat + ? timeData.value : 1, + inputAttributes: { + min: 1, + max: 31, + step: 1 + } + } + } + if (timeFormat.value === 'hours') { + return { + ...alertOptions, + text, + inputValue: timeFormat.value === timeData.timeFormat + ? timeData.value : 2, + inputAttributes: { + min: 1, + max: 23, + step: 1 + } + } + } + + return { + ...alertOptions, + text, + inputValue: timeFormat.value === timeData.timeFormat + ? timeData.value : 20, + inputAttributes: { + min: 10, + max: 59, + step: 1 + } + } + } + + return async () => { + const win = electron.BrowserWindow.getFocusedWindow() + win.once('closed', closeTimeFormatAlert) + win.once('closed', closeAlert) + + try { + const savedSchedulerRule = await configsKeeper + .getConfigByName('schedulerRule') + const timeData = _getTimeDataFromRule(savedSchedulerRule) + + const timeFormat = await timeFormatAlert.fireFrameless( + { + ...timeFormatAlertOptions, + inputValue: timeData.timeFormat + }, + null, true, false, sound + ) + win.removeListener('closed', closeTimeFormatAlert) + + if (timeFormat.dismiss) { + return + } + + const alertRes = await alert.fireFrameless( + getAlertOpts(timeFormat, timeData), + null, true, false, sound + ) + win.removeListener('closed', closeAlert) + + if (alertRes.dismiss) { + return + } + + const schedulerRule = _getSchedulerRule( + timeFormat, + alertRes + ) + + if (savedSchedulerRule === schedulerRule) { + return + } + + await pauseApp() + const isSaved = await configsKeeper + .saveConfigs({ schedulerRule }) + + if (!isSaved) { + throw new SyncFrequencyChangingError() + } + + relaunch() + } catch (err) { + try { + await showErrorModalDialog(win, 'Change sync frequency', err) + } catch (err) { + console.error(err) + } + + console.error(err) + relaunch() + } + } +} diff --git a/src/create-menu.js b/src/create-menu.js index e20b946d..45a34de6 100644 --- a/src/create-menu.js +++ b/src/create-menu.js @@ -9,6 +9,7 @@ const exportDB = require('./export-db') const importDB = require('./import-db') const removeDB = require('./remove-db') const changeReportsFolder = require('./change-reports-folder') +const changeSyncFrequency = require('./change-sync-frequency') const triggerElectronLoad = require('./trigger-electron-load') const showAboutModalDialog = require('./show-about-modal-dialog') @@ -81,6 +82,11 @@ module.exports = ({ label: 'Change reports folder', accelerator: 'CmdOrCtrl+F', click: changeReportsFolder({ pathToUserDocuments }) + }, + { + label: 'Change sync frequency', + accelerator: 'CmdOrCtrl+S', + click: changeSyncFrequency() } ] }, diff --git a/src/errors/index.js b/src/errors/index.js index 9bd89d2b..34109044 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -89,6 +89,12 @@ class ReportsFolderChangingError extends BaseError { } } +class SyncFrequencyChangingError extends BaseError { + constructor (message = 'ERR_SYNC_FREQUENCY_HAS_NOT_CHANGED') { + super(message) + } +} + module.exports = { BaseError, InvalidFilePathError, @@ -103,5 +109,6 @@ module.exports = { WrongPathToUserDataError, WrongPathToUserCsvError, WrongSecretKeyError, - ReportsFolderChangingError + ReportsFolderChangingError, + SyncFrequencyChangingError } diff --git a/src/initialize-app.js b/src/initialize-app.js index a8c1a36e..8a012e1a 100644 --- a/src/initialize-app.js +++ b/src/initialize-app.js @@ -39,6 +39,10 @@ const pathToLayoutAppInitErr = path const pathToLayoutExprPortReq = path .join(pathToLayouts, 'express-port-required.html') +const { rule: schedulerRule } = require( + '../bfx-reports-framework/config/schedule.json' +) + const _ipcMessToPromise = (ipc) => { return new Promise((resolve, reject) => { try { @@ -74,7 +78,8 @@ module.exports = () => { { pathToUserCsv: process.platform === 'darwin' ? pathToUserDocuments - : '../../..' + : '../../..', + schedulerRule } ) const secretKey = await makeOrReadSecretKey( diff --git a/src/modal-dialog-src/modal-dialog.css b/src/modal-dialog-src/modal-dialog.css new file mode 100644 index 00000000..54e53a92 --- /dev/null +++ b/src/modal-dialog-src/modal-dialog.css @@ -0,0 +1,72 @@ +.rangeInput { + display: flex; + align-items: center; +} + +.radioInput input[type=radio] { + -webkit-appearance: none; + margin: -5px 10px -4px 5px; + border: 2px solid #f5f8fa; + cursor: pointer; + width: 28px; + height: 28px; + border-radius: 14px; +} + +.radioInput input[type=radio]:focus { + outline: none; +} + +.radioInput input[type=radio]:checked { + background-color: #3085d6; +} + +.rangeInput output { + width: 20%; +} + +.rangeInput input[type=range] { + margin: 0; + background: #f5f8fa; + border: 0.2px solid #010101; + border-radius: 1.3px; + height: 6.4px; + width: 80%; + -webkit-appearance: none; +} + +.rangeInput input[type=range]:focus { + outline: none; +} + +.rangeInput input[type=range]::-webkit-slider-runnable-track { + background-color: transparent; + width: 100%; + height: 6.4px; + cursor: pointer; + -webkit-appearance: none; +} + +.rangeInput input[type=range]::-webkit-slider-thumb { + margin-top: -10.8px; + width: 28px; + height: 28px; + background: #3085d6; + border: none; + border-radius: 14px; + cursor: pointer; + -webkit-appearance: none; + box-shadow: -800px 0 0 -10.8px #3085d6; +} + +.rangeInput input[type=range]:focus::-webkit-slider-runnable-track { + background-image: linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,.2)); +} + +.textColor { + color: #f5f8fa; +} + +.titleColor { + color: #82baf6; +} diff --git a/src/modal-dialog-src/modal-dialog.js b/src/modal-dialog-src/modal-dialog.js new file mode 100644 index 00000000..a02c5d39 --- /dev/null +++ b/src/modal-dialog-src/modal-dialog.js @@ -0,0 +1,30 @@ +'use strict' + +window.addEventListener('load', () => { + const inputs = document.querySelectorAll('.rangeInput input') + + const resizeProgress = (e) => { + try { + const target = e && e.target ? e.target : e + const val = Number.parseFloat(target.value) + const min = Number.parseFloat(target.min) + const max = Number.parseFloat(target.max) + const per = (((val - min) * 100) / (max - min)) + + target.style.background = `linear-gradient( + to right, + #3085d6 0%, + #3085d6 ${per}%, + #f5f8fa ${per}%, + #f5f8fa 100% + )` + } catch (err) { + console.error(err) + } + } + + for (const input of inputs) { + input.addEventListener('input', resizeProgress) + resizeProgress(input) + } +}) diff --git a/src/run-server.js b/src/run-server.js index 4dd0075a..d5ef2abb 100644 --- a/src/run-server.js +++ b/src/run-server.js @@ -12,11 +12,14 @@ module.exports = ({ pathToUserData, secretKey }) => { + const mainConfsKeeper = getConfigsKeeperByName('main') const env = { ...process.env, PATH_TO_USER_DATA: pathToUserData, - PATH_TO_USER_CSV: getConfigsKeeperByName('main') + PATH_TO_USER_CSV: mainConfsKeeper .getConfigByName('pathToUserCsv'), + SCHEDULER_RULE: mainConfsKeeper + .getConfigByName('schedulerRule'), SECRET_KEY: secretKey } const ipc = fork(serverPath, [], { diff --git a/src/show-error-modal-dialog.js b/src/show-error-modal-dialog.js index 6046da06..e689e22e 100644 --- a/src/show-error-modal-dialog.js +++ b/src/show-error-modal-dialog.js @@ -6,7 +6,8 @@ const { DbImportingError, DbRemovingError, InvalidFolderPathError, - ReportsFolderChangingError + ReportsFolderChangingError, + SyncFrequencyChangingError } = require('./errors') const showMessageModalDialog = require('./show-message-modal-dialog') @@ -66,6 +67,11 @@ module.exports = async (win, title = 'Error', err) => { return _showErrorBox(win, title, message) } + if (err instanceof SyncFrequencyChangingError) { + const message = 'The sync frequency has not been changed' + + return _showErrorBox(win, title, message) + } const message = 'An unexpected exception occurred'