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:
parent
a6ff15d1f3
commit
873f8bad74
|
@ -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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Reference in New Issue