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'