Skip to content

Commit

Permalink
Merge pull request #1359 from davemfish/task/persistent-data-schema
Browse files Browse the repository at this point in the history
Move Workbench settings to a new persistent data store
  • Loading branch information
emlys authored Aug 4, 2023
2 parents b9e15a9 + 62a03b3 commit 7843942
Show file tree
Hide file tree
Showing 18 changed files with 1,564 additions and 1,907 deletions.
23 changes: 19 additions & 4 deletions workbench/__mocks__/electron-store.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
export default class Store {
constructor() {
this.store = {};
constructor(options) {
this.defaults = options.defaults || {};
this.store = this.defaults;
}

get(key) {
return this.store[key];
}

set(key, val) {
this.store[key] = val;
}

delete(key) {
delete this.store[key];
}

reset() {
this.store = this.defaults;
}
get() {}
set() {}
}
1 change: 1 addition & 0 deletions workbench/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@vitejs/plugin-react": "^4.0.0",
"ajv": "^8.12.0",
"babel-eslint": "^10.1.0",
"bootstrap": "4.3.1",
"concurrently": "^8.2.0",
Expand Down
6 changes: 4 additions & 2 deletions workbench/src/main/ipcMainChannels.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export const ipcMainChannels = {
CHANGE_LANGUAGE: 'change-language',
CHECK_FILE_PERMISSIONS: 'check-file-permissions',
CHECK_STORAGE_TOKEN: 'check-storage-token',
DOWNLOAD_URL: 'download-url',
GET_N_CPUS: 'get-n-cpus',
GET_ELECTRON_PATHS: 'get-electron-paths',
GET_N_CPUS: 'get-n-cpus',
GET_SETTING: 'get-setting',
GET_LANGUAGE: 'get-language',
INVEST_KILL: 'invest-kill',
INVEST_READ_LOG: 'invest-read-log',
Expand All @@ -13,8 +15,8 @@ export const ipcMainChannels = {
LOGGER: 'logger',
OPEN_EXTERNAL_URL: 'open-external-url',
OPEN_LOCAL_HTML: 'open-local-html',
SET_SETTING: 'set-setting',
SHOW_ITEM_IN_FOLDER: 'show-item-in-folder',
SHOW_OPEN_DIALOG: 'show-open-dialog',
SHOW_SAVE_DIALOG: 'show-save-dialog',
CHANGE_LANGUAGE: 'change-language',
};
13 changes: 4 additions & 9 deletions workbench/src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
ipcMain
} from 'electron';

import Store from 'electron-store';

import {
createPythonFlaskProcess,
getFlaskIsReady,
Expand All @@ -30,16 +28,16 @@ import {
import setupGetNCPUs from './setupGetNCPUs';
import setupOpenExternalUrl from './setupOpenExternalUrl';
import setupOpenLocalHtml from './setupOpenLocalHtml';
import setupChangeLanguage from './setupChangeLanguage';
import { settingsStore, setupSettingsHandlers } from './settingsStore';
import setupGetElectronPaths from './setupGetElectronPaths';
import setupRendererLogger from './setupRendererLogger';
import { ipcMainChannels } from './ipcMainChannels';
import menuTemplate from './menubar';
import ELECTRON_DEV_MODE from './isDevMode';
import BASE_URL from './baseUrl';
import { getLogger } from './logger';
import pkg from '../../package.json';
import i18n from './i18n/i18n';
import pkg from '../../package.json';

const logger = getLogger(__filename.split('/').slice(-1)[0]);

Expand Down Expand Up @@ -72,10 +70,7 @@ export const createWindow = async () => {
logger.info(`Running invest-workbench version ${pkg.version}`);
nativeTheme.themeSource = 'light'; // override OS/browser setting

// read language setting from storage and switch to that language
// default to en if no language setting exists
const store = new Store();
i18n.changeLanguage(store.get('language', 'en'));
i18n.changeLanguage(settingsStore.get('language'));

splashScreen = new BrowserWindow({
width: 574, // dims set to match the image in splash.html
Expand All @@ -92,7 +87,7 @@ export const createWindow = async () => {
setupCheckFilePermissions();
setupCheckFirstRun();
setupCheckStorageToken();
setupChangeLanguage();
setupSettingsHandlers();
setupGetElectronPaths();
setupGetNCPUs();
setupInvestLogReaderHandler();
Expand Down
101 changes: 101 additions & 0 deletions workbench/src/main/settingsStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { app, ipcMain } from 'electron';
import Store from 'electron-store';
import Ajv from 'ajv';

import { ipcMainChannels } from './ipcMainChannels';
import { getLogger } from './logger';

const logger = getLogger(__filename.split('/').slice(-1)[0]);

export const defaults = {
nWorkers: -1,
taskgraphLoggingLevel: 'INFO',
loggingLevel: 'INFO',
language: 'en',
};

export const schema = {
type: 'object',
properties: {
nWorkers: {
type: 'number',
},
taskgraphLoggingLevel: {
enum: ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
},
loggingLevel: {
enum: ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
},
language: {
enum: ['en', 'es', 'zh'],
},
},
required: ['nWorkers', 'taskgraphLoggingLevel', 'loggingLevel', 'language']
};

/**
* Open a store and validate against a schema.
*
* Required properties missing from the store are initialized with defaults.
* Invalid properties are reset to defaults.
*
* @param {object} data key-values with which to initialize a store.
* @returns {Store} an instance of an electron-store Store
*/
export function initStore(data = defaults) {
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
const store = new Store({ defaults: data });
const valid = validate(store.store);
if (!valid) {
validate.errors.forEach((e) => {
logger.debug(e);
let property;
if (e.instancePath) {
property = e.instancePath.split('/').pop();
} else if (e.keyword === 'required') {
property = e.params.missingProperty;
} else {
// something is invalid that we're not prepared to fix
// so just reset the whole store to defaults.
logger.debug(e);
store.reset();
}
logger.debug(`resetting value for setting ${property}`);
store.set(property, defaults[property]);
});
}
return store;
}

export const settingsStore = initStore();

export function setupSettingsHandlers() {
ipcMain.handle(
ipcMainChannels.GET_SETTING,
(event, key) => settingsStore.get(key)
);

ipcMain.on(
ipcMainChannels.SET_SETTING,
(event, key, value) => settingsStore.set(key, value)
);

// language is stored in the same store, but has special
// needs for getting & setting because we need to get
// the value synchronously during preload, and trigger
// an app restart on change.
ipcMain.on(ipcMainChannels.GET_LANGUAGE, (event) => {
event.returnValue = settingsStore.get('language');
});

ipcMain.handle(
ipcMainChannels.CHANGE_LANGUAGE,
(e, languageCode) => {
logger.debug('changing language to', languageCode);
settingsStore.set('language', languageCode);
app.relaunch();
app.quit();
}
);
}
12 changes: 10 additions & 2 deletions workbench/src/main/setupInvestHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ELECTRON_DEV_MODE from './isDevMode';
import investUsageLogger from './investUsageLogger';
import markupMessage from './investLogMarkup';
import writeInvestParameters from './writeInvestParameters';
import { settingsStore } from './settingsStore';

const logger = getLogger(__filename.split('/').slice(-1)[0]);

Expand Down Expand Up @@ -45,12 +46,16 @@ export function setupInvestRunHandlers(investExe) {
});

ipcMain.on(ipcMainChannels.INVEST_RUN, async (
event, modelRunName, pyModuleName, args, loggingLevel, taskgraphLoggingLevel, language, tabID
event, modelRunName, pyModuleName, args, tabID
) => {
let investRun;
let investStarted = false;
let investStdErr = '';
const usageLogger = investUsageLogger();
const loggingLevel = settingsStore.get('loggingLevel');
const taskgraphLoggingLevel = settingsStore.get('taskgraphLoggingLevel');
const language = settingsStore.get('language');
const nWorkers = settingsStore.get('nWorkers');

// Write a temporary datastack json for passing to invest CLI
try {
Expand All @@ -64,7 +69,10 @@ export function setupInvestRunHandlers(investExe) {
filepath: datastackPath,
moduleName: pyModuleName,
relativePaths: false,
args: JSON.stringify(args),
args: JSON.stringify({
...args,
n_workers: nWorkers,
}),
};
await writeInvestParameters(payload);

Expand Down
50 changes: 7 additions & 43 deletions workbench/src/renderer/app.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import i18n from 'i18next';

import TabPane from 'react-bootstrap/TabPane';
import TabContent from 'react-bootstrap/TabContent';
Expand All @@ -19,14 +20,9 @@ import InvestTab from './components/InvestTab';
import SettingsModal from './components/SettingsModal';
import DataDownloadModal from './components/DataDownloadModal';
import DownloadProgressBar from './components/DownloadProgressBar';
import {
saveSettingsStore, getAllSettings,
} from './components/SettingsModal/SettingsStorage';
import { getInvestModelNames } from './server_requests';
import InvestJob from './InvestJob';
import { dragOverHandlerNone } from './utils';
import { ipcMainChannels } from '../main/ipcMainChannels';
import i18n from 'i18next';

const { ipcRenderer } = window.Workbench.electron;

Expand All @@ -43,30 +39,25 @@ export default class App extends React.Component {
openJobs: {},
investList: null,
recentJobs: [],
investSettings: null,
showDownloadModal: false,
downloadedNofN: null,
};
this.saveSettings = this.saveSettings.bind(this);
this.switchTabs = this.switchTabs.bind(this);
this.openInvestModel = this.openInvestModel.bind(this);
this.closeInvestModel = this.closeInvestModel.bind(this);
this.updateJobProperties = this.updateJobProperties.bind(this);
this.saveJob = this.saveJob.bind(this);
this.clearRecentJobs = this.clearRecentJobs.bind(this);
this.storeDownloadDir = this.storeDownloadDir.bind(this);
this.showDownloadModal = this.showDownloadModal.bind(this);
}

/** Initialize the list of invest models, recent invest jobs, etc. */
async componentDidMount() {
const investList = await getInvestModelNames();
const recentJobs = await InvestJob.getJobStore();
const investSettings = await getAllSettings();
this.setState({
investList: investList,
recentJobs: recentJobs,
investSettings: investSettings,
showDownloadModal: this.props.isFirstRun,
});
await i18n.changeLanguage(window.Workbench.LANGUAGE);
Expand All @@ -91,21 +82,6 @@ export default class App extends React.Component {
);
}

async saveSettings(settings) {
await saveSettingsStore(settings);
this.setState({ investSettings: settings });
}

/** Store a sampledata filepath in localforage.
*
* @param {string} dir - the path to the user-selected dir
*/
storeDownloadDir(dir) {
const { investSettings } = this.state;
investSettings.sampleDataDir = dir;
this.saveSettings(investSettings);
}

showDownloadModal(shouldShow) {
this.setState({
showDownloadModal: shouldShow,
Expand Down Expand Up @@ -196,7 +172,6 @@ export default class App extends React.Component {
render() {
const {
investList,
investSettings,
recentJobs,
openJobs,
openTabIDs,
Expand Down Expand Up @@ -273,7 +248,6 @@ export default class App extends React.Component {
<InvestTab
job={job}
tabID={id}
investSettings={investSettings}
saveJob={this.saveJob}
updateJobProperties={this.updateJobProperties}
/>
Expand All @@ -286,7 +260,6 @@ export default class App extends React.Component {
<DataDownloadModal
show={showDownloadModal}
closeModal={() => this.showDownloadModal(false)}
storeDownloadDir={this.storeDownloadDir}
/>
<TabContainer activeKey={activeTab}>
<Navbar
Expand Down Expand Up @@ -328,21 +301,12 @@ export default class App extends React.Component {
)
: <div />
}
{
// don't render until after we fetched the data
(investSettings)
? (
<SettingsModal
className="mx-3"
saveSettings={this.saveSettings}
investSettings={investSettings}
clearJobsStorage={this.clearRecentJobs}
showDownloadModal={() => this.showDownloadModal(true)}
nCPU={this.props.nCPU}
/>
)
: <div />
}
<SettingsModal
className="mx-3"
clearJobsStorage={this.clearRecentJobs}
showDownloadModal={() => this.showDownloadModal(true)}
nCPU={this.props.nCPU}
/>
</Col>
</Row>
</Navbar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ class DataDownloadModal extends React.Component {
this.state.selectedLinksArray,
data.filePaths[0]
);
this.props.storeDownloadDir(data.filePaths[0]);
this.closeDialog();
}
}
Expand Down Expand Up @@ -283,7 +282,6 @@ class DataDownloadModal extends React.Component {
DataDownloadModal.propTypes = {
show: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
storeDownloadDir: PropTypes.func.isRequired,
};

export default withTranslation()(DataDownloadModal)
export default withTranslation()(DataDownloadModal);
Loading

0 comments on commit 7843942

Please sign in to comment.