refactor: Advanced settings component

This commit is contained in:
fernandomg 2020-07-17 14:09:26 -03:00
parent ad9ee25b36
commit cc103c00d7
5 changed files with 301 additions and 230 deletions

View File

@ -0,0 +1,125 @@
import { Button, Text } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles'
import TableContainer from '@material-ui/core/TableContainer'
import styled from 'styled-components'
import cn from 'classnames'
import React from 'react'
import { useSelector } from 'react-redux'
import { generateColumns, ModuleAddressColumn, MODULES_TABLE_ADDRESS_ID } from './dataFetcher'
import RemoveModuleModal from './RemoveModuleModal'
import { styles } from './style'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { ModulePair } from 'src/routes/safe/store/models/safe'
import Table from 'src/components/Table'
import { TableCell, TableRow } from 'src/components/layout/Table'
import Block from 'src/components/layout/Block'
import Identicon from 'src/components/Identicon'
import Row from 'src/components/layout/Row'
const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn'
const MODULES_ROW_TEST_ID = 'owners-row'
const useStyles = makeStyles(styles)
const AddressText = styled(Text)`
margin-left: 12px;
`
const TableActionButton = styled(Button)`
background-color: transparent;
&:hover {
background-color: transparent;
}
`
interface ModulesTableProps {
moduleData: ModuleAddressColumn | null
}
const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter(({ custom }) => !custom)
const granted = useSelector(grantedSelector)
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
const [selectedModule, setSelectedModule] = React.useState(null)
const triggerRemoveSelectedModule = (module: ModulePair): void => {
setSelectedModule(module)
setViewRemoveModuleModal(true)
}
return (
<>
<TableContainer>
<Table
columns={columns}
data={moduleData}
defaultFixed
defaultOrderBy={MODULES_TABLE_ADDRESS_ID}
disablePagination
label="Modules"
noBorder
size={moduleData.length}
>
{(sortedData) =>
sortedData.map((row, index) => (
<TableRow
className={cn(classes.hide, index >= 3 && index === sortedData.size - 1 && classes.noBorderBottom)}
data-testid={MODULES_ROW_TEST_ID}
key={index}
tabIndex={-1}
>
{autoColumns.map((column, index) => {
const columnId = column.id
const rowElement = row[columnId]
return (
<React.Fragment key={`${columnId}-${index}`}>
<TableCell align={column.align} component="td" key={columnId}>
{columnId === MODULES_TABLE_ADDRESS_ID ? (
<Block justify="left">
<Identicon address={rowElement[0]} diameter={32} />
<AddressText size="lg">{rowElement[0]}</AddressText>
</Block>
) : (
rowElement
)}
</TableCell>
<TableCell component="td">
<Row align="end" className={classes.actions}>
{granted && (
<TableActionButton
size="md"
iconType="delete"
color="error"
variant="outlined"
onClick={() => triggerRemoveSelectedModule(rowElement)}
data-testid={REMOVE_MODULE_BTN_TEST_ID}
>
{null}
</TableActionButton>
)}
</Row>
</TableCell>
</React.Fragment>
)
})}
</TableRow>
))
}
</Table>
</TableContainer>
{viewRemoveModuleModal && <RemoveModuleModal onClose={hideRemoveModuleModal} selectedModule={selectedModule} />}
</>
)
}
export default ModulesTable

View File

