From 5e7f96bedbcf376986da8212e193c725362325d4 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Fri, 20 May 2022 11:03:13 +1000 Subject: [PATCH] Ipc/account saving (#129) * human name saves to config using PromiseIPC Signed-off-by: Sven Dowideit * use react-editext for inline editing - styling and fixes to come Signed-off-by: Sven Dowideit * AccountView Alias edit styling hack Signed-off-by: Sven Dowideit * linties Signed-off-by: Sven Dowideit * save the private key when WB creates a new KeyPair Signed-off-by: Sven Dowideit * linties Signed-off-by: Sven Dowideit --- package.json | 1 + src/main/ipc/accounts.ts | 31 ++++ src/main/{ => ipc}/config.ts | 2 +- src/main/main.ts | 4 +- src/renderer/App.scss | 19 +++ .../components/AccountNameEditable.tsx | 46 ------ src/renderer/components/AccountView.tsx | 104 ++++++++++-- src/renderer/components/Editable.tsx | 155 ------------------ src/renderer/components/ProgramChange.tsx | 9 +- src/renderer/components/ProgramChangeView.tsx | 38 ++--- src/renderer/data/accounts/account.ts | 2 +- src/renderer/data/accounts/accountState.ts | 111 +++++++++++++ src/renderer/data/accounts/getAccount.ts | 6 +- src/renderer/store.ts | 5 + src/types/types.tsx | 2 +- 15 files changed, 293 insertions(+), 242 deletions(-) create mode 100644 src/main/ipc/accounts.ts rename src/main/{ => ipc}/config.ts (96%) delete mode 100644 src/renderer/components/AccountNameEditable.tsx delete mode 100644 src/renderer/components/Editable.tsx create mode 100644 src/renderer/data/accounts/accountState.ts diff --git a/package.json b/package.json index fb5494b0..2801e52b 100644 --- a/package.json +++ b/package.json @@ -257,6 +257,7 @@ "react": "^18.1.0", "react-bootstrap": "^2.0.2", "react-dom": "^18.1.0", + "react-editext": "^4.2.1", "react-outside-click-handler": "^1.3.0", "react-redux": "^8.0.1", "react-router": "^6.2.2", diff --git a/src/main/ipc/accounts.ts b/src/main/ipc/accounts.ts new file mode 100644 index 00000000..ca8ce561 --- /dev/null +++ b/src/main/ipc/accounts.ts @@ -0,0 +1,31 @@ +import cfg from 'electron-cfg'; +import promiseIpc from 'electron-promise-ipc'; +import type { IpcMainEvent, IpcRendererEvent } from 'electron'; + +import { logger } from '../logger'; + +declare type IpcEvent = IpcRendererEvent & IpcMainEvent; + +// Need to import the file and call a function (from the main process) to get the IPC promise to exist. +export function initAccountPromises() { + // gets written to .\AppData\Roaming\SolanaWorkbench\electron-cfg.json on windows + promiseIpc.on('ACCOUNT-GetAll', (event: IpcEvent | undefined) => { + logger.silly('main: called ACCOUNT-GetAll', event); + const config = cfg.get('accounts'); + if (!config) { + return {}; + } + return config; + }); + // TODO: so the idea is that this == a list of private keys with annotations (like human name...) + // so it could be key: public key, value is a map[string]interface{} with a convention that 'privatekey' contains that in X form... + promiseIpc.on( + 'ACCOUNT-Set', + (key: unknown, val: unknown, event?: IpcEvent | undefined) => { + logger.silly(`main: called ACCOUNT-Set, ${key}, ${val}, ${event}`); + return cfg.set(`accounts.${key}`, val); + } + ); +} + +export default {}; diff --git a/src/main/config.ts b/src/main/ipc/config.ts similarity index 96% rename from src/main/config.ts rename to src/main/ipc/config.ts index 83b6259e..94a06d25 100644 --- a/src/main/config.ts +++ b/src/main/ipc/config.ts @@ -2,7 +2,7 @@ import cfg from 'electron-cfg'; import promiseIpc from 'electron-promise-ipc'; import type { IpcMainEvent, IpcRendererEvent } from 'electron'; -import { logger } from './logger'; +import { logger } from '../logger'; declare type IpcEvent = IpcRendererEvent & IpcMainEvent; diff --git a/src/main/main.ts b/src/main/main.ts index 318dfe08..677a95ac 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -8,7 +8,8 @@ import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; import { logger, initLogging } from './logger'; import { runValidator, validatorLogs } from './validator'; -import { initConfigPromises } from './config'; +import { initConfigPromises } from './ipc/config'; +import { initAccountPromises } from './ipc/accounts'; import fetchAnchorIdl from './anchor'; import { @@ -29,6 +30,7 @@ let mainWindow: BrowserWindow | null = null; const MAX_STRING_LOG_LENGTH = 32; initConfigPromises(); +initAccountPromises(); ipcMain.on( 'main', diff --git a/src/renderer/App.scss b/src/renderer/App.scss index f4410c83..2dde03f2 100644 --- a/src/renderer/App.scss +++ b/src/renderer/App.scss @@ -191,4 +191,23 @@ th, .vscroll { overflow-y: auto; +} + +// EdiText - https://github.com/alioguzhan/react-editext#styling-with-styled-components +div[editext='view-container'], +div[editext='view'], +div[editext='edit-container'] { + flex: 0 0 auto; + width: 100%; +} + +div[editext='view'] { + width: calc(100% - 32px); +} +.align-center { + flex: 0 0 auto; + width: 100%; + display: flex; + align-items: center; + height: 2.4em; /* TODO: this is a terrible hack for aligning the "Editable Alias" with the inline edit box height. */ } \ No newline at end of file diff --git a/src/renderer/components/AccountNameEditable.tsx b/src/renderer/components/AccountNameEditable.tsx deleted file mode 100644 index 0ea7c055..00000000 --- a/src/renderer/components/AccountNameEditable.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useRef } from 'react'; -import analytics from '../common/analytics'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { accountsActions } from '../data/SelectedAccountsList/selectedAccountsState'; -import { selectValidatorNetworkState } from '../data/ValidatorNetwork/validatorNetworkState'; - -import { AccountInfo } from '../data/accounts/accountInfo'; -import { getHumanName } from '../data/accounts/getAccount'; - -import Editable from './Editable'; - -function AccountNameEditable(props: { - account: AccountInfo; - innerProps: { - placeholder: string; - outerSelected: boolean | undefined; - outerHovered: boolean | undefined; - }; -}) { - const { account, innerProps } = props; - const { net } = useAppSelector(selectValidatorNetworkState); - const dispatch = useAppDispatch(); - const { pubKey } = account; - const humanName = getHumanName(account); - const ref = useRef({} as HTMLInputElement); - return ( - dispatch(accountsActions.setEdited(pubKey))} - editingStopped={() => dispatch(accountsActions.setEdited(''))} - handleOutsideClick={() => { - analytics('updateAccountName', { net, pubKey }); - // updateAccountName({ - // net, - // pubKey, - // humanName: ref.current.value, - // }) - }} - // eslint-disable-next-line react/jsx-props-no-spreading - {...innerProps} - /> - ); -} - -export default AccountNameEditable; diff --git a/src/renderer/components/AccountView.tsx b/src/renderer/components/AccountView.tsx index 9d0268a4..b5d239b7 100644 --- a/src/renderer/components/AccountView.tsx +++ b/src/renderer/components/AccountView.tsx @@ -1,13 +1,24 @@ -import { useState } from 'react'; -import { faTerminal } from '@fortawesome/free-solid-svg-icons'; +import { useState, useEffect } from 'react'; +import { + faTerminal, + faEdit, + faSave, + faCancel, + faKey, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Container from 'react-bootstrap/Container'; import ButtonGroup from 'react-bootstrap/ButtonGroup'; import ButtonToolbar from 'react-bootstrap/ButtonToolbar'; -import { useInterval, useAppSelector } from '../hooks'; +import EdiText from 'react-editext'; +import { useInterval, useAppSelector, useAppDispatch } from '../hooks'; import analytics from '../common/analytics'; import { AccountInfo } from '../data/accounts/accountInfo'; +import { + setAccountValues, + useAccountMeta, +} from '../data/accounts/accountState'; import { truncateLamportAmount, getHumanName, @@ -43,6 +54,9 @@ const explorerURL = (net: Net, address: string) => { function AccountView(props: { pubKey: string | undefined }) { const { pubKey } = props; const { net, status } = useAppSelector(selectValidatorNetworkState); + const dispatch = useAppDispatch(); + const accountMeta = useAccountMeta(pubKey); + const [humanName, setHumanName] = useState(''); const [account, setSelectedAccountInfo] = useState( undefined @@ -61,9 +75,28 @@ function AccountView(props: { pubKey: string | undefined }) { } }, 666); - const humanName = account - ? getHumanName(account) - : 'No on-chain account selected'; + useEffect(() => { + const alias = getHumanName(accountMeta); + setHumanName(alias); + logger.info(`get human name for pubKey ${pubKey} == ${alias}`); + }, [pubKey, accountMeta]); + + const handleHumanNameSave = (val: string) => { + if (!pubKey) { + return; + } + dispatch( + setAccountValues({ + key: pubKey, + value: { + ...accountMeta, + humanname: val, + }, + }) + ); + }; + + // const humanName = getHumanName(accountMeta); return ( @@ -72,21 +105,40 @@ function AccountView(props: { pubKey: string | undefined }) { -
-
-
-
- {humanName !== '' ? humanName :
 
} -
-
-
-
+
-
+
+ + + + + + + +
+
+
+ Editable Alias +
+
+
+ + } + saveButtonContent={} + cancelButtonContent={ + + } + /> + +
Pubkey @@ -127,6 +179,26 @@ function AccountView(props: { pubKey: string | undefined }) { )}
+ Private key known + + {accountMeta?.privatekey ? ( +
+ + Yes +
+ ) : ( + + No + + )} +
Explorer diff --git a/src/renderer/components/Editable.tsx b/src/renderer/components/Editable.tsx deleted file mode 100644 index 51eb831b..00000000 --- a/src/renderer/components/Editable.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -import React, { useEffect, useState } from 'react'; -import { FormControl, InputGroup } from 'react-bootstrap'; -import OutsideClickHandler from 'react-outside-click-handler'; -import { useDispatch } from 'react-redux'; -import { - setSelected, - setHovered, -} from '../data/SelectedAccountsList/selectedAccountsState'; - -type EditableProps = { - value: string; - outerHovered?: boolean; - outerSelected?: boolean; - className?: string; - inputClassName?: string; - clearAllOnSelect?: boolean; - placeholder?: string; - - // TODO: factor these out into forwardRefs? - onClick?: () => void; - handleOutsideClick?: () => void; - editingStopped?: () => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onPaste?: (e: any) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onKeyDown?: (e: any) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onBlur?: (e: any) => void; - effect?: React.EffectCallback; -}; - -const Editable = React.forwardRef( - (props, ref) => { - const dispatch = useDispatch(); - const { - value, - outerHovered, - outerSelected, - onClick, - editingStopped, - className, - inputClassName, - handleOutsideClick, - clearAllOnSelect, - placeholder, - onPaste, - onKeyDown, - onBlur, - effect, - } = props; - const [hovering, setHovering] = useState(false); - const [editing, setEditing] = useState(false); - - // eslint-disable-next-line react-hooks/rules-of-hooks - if (effect) useEffect(effect); - - let formValue = value; - if (clearAllOnSelect) { - formValue = ''; - } - - let classes = `${className} border rounded`; - if (outerHovered) { - classes = `${classes} bg-white`; - } else if (!outerSelected) { - classes = `${classes} border-white`; - } - if (hovering && !editing) { - classes = `${classes} border-soft-dark`; - } - if (editing) { - classes = `${classes} border-white`; - } - if (outerSelected && !outerHovered) { - classes = `${classes} border-selected`; - } - - const completeEdit = () => { - if (editing) { - setHovering(false); - setEditing(false); - if (editingStopped) editingStopped(); - if (handleOutsideClick) handleOutsideClick(); - } - }; - - return ( - -
setHovering(true)} - onMouseLeave={() => setHovering(false)} - onClick={(e) => { - e.stopPropagation(); - setEditing(true); - dispatch(setSelected('')); - dispatch(setHovered('')); - if (onClick) onClick(); - }} - > - - { - if (e.key === 'Enter') { - e.preventDefault(); - completeEdit(); - } - }} - onFocus={() => { - setEditing(true); - }} - onKeyDown={(e) => { - if (onKeyDown) onKeyDown(e); - }} - onPaste={(e) => { - if (onPaste) onPaste(e); - }} - onBlur={(e) => { - if (onBlur) onBlur(e); - }} - /> - -
-
- ); - } -); - -Editable.defaultProps = { - className: '', - inputClassName: 'input-clean', - clearAllOnSelect: false, - placeholder: '', - outerHovered: false, - outerSelected: false, - editingStopped: () => {}, - handleOutsideClick: () => {}, - onPaste: () => {}, - onKeyDown: () => {}, - onClick: () => {}, - onBlur: () => {}, - effect: () => {}, -}; - -export default Editable; diff --git a/src/renderer/components/ProgramChange.tsx b/src/renderer/components/ProgramChange.tsx index fbd0ea13..b4ce6f3d 100644 --- a/src/renderer/components/ProgramChange.tsx +++ b/src/renderer/components/ProgramChange.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback } from 'react'; -import { faStar } from '@fortawesome/free-solid-svg-icons'; +import { faStar, faKey } from '@fortawesome/free-solid-svg-icons'; import * as faRegular from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { setSelected } from 'renderer/data/SelectedAccountsList/selectedAccountsState'; @@ -18,6 +18,7 @@ import { NetStatus, selectValidatorNetworkState, } from '../data/ValidatorNetwork/validatorNetworkState'; +import { useAccountMeta } from '../data/accounts/accountState'; const logger = window.electron.log; @@ -32,6 +33,7 @@ export function ProgramChange(props: { const { pubKey, selected, net, pinned, pinAccount } = props; const [change, setChangeInfo] = useState(undefined); const { status } = useAppSelector(selectValidatorNetworkState); + const accountMeta = useAccountMeta(pubKey); const updateAccount = useCallback(() => { if (status !== NetStatus.Running) { @@ -77,6 +79,11 @@ export function ProgramChange(props: {
+ {accountMeta?.privatekey ? ( + + ) : ( + '' + )} diff --git a/src/renderer/components/ProgramChangeView.tsx b/src/renderer/components/ProgramChangeView.tsx index ff6617c4..65cd6f49 100644 --- a/src/renderer/components/ProgramChangeView.tsx +++ b/src/renderer/components/ProgramChangeView.tsx @@ -14,6 +14,7 @@ import ButtonToolbar from 'react-bootstrap/ButtonToolbar'; import Table from 'react-bootstrap/Table'; import { toast } from 'react-toastify'; import Popover from 'react-bootstrap/Popover'; +import EdiText from 'react-editext'; import OutsideClickHandler from 'react-outside-click-handler'; @@ -35,7 +36,6 @@ import { } from '../data/accounts/getAccount'; import { AccountInfo } from '../data/accounts/accountInfo'; -import Editable from './Editable'; import { ProgramChange } from './ProgramChange'; import { unsubscribeProgramChanges, @@ -43,6 +43,7 @@ import { } from '../data/accounts/programChanges'; import createNewAccount from '../data/accounts/account'; import WatchAccountButton from './WatchAccountButton'; +import { setAccountValues } from '../data/accounts/accountState'; export const MAX_PROGRAM_CHANGES_DISPLAYED = 20; export enum KnownProgramID { @@ -130,7 +131,9 @@ function ProgramChangeView() { const [filterDropdownShow, setFilterDropdownShow] = useState(false); const filterProgramIDRef = useRef({} as HTMLInputElement); - const [programID, setProgramID] = useState(KnownProgramID.SystemProgram); + const [programID, setProgramID] = useState( + KnownProgramID.SystemProgram + ); const [anchorEl, setAnchorEl] = useState(undefined); useEffect(() => { @@ -208,23 +211,11 @@ function ProgramChangeView() { Serum DEX V3
- { - filterProgramIDRef.current.value = ''; - }} - placeholder="Paste Program ID" - onKeyDown={(e) => { - if (!(e.code === 'MetaRight' || e.code === 'KeyV')) { - toast.warn('Must paste in valid program ID'); - filterProgramIDRef.current.value = 'Custom'; - filterProgramIDRef.current.blur(); - setFilterDropdownShow(false); - } - }} - onPaste={(e) => { - const pastedID = e.clipboardData.getData('Text'); + { + const pastedID = val; if (pastedID.match(BASE58_PUBKEY_REGEX)) { unsubscribeProgramChanges(net, programID); subscribeProgramChanges( @@ -291,6 +282,15 @@ function ProgramChangeView() {