From 26ee2ceb68ad13f7073041c50736218c20d02b6d Mon Sep 17 00:00:00 2001 From: Nathan LeClaire Date: Wed, 9 Mar 2022 13:21:50 -0800 Subject: [PATCH 1/2] Start config work --- assets/migrations/0002_config.sql | 5 +++++ src/main/config.ts | 15 +++++++++++++++ src/main/main.ts | 4 ++++ src/main/preload.js | 3 +++ src/types/types.tsx | 10 ++++++++++ 5 files changed, 37 insertions(+) create mode 100644 assets/migrations/0002_config.sql create mode 100644 src/main/config.ts diff --git a/assets/migrations/0002_config.sql b/assets/migrations/0002_config.sql new file mode 100644 index 00000000..e05e3f78 --- /dev/null +++ b/assets/migrations/0002_config.sql @@ -0,0 +1,5 @@ +CREATE TABLE config ( + id INTEGER PRIMARY KEY, + key TEXT NOT NULL, + val TEXT NOT NULL, +); \ No newline at end of file diff --git a/src/main/config.ts b/src/main/config.ts new file mode 100644 index 00000000..c38cd0e9 --- /dev/null +++ b/src/main/config.ts @@ -0,0 +1,15 @@ +import { WBConfigRequest, WBConfigResponse } from 'types/types'; +import { db } from './db'; + +async function wbConfig(msg: WBConfigRequest): Promise { + const { action, key } = msg; + if (action === 'set') { + const { val } = msg; + db.run('UPDATE config SET val = ? WHERE name = ?', val, key); + return { val }; + } + const val = await db.get('SELECT val FROM config WHERE name = ?', key); + return { val }; +} + +export default wbConfig; diff --git a/src/main/main.ts b/src/main/main.ts index b473d7c8..99881ece 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,6 +23,7 @@ import { } from './programChanges'; import { RESOURCES_PATH } from './const'; import { db, initDB } from './db'; +import wbConfig from './config'; export default class AppUpdater { constructor() { @@ -74,6 +75,9 @@ ipcMain.on( case 'unsubscribe-program-changes': await unsubscribeProgramChanges(msg); break; + case 'config': + res = await wbConfig(msg); + break; default: } logger.info('OK', { method, ...res }); diff --git a/src/main/preload.js b/src/main/preload.js index b66d185c..74928fac 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -48,6 +48,9 @@ contextBridge.exposeInMainWorld('electron', { unsubscribeProgramChanges(msg) { send('unsubscribe-program-changes', msg); }, + config(msg) { + send('config', msg); + }, on(method, func) { ipcRenderer.on(method, (event, ...args) => func(...args)); }, diff --git a/src/types/types.tsx b/src/types/types.tsx index 90004a3b..47dc25d2 100644 --- a/src/types/types.tsx +++ b/src/types/types.tsx @@ -99,6 +99,16 @@ export type FetchAnchorIDLRequest = { programID: string; }; +export type WBConfigRequest = { + key: string; + val?: string; + action: string; +}; + +export type WBConfigResponse = { + val: string | undefined; +}; + export type ProgramAccountChange = { pubKey: string; net: Net; From feeb7a301b89f2cbe70765ffa90a6a1b3ac54909 Mon Sep 17 00:00:00 2001 From: Nathan LeClaire Date: Wed, 9 Mar 2022 15:58:34 -0800 Subject: [PATCH 2/2] Finish config / analytics opt out support --- assets/migrations/0002_config.sql | 2 +- src/common/analytics.ts | 10 +- src/main/accounts.ts | 1 - src/main/config.ts | 27 +++- src/renderer/App.scss | 7 ++ src/renderer/App.tsx | 196 +++++++++++++++++++++--------- src/renderer/slices/mainSlice.ts | 29 ++++- src/types/types.tsx | 20 ++- 8 files changed, 225 insertions(+), 67 deletions(-) diff --git a/assets/migrations/0002_config.sql b/assets/migrations/0002_config.sql index e05e3f78..b4176e7c 100644 --- a/assets/migrations/0002_config.sql +++ b/assets/migrations/0002_config.sql @@ -1,5 +1,5 @@ CREATE TABLE config ( id INTEGER PRIMARY KEY, key TEXT NOT NULL, - val TEXT NOT NULL, + val TEXT NOT NULL ); \ No newline at end of file diff --git a/src/common/analytics.ts b/src/common/analytics.ts index 11993437..710e3950 100644 --- a/src/common/analytics.ts +++ b/src/common/analytics.ts @@ -1,4 +1,9 @@ import amplitude from 'amplitude-js'; +import { ConfigKey } from 'types/types'; + +// TODO(nathanleclaire): Not the largest fan of this spaghetti-ish import +// renderer is really supposed to import common not vice versa +import store from '../renderer/store'; const AMPLITUDE_KEY = 'f1cde3642f7e0f483afbb7ac15ae8277'; const AMPLITUDE_HEARTBEAT_INTERVAL = 3600000; @@ -6,7 +11,10 @@ const AMPLITUDE_HEARTBEAT_INTERVAL = 3600000; amplitude.getInstance().init(AMPLITUDE_KEY); const analytics = (event: string, metadata: any) => { - if (process.env.NODE_ENV !== 'development' /* and user has not opted out */) { + if ( + process.env.NODE_ENV !== 'development' && + store.getState().config.values[ConfigKey.AnalyticsEnabled] + ) { amplitude.getInstance().logEvent(event, metadata); } }; diff --git a/src/main/accounts.ts b/src/main/accounts.ts index 3bc6fa60..57001a1a 100644 --- a/src/main/accounts.ts +++ b/src/main/accounts.ts @@ -83,7 +83,6 @@ async function accounts(msg: AccountsRequest): Promise { await addKeypair(KEY_PATH); } const kp = await localKeypair(KEY_PATH); - logger.info('accounts', { net, pubKey: kp.publicKey }); const solConn = new sol.Connection(netToURL(net)); const existingAccounts = await db.all( 'SELECT * FROM account WHERE net = ? ORDER BY created_at DESC, humanName ASC', diff --git a/src/main/config.ts b/src/main/config.ts index c38cd0e9..8850ab79 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -1,15 +1,30 @@ -import { WBConfigRequest, WBConfigResponse } from 'types/types'; +import { + ConfigAction, + ConfigMap, + WBConfigRequest, + WBConfigResponse, +} from '../types/types'; import { db } from './db'; async function wbConfig(msg: WBConfigRequest): Promise { const { action, key } = msg; - if (action === 'set') { + if (action === ConfigAction.Set) { const { val } = msg; - db.run('UPDATE config SET val = ? WHERE name = ?', val, key); - return { val }; + const existingRow = await db.get('SELECT * FROM config WHERE key = ?', key); + if (existingRow) { + await db.run('UPDATE config SET val = ? WHERE key = ?', val, key); + } else { + await db.run('INSERT INTO config (key, val) VALUES (?, ?)', key, val); + } } - const val = await db.get('SELECT val FROM config WHERE name = ?', key); - return { val }; + const cfgVals = await db.all('SELECT * FROM config'); + const values: ConfigMap = {}; + if (cfgVals) { + cfgVals.forEach((setting: any) => { + values[setting.key] = setting.val; + }); + } + return { values }; } export default wbConfig; diff --git a/src/renderer/App.scss b/src/renderer/App.scss index 2049b8e2..1a64c5ef 100644 --- a/src/renderer/App.scss +++ b/src/renderer/App.scss @@ -329,3 +329,10 @@ $top-nav-height: 47px; // hack -- def better way to compute somehow .dropdown-menu { z-index: 1020; // hack to fix other items being on top of dropdown } + +.btn-outline-dark { + &:hover { + color: $black; + background-color: $white; + } +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index efd5e208..8c3f69d9 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -18,15 +18,16 @@ import { faNetworkWired, faCircle, } from '@fortawesome/free-solid-svg-icons'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { RootState, validatorActions } from './slices/mainSlice'; -import { Net } from '../types/types'; +import { configActions, RootState, validatorActions } from './slices/mainSlice'; +import { ConfigAction, ConfigKey, Net } from '../types/types'; import analytics from 'common/analytics'; import Toast from './components/Toast'; import Accounts from './nav/Accounts'; import Anchor from './nav/Anchor'; import Validator from './nav/Validator'; +import { Button, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'; declare global { interface Window { @@ -111,8 +112,11 @@ export default function App() { const dispatch = useDispatch(); const { toasts } = useSelector((state: RootState) => state.toast); const validator = useSelector((state: RootState) => state.validator); + const config = useSelector((state: RootState) => state.config); const { net } = validator; + const [analyticsEnabled, setAnalyticsEnabled] = useState('yes'); + useEffect(() => { const listener = (resp: any) => { const { method, res } = resp; @@ -126,6 +130,14 @@ export default function App() { dispatch(validatorActions.setWaitingForRun(false)); } break; + case 'config': + dispatch( + configActions.set({ + loading: false, + values: res.values, + }) + ); + break; default: } }; @@ -133,6 +145,9 @@ export default function App() { window.electron.ipcRenderer.validatorState({ net: Net.Localhost, }); + window.electron.ipcRenderer.config({ + action: ConfigAction.Get, + }); return () => { window.electron.ipcRenderer.removeListener('main', listener); @@ -169,61 +184,132 @@ export default function App() { ); } - return ( - - -
-
-
-
-
-
-
- - {statusDisplay} - - - {Net.Localhost} - - - {Net.Dev} - - - {Net.Test} - - - {Net.MainnetBeta} - - - -
-
-
- - - - - - - - - + let mainDisplay = <>; + + if (!config.loading && !(`${ConfigKey.AnalyticsEnabled}` in config.values)) { + mainDisplay = ( +
+
+

Will you help us out?

+ Workbench collects usage analytics. You can audit this code on{' '} + + Github + + . You can opt out below. +
+
+
What We Collect
+
    +
  • Which features are popular
  • +
  • System properties like OS version
  • +
  • How often people are using Workbench
  • +
+ We do not collect addresses or private keys. +
+ + { + setAnalyticsEnabled('yes'); + }} + > + Sure, I'll Help + + { + setAnalyticsEnabled('no'); + }} + > + No Thanks + + + +
+ ); + } else { + mainDisplay = ( +
+
+
+
+
+
+
+ + {statusDisplay} + + + {Net.Localhost} + + + {Net.Dev} + + + {Net.Test} + + + {Net.MainnetBeta} + + +
+
+ + + + + + + + + +
- +
+ ); + } + + return ( + + {mainDisplay} ); } diff --git a/src/renderer/slices/mainSlice.ts b/src/renderer/slices/mainSlice.ts index 5c6fcc8d..6e32b6b4 100644 --- a/src/renderer/slices/mainSlice.ts +++ b/src/renderer/slices/mainSlice.ts @@ -9,6 +9,7 @@ import { TOAST_BOTTOM_OFFSET, ToastProps, Net, + ConfigState, } from 'types/types'; const validatorState: ValidatorState = { @@ -126,16 +127,40 @@ export const accountsSlice = createSlice({ }, }); +const configState: ConfigState = { + loading: true, + values: {}, +}; + +export const configSlice = createSlice({ + name: 'config', + initialState: configState, + reducers: { + set: (state, action: PayloadAction) => { + state.loading = action.payload.loading; + state.values = action.payload.values; + }, + }, +}); + const mainReducer = combineReducers({ toast: toastSlice.reducer, validator: validatorSlice.reducer, accounts: accountsSlice.reducer, + config: configSlice.reducer, }); export type RootState = ReturnType; -const [toastActions, accountsActions, validatorActions] = [ +const [toastActions, accountsActions, validatorActions, configActions] = [ toastSlice.actions, accountsSlice.actions, validatorSlice.actions, + configSlice.actions, ]; -export { toastActions, accountsActions, validatorActions, mainReducer }; +export { + toastActions, + accountsActions, + validatorActions, + configActions, + mainReducer, +}; diff --git a/src/types/types.tsx b/src/types/types.tsx index 47dc25d2..37d148a3 100644 --- a/src/types/types.tsx +++ b/src/types/types.tsx @@ -25,6 +25,15 @@ export enum ProgramID { TokenProgram = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', } +export enum ConfigKey { + AnalyticsEnabled = 'analytics_enabled', +} + +export enum ConfigAction { + Get = 'get', + Set = 'set', +} + export type WBAccount = { net: Net | undefined; pubKey: string; @@ -106,7 +115,7 @@ export type WBConfigRequest = { }; export type WBConfigResponse = { - val: string | undefined; + values: ConfigMap; }; export type ProgramAccountChange = { @@ -173,3 +182,12 @@ export interface ProgramChangesState { changes: ProgramAccountChange[]; paused: boolean; } + +export interface ConfigMap { + [key: string]: string; +} + +export interface ConfigState { + loading: boolean; + values: ConfigMap; +}