@ -0,0 +1,144 @@
import { Button } from '@gnosis.pm/safe-react-components'
import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import OpenInNew from '@material-ui/icons/OpenInNew'
import cn from 'classnames'
import { useSnackbar } from 'notistack'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import { styles } from './style'
import { ModulePair } from 'src/routes/safe/store/models/safe'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import Modal from 'src/components/Modal'
import Row from 'src/components/layout/Row'
import Paragraph from 'src/components/layout/Paragraph'
import Hairline from 'src/components/layout/Hairline'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Identicon from 'src/components/Identicon'
import Link from 'src/components/layout/Link'
import { getEtherScanLink } from 'src/logic/wallets/getWeb3'
import { md, secondary } from 'src/theme/variables'
const useStyles = makeStyles(styles)
const FooterWrapper = styled.div`
display: flex;
justify-content: space-around;
`
const openIconStyle = {
height: md,
color: secondary,
}
interface RemoveModuleModal {
onClose: () => void
selectedModule: ModulePair
}
const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): React.ReactElement => {
const classes = useStyles()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const dispatch = useDispatch()
const removeSelectedModule = async (): Promise<void> => {
try {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const [module, prevModule] = selectedModule
const txData = safeInstance.contract.methods.disableModule(prevModule, module).encodeABI()
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
} catch (e) {
console.error(`failed to remove the module ${selectedModule}`, e.message)
}
}
return (
<>
<Modal
description="Remove the selected Module"
handleClose={onClose}
paperClassName={classes.modal}
title="Remove Module"
open
>
<Row align="center" className={classes.modalHeading} grow>
<Paragraph className={classes.modalManage} noMargin weight="bolder">
Remove Module
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<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>
After removing this module, any feature or app that uses this module might no longer work. If this Safe
requires more then one signature, the module removal will have to be confirmed by other owners as well.
</Paragraph>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.modalButtonRow}>
<FooterWrapper>
<Button size="md" color="secondary" onClick={onClose}>
Cancel
</Button>
<Button color="error" size="md" variant="contained" onClick={removeSelectedModule}>
Remove
</Button>
</FooterWrapper>
</Row>
</Modal>
</>
)
}
export default RemoveModuleModal

View File

@ -5,9 +5,9 @@ import { ModulePair } from 'src/routes/safe/store/models/safe'
export const MODULES_TABLE_ADDRESS_ID = 'address'
export const MODULES_TABLE_ACTIONS_ID = 'actions'
export const getModuleData = (
modulesList: List<ModulePair> | null,
): List<{ [MODULES_TABLE_ADDRESS_ID]: ModulePair }> | undefined => {
export type ModuleAddressColumn = { [MODULES_TABLE_ADDRESS_ID]: ModulePair }[]
export const getModuleData = (modulesList: ModulePair[] | null): ModuleAddressColumn | undefined => {
return modulesList?.map((modules) => ({
[MODULES_TABLE_ADDRESS_ID]: modules,
}))

View File

@ -1,51 +1,18 @@
import { Button, Loader, Text, Title, theme } from '@gnosis.pm/safe-react-components'
import { Loader, Text, theme, Title } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles'
import TableContainer from '@material-ui/core/TableContainer'
import cn from 'classnames'
import { useSnackbar } from 'notistack'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { generateColumns, MODULES_TABLE_ADDRESS_ID, getModuleData } from './dataFetcher'
import { styles } from './style'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block'
import Row from 'src/components/layout/Row'
import { TableCell, TableRow } from 'src/components/layout/Table'
import Table from 'src/components/Table'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { grantedSelector } from 'src/routes/safe/container/selector'
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
import {
safeParamAddressFromStateSelector,
safeNonceSelector,
safeModulesSelector,
} from 'src/routes/safe/store/selectors'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import Modal from 'src/components/Modal'
import Paragraph from 'src/components/layout/Paragraph'
import IconButton from '@material-ui/core/IconButton'
import Close from '@material-ui/icons/Close'
import Hairline from 'src/components/layout/Hairline'
import Col from 'src/components/layout/Col'
import Link from 'src/components/layout/Link'
import OpenInNew from '@material-ui/icons/OpenInNew'
import { getEtherScanLink } from 'src/logic/wallets/getWeb3'
import { md, secondary } from 'src/theme/variables'
import { ModulePair } from 'src/routes/safe/store/models/safe'
export const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn'
export const MODULES_ROW_TEST_ID = 'owners-row'
import { getModuleData } from './dataFetcher'
import { styles } from './style'
import ModulesTable from './ModulesTable'
import Block from 'src/components/layout/Block'
import { safeModulesSelector, safeNonceSelector } from 'src/routes/safe/store/selectors'
const useStyles = makeStyles(styles)
const AddressText = styled(Text)`
margin-left: 12px;
`
const InfoText = styled(Text)`
margin-top: 16px;
`
@ -54,69 +21,28 @@ const Bold = styled.strong`
color: ${theme.colors.text};
`
const TableActionButton = styled(Button)`
background-color: transparent;
const NoModuleLegend = (): React.ReactElement => (
<InfoText color="secondaryLight" size="xl">
No modules enabled
</InfoText>
)
&:hover {
background-color: transparent;
}
`
const LoadingModules = (): React.ReactElement => {
const classes = useStyles()
const FooterWrapper = styled.div`
display: flex;
justify-content: space-around;
`
const openIconStyle = {
height: md,
color: secondary,
return (
<Block className={classes.container}>
<Loader size="md" />
</Block>
)
}
const Advanced = (): React.ReactElement => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter(({ custom }) => !custom)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const nonce = useSelector(safeNonceSelector)
const granted = useSelector(grantedSelector)
const modules = useSelector(safeModulesSelector)
const moduleData = getModuleData(modules) ?? modules
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
const [selectedModule, setSelectedModule] = React.useState(null)
const triggerRemoveSelectedModule = (module: ModulePair): void => {
setSelectedModule(module)
setViewRemoveModuleModal(true)
}
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const dispatch = useDispatch()
const removeSelectedModule = async (): Promise<void> => {
try {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const [module, prevModule] = selectedModule
const txData = safeInstance.contract.methods.disableModule(prevModule, module).encodeABI()
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
} catch (e) {
console.error(`failed to remove the module ${selectedModule}`, e.message)
}
}
const moduleData = getModuleData(modules) ?? null
return (
<>
@ -151,139 +77,15 @@ const Advanced = (): React.ReactElement => {
</a>
.
</InfoText>
{moduleData === null ? (
<InfoText color="secondaryLight" size="xl">
No modules enabled
</InfoText>
) : moduleData.size === 0 ? (
<Block className={classes.container}>
<Loader size="md" />
</Block>
) : (
<TableContainer>
<Table
columns={columns}
data={moduleData}
defaultFixed
defaultOrderBy={MODULES_TABLE_ADDRESS_ID}
disablePagination
label="Modules"
noBorder
size={moduleData.size}
>
{(sortedData) =>
sortedData.map((row, index) => (
<TableRow
className={cn(classes.hide, index >= 3 && index === sortedData.size - 1 && classes.noBorderBottom)}
data-testid={MODULES_ROW_TEST_ID}
key={index}
tabIndex={-1}
>
{autoColumns.map((column) => {
const columnId = column.id
const rowElement = row[columnId]
return (
<>
<TableCell align={column.align} component="td" key={columnId}>
{columnId === MODULES_TABLE_ADDRESS_ID ? (
<Block justify="left">
<Identicon address={rowElement[0]} diameter={32} />
<AddressText size="lg">{rowElement[0]}</AddressText>
</Block>
{moduleData === null ? (
<NoModuleLegend />
) : moduleData?.length === 0 ? (
<LoadingModules />
) : (
rowElement
)}
</TableCell>
<TableCell component="td">
<Row align="end" className={classes.actions}>
{granted && (
<TableActionButton
size="md"
iconType="delete"
color="error"
variant="outlined"
onClick={() => triggerRemoveSelectedModule(rowElement)}
data-testid={REMOVE_MODULE_BTN_TEST_ID}
>
{null}
</TableActionButton>
)}
</Row>
</TableCell>
</>
)
})}
</TableRow>
))
}
</Table>
</TableContainer>
<ModulesTable moduleData={moduleData} />
)}
</Block>
{viewRemoveModuleModal && (
<Modal
description="Remove the selected Module"
handleClose={hideRemoveModuleModal}
open={viewRemoveModuleModal}
paperClassName={classes.modal}
title="Remove Module"
>
<Row align="center" className={classes.modalHeading} grow>
<Paragraph className={classes.modalManage} noMargin weight="bolder">
Remove Module
</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>
After removing this module, any feature or app that uses this module might no longer work. If this Safe
requires more then one signature, the module removal will have to be confirmed by other owners as well.
</Paragraph>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.modalButtonRow}>
<FooterWrapper>
<Button size="md" color="secondary" onClick={hideRemoveModuleModal}>
Cancel
</Button>
<Button color="error" size="md" variant="contained" onClick={removeSelectedModule}>
Remove
</Button>
</FooterWrapper>
</Row>
</Modal>
)}
</>
)
}

View File

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