Enable "disable module" functionality

- changed `remoteModules` in favor of `remoteModulesPaginated`, so we can properly have track of the `prevModule` for every module
- refactored how module is stored from an array of `moduleAddress`es, to an array of `[moduleAddress, prevModuleAddress]`
- Implemented the RemoveModule by copying the `RemoveSafeModule` implementation
- TODO: display data
This commit is contained in:
fernandomg 2020-06-30 21:30:51 -03:00
parent a6ff15d1f3
commit 873f8bad74
5 changed files with 182 additions and 39 deletions

View File

@ -1,11 +1,13 @@
import { List, Set } from 'immutable' import { List } from 'immutable'
export const MODULES_TABLE_ADDRESS_ID = 'address' export const MODULES_TABLE_ADDRESS_ID = 'address'
export const MODULES_TABLE_ACTIONS_ID = 'actions' export const MODULES_TABLE_ACTIONS_ID = 'actions'
export const getModuleData = (modules: Set<string>): List<{ [MODULES_TABLE_ADDRESS_ID]: string }> => { export const getModuleData = (
return modules.toList().map((module) => ({ modulesList: List<[string, string]>,
[MODULES_TABLE_ADDRESS_ID]: module, ): List<{ [MODULES_TABLE_ADDRESS_ID]: [string, string] }> => {
return modulesList.map((modules) => ({
[MODULES_TABLE_ADDRESS_ID]: modules,
})) }))
} }

View File

@ -1,8 +1,8 @@
import { Button, GenericModal, Text, Title, theme } from '@gnosis.pm/safe-react-components' import { Button, Text, Title, theme } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import TableContainer from '@material-ui/core/TableContainer' import TableContainer from '@material-ui/core/TableContainer'
import cn from 'classnames' import cn from 'classnames'
import { Set } from 'immutable' import { List } from 'immutable'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import React from 'react' import React from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
@ -26,6 +26,16 @@ import {
safeModulesSelector, safeModulesSelector,
} from 'src/routes/safe/store/selectors' } from 'src/routes/safe/store/selectors'
import styled from 'styled-components' import styled from 'styled-components'
import Modal from '../../../../../components/Modal'
import Paragraph from '../../../../../components/layout/Paragraph'
import IconButton from '@material-ui/core/IconButton'
import Close from '@material-ui/icons/Close'
import Hairline from '../../../../../components/layout/Hairline'
import Col from '../../../../../components/layout/Col'
import Link from '../../../../../components/layout/Link'
import OpenInNew from '@material-ui/icons/OpenInNew'
import { getEtherScanLink } from '../../../../../logic/wallets/getWeb3'
import { md, secondary } from '../../../../../theme/variables'
export const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn' export const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn'
export const MODULES_ROW_TEST_ID = 'owners-row' export const MODULES_ROW_TEST_ID = 'owners-row'
@ -51,11 +61,17 @@ const TableActionButton = styled(Button)`
background-color: transparent; background-color: transparent;
} }
` `
const FooterWrapper = styled.div` const FooterWrapper = styled.div`
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
` `
const openIconStyle = {
height: md,
color: secondary,
}
const Advanced: React.FC = () => { const Advanced: React.FC = () => {
const classes = useStyles() const classes = useStyles()
@ -66,14 +82,14 @@ const Advanced: React.FC = () => {
const nonce = useSelector(safeNonceSelector) const nonce = useSelector(safeNonceSelector)
const granted = useSelector(grantedSelector) const granted = useSelector(grantedSelector)
const modules = useSelector(safeModulesSelector) const modules = useSelector(safeModulesSelector)
const moduleData = getModuleData(Set(modules)) const moduleData = modules !== null ? getModuleData(List(modules)) : modules
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false) const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false) const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
const [selectedModule, setSelectedModule] = React.useState(null) const [selectedModule, setSelectedModule] = React.useState(null)
const triggerRemoveSelectedModule = (moduleAddress: string): void => { const triggerRemoveSelectedModule = (module: [string, string]): void => {
setSelectedModule(moduleAddress) setSelectedModule(module)
setViewRemoveModuleModal(true) setViewRemoveModuleModal(true)
} }
@ -81,8 +97,10 @@ const Advanced: React.FC = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const removeSelectedModule = async (): Promise<void> => { const removeSelectedModule = async (): Promise<void> => {
try {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const txData = safeInstance.contract.methods.disableModule(selectedModule).encodeABI() const [module, prevModule] = selectedModule
const txData = safeInstance.contract.methods.disableModule(module, prevModule).encodeABI()
dispatch( dispatch(
createTransaction({ createTransaction({
@ -95,6 +113,9 @@ const Advanced: React.FC = () => {
closeSnackbar, closeSnackbar,
}), }),
) )
} catch (e) {
console.error(`failed to remove the module ${selectedModule}`, e.message)
}
} }
return ( return (
@ -130,7 +151,7 @@ const Advanced: React.FC = () => {
</a> </a>
. .
</InfoText> </InfoText>
{moduleData.size === 0 ? ( {moduleData === null ? (
<InfoText color="secondaryLight" size="xl"> <InfoText color="secondaryLight" size="xl">
No modules enabled No modules enabled
</InfoText> </InfoText>
@ -163,8 +184,8 @@ const Advanced: React.FC = () => {
<TableCell align={column.align} component="td" key={columnId}> <TableCell align={column.align} component="td" key={columnId}>
{columnId === MODULES_TABLE_ADDRESS_ID ? ( {columnId === MODULES_TABLE_ADDRESS_ID ? (
<Block justify="left"> <Block justify="left">
<Identicon address={rowElement} diameter={32} /> <Identicon address={rowElement[0]} diameter={32} />
<AddressText size="lg">{rowElement}</AddressText> <AddressText size="lg">{rowElement[0]}</AddressText>
</Block> </Block>
) : ( ) : (
rowElement rowElement
@ -178,7 +199,7 @@ const Advanced: React.FC = () => {
iconType="delete" iconType="delete"
color="error" color="error"
variant="outlined" variant="outlined"
onClick={() => triggerRemoveSelectedModule(columnId)} onClick={() => triggerRemoveSelectedModule(rowElement)}
data-testid={REMOVE_MODULE_BTN_TEST_ID} data-testid={REMOVE_MODULE_BTN_TEST_ID}
> >
{null} {null}
@ -197,11 +218,57 @@ const Advanced: React.FC = () => {
)} )}
</Block> </Block>
{viewRemoveModuleModal && ( {viewRemoveModuleModal && (
<GenericModal <Modal
onClose={hideRemoveModuleModal} description="Remove the selected Module"
title="Disable Module" handleClose={hideRemoveModuleModal}
body={<div>This is the body</div>} open={viewRemoveModuleModal}
footer={ paperClassName={classes.modal}
title="Remove Module"
>
<Row align="center" className={classes.modalHeading} grow>
<Paragraph className={classes.modalManage} noMargin weight="bolder">
Remove Modal
</Paragraph>
<IconButton disableRipple onClick={hideRemoveModuleModal}>
<Close className={classes.modalClose} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.modalContainer}>
<Row className={classes.modalOwner}>
<Col align="center" xs={1}>
<Identicon address={selectedModule[0]} diameter={32} />
</Col>
<Col xs={11}>
<Block className={cn(classes.modalName, classes.modalUserName)}>
<Paragraph noMargin size="lg" weight="bolder">
{selectedModule[0]}
</Paragraph>
<Block className={classes.modalUser} justify="center">
<Paragraph color="disabled" noMargin size="md">
{selectedModule[0]}
</Paragraph>
<Link
className={classes.modalOpen}
target="_blank"
to={getEtherScanLink('address', selectedModule[0])}
>
<OpenInNew style={openIconStyle} />
</Link>
</Block>
</Block>
</Col>
</Row>
<Hairline />
<Row className={classes.modalDescription}>
<Paragraph noMargin>
By removing a Module you will sign a new Safe Transaction. The Safe will not be able to interact with it
any longer until it is enabled again through a new Safe Transaction.
</Paragraph>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.modalButtonRow}>
<FooterWrapper> <FooterWrapper>
<Button size="md" color="secondary" onClick={hideRemoveModuleModal}> <Button size="md" color="secondary" onClick={hideRemoveModuleModal}>
Cancel Cancel
@ -210,8 +277,8 @@ const Advanced: React.FC = () => {
Remove Remove
</Button> </Button>
</FooterWrapper> </FooterWrapper>
} </Row>
/> </Modal>
)} )}
</> </>
) )

View File

@ -1,5 +1,5 @@
import { createStyles } from '@material-ui/core' import { createStyles } from '@material-ui/core'
import { border, fontColor, lg, secondaryText, smallFontSize, xl } from 'src/theme/variables' import { background, border, error, fontColor, lg, md, secondaryText, sm, smallFontSize, xl } from 'src/theme/variables'
export const styles = createStyles({ export const styles = createStyles({
title: { title: {
@ -54,4 +54,59 @@ export const styles = createStyles({
marginLeft: lg, marginLeft: lg,
cursor: 'pointer', cursor: 'pointer',
}, },
modalHeading: {
boxSizing: 'border-box',
justifyContent: 'space-between',
maxHeight: '75px',
padding: `${sm} ${lg}`,
},
modalContainer: {
minHeight: '369px',
},
modalManage: {
fontSize: lg,
},
modalClose: {
height: '35px',
width: '35px',
},
modalButtonRow: {
height: '84px',
justifyContent: 'center',
},
modalButtonRemove: {
color: '#fff',
backgroundColor: error,
height: '42px',
},
modalName: {
textOverflow: 'ellipsis',
overflow: 'hidden',
},
modalUserName: {
whiteSpace: 'nowrap',
},
modalOwner: {
backgroundColor: background,
padding: md,
alignItems: 'center',
},
modalUser: {
justifyContent: 'left',
},
modalDescription: {
padding: md,
},
modalOpen: {
paddingLeft: sm,
width: 'auto',
'&:hover': {
cursor: 'pointer',
},
},
modal: {
height: 'auto',
maxWidth: 'calc(100% - 30px)',
overflow: 'hidden',
},
}) })

View File

@ -15,6 +15,7 @@ import { makeOwner } from 'src/routes/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeOwner } from '../models/safe' import { SafeOwner } from '../models/safe'
import addSafeModules from './addSafeModules' import addSafeModules from './addSafeModules'
import { SENTINEL_ADDRESS } from '../../../../logic/contracts/safeContracts'
const buildOwnersFrom = ( const buildOwnersFrom = (
safeOwners, safeOwners,
@ -37,6 +38,18 @@ const buildOwnersFrom = (
}) })
}) })
const buildModulesLinkedList = (modulesPaginated: [Array<string>, string] | null): Array<[string, string]> | null => {
if (modulesPaginated.length) {
const [remoteModules, nextModule] = modulesPaginated
return remoteModules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1]
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
})
}
return null
}
export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: any) => { export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: any) => {
const safeAddress = checksumAddress(safeAdd) const safeAddress = checksumAddress(safeAdd)
@ -75,8 +88,14 @@ export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?:
export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => { export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => {
const safeAddress = checksumAddress(safeAdd) const safeAddress = checksumAddress(safeAdd)
// Check if the owner's safe did change and update them // Check if the owner's safe did change and update them
const safeParams = ['getThreshold', 'nonce', 'getOwners', 'getModules'] const safeParams = [
const [[remoteThreshold, remoteNonce, remoteOwners, remoteModules], localSafe] = await Promise.all([ 'getThreshold',
'nonce',
'getOwners',
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] },
]
const [[remoteThreshold, remoteNonce, remoteOwners, remoteModulesPaginated], localSafe] = await Promise.all([
generateBatchRequests({ generateBatchRequests({
abi: GnosisSafeSol.abi, abi: GnosisSafeSol.abi,
address: safeAddress, address: safeAddress,
@ -93,7 +112,7 @@ export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => {
dispatch( dispatch(
addSafeModules({ addSafeModules({
safeAddress, safeAddress,
modulesAddresses: remoteModules, modulesAddresses: buildModulesLinkedList(remoteModulesPaginated),
}), }),
) )

View File

@ -11,7 +11,7 @@ export type SafeRecordProps = {
threshold: number threshold: number
ethBalance: number | string ethBalance: number | string
owners: List<{ name: string; address: string }> owners: List<{ name: string; address: string }>
modules: Set<string> modules: List<[string, string]> | null
activeTokens: Set<string> activeTokens: Set<string>
activeAssets: Set<string> activeAssets: Set<string>
blacklistedTokens: Set<string> blacklistedTokens: Set<string>
@ -31,7 +31,7 @@ const makeSafe = Record<SafeRecordProps>({
threshold: 0, threshold: 0,
ethBalance: '0', ethBalance: '0',
owners: List([]), owners: List([]),
modules: Set(), modules: List(),
activeTokens: Set(), activeTokens: Set(),
activeAssets: Set(), activeAssets: Set(),
blacklistedTokens: Set(), blacklistedTokens: Set(),