Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

[Epic] Dashboard #3696

Merged
merged 17 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"@gnosis.pm/safe-core-sdk": "^2.0.0",
"@gnosis.pm/safe-deployments": "^1.8.0",
"@gnosis.pm/safe-react-components": "^1.1.2",
"@gnosis.pm/safe-react-gateway-sdk": "^2.10.2",
"@gnosis.pm/safe-react-gateway-sdk": "^2.10.3",
"@gnosis.pm/safe-web3-lib": "^1.0.0",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.0",
Expand Down
5 changes: 5 additions & 0 deletions src/assets/icons/explore.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/components/AppLayout/Sidebar/useSidebarItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ const useSidebarItems = (): ListItemType[] => {
].filter(Boolean)

return [
makeEntryItem({
label: 'Home',
iconType: 'home',
href: currentSafeRoutes.DASHBOARD,
}),
makeEntryItem({
label: 'Assets',
iconType: 'assets',
Expand Down
2 changes: 1 addition & 1 deletion src/components/AppLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const ContentWrapper = styled.div`
flex-direction: column;
overflow-x: auto;

padding: 0 16px;
padding: 8px 24px;

> :nth-child(1) {
flex-grow: 1;
Expand Down
69 changes: 69 additions & 0 deletions src/components/Dashboard/FeaturedApps/FeaturedApps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ReactElement, useMemo } from 'react'
import { useAppList } from 'src/routes/safe/components/Apps/hooks/appList/useAppList'
import { Text } from '@gnosis.pm/safe-react-components'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { Box, Grid } from '@material-ui/core'

import styled from 'styled-components'
import { getSafeAppUrl, SafeRouteParams } from 'src/routes/routes'
import { currentSafe } from 'src/logic/safe/store/selectors'
import { getShortName } from 'src/config'
import { Card, WidgetBody, WidgetContainer, WidgetTitle } from 'src/components/Dashboard/styled'

export const FEATURED_APPS_TAG = 'dashboard-widgets'

const StyledImage = styled.img`
width: 64px;
height: 64px;
`

const StyledLink = styled(Link)`
margin-top: 10px;
text-decoration: none;
`

export const FeaturedApps = (): ReactElement | null => {
const { allApps, isLoading } = useAppList()
const { address } = useSelector(currentSafe) ?? {}
const featuredApps = useMemo(() => allApps.filter((app) => app.tags?.includes(FEATURED_APPS_TAG)), [allApps])

const routesSlug: SafeRouteParams = {
shortName: getShortName(),
safeAddress: address,
}

if (!featuredApps.length && !isLoading) return null

return (
<Grid item xs={12} md={6}>
<WidgetContainer>
<WidgetTitle>Connect & Transact</WidgetTitle>
<WidgetBody>
{featuredApps.map((app) => {
const appRoute = getSafeAppUrl(app.url, routesSlug)
return (
<Card key={app.id}>
<Grid container alignItems="center" spacing={3}>
<Grid item xs={12} md={3}>
<StyledImage src={app.iconUrl} alt={app.name} />
</Grid>
<Grid item xs={12} md={9}>
<Box mb={1}>
<Text size="xl">{app.description}</Text>
</Box>
<StyledLink to={appRoute}>
<Text color="primary" size="lg" strong>
Use {app.name}
</Text>
</StyledLink>
</Grid>
</Grid>
</Card>
)
})}
</WidgetBody>
</WidgetContainer>
</Grid>
)
}
164 changes: 164 additions & 0 deletions src/components/Dashboard/Overview/Overview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { ReactElement } from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import { Text, Identicon } from '@gnosis.pm/safe-react-components'
import { useHistory } from 'react-router-dom'
import { Box, Grid } from '@material-ui/core'
import { Skeleton } from '@material-ui/lab'

import { currentSafeLoaded, currentSafeWithNames } from 'src/logic/safe/store/selectors'
import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo'
import { primaryLite, primaryActive, smallFontSize, md, lg } from 'src/theme/variables'
import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel'
import { nftLoadedSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import { Card, DashboardTitle } from 'src/components/Dashboard/styled'
import { WidgetBody, WidgetContainer } from 'src/components/Dashboard/styled'
import Button from 'src/components/layout/Button'
import { generateSafeRoute, SAFE_ROUTES } from 'src/routes/routes'
import { currentChainId } from 'src/logic/config/store/selectors'
import { getChainById } from 'src/config'

const IdenticonContainer = styled.div`
position: relative;
margin-bottom: ${md};
`

const SafeThreshold = styled.div`
position: absolute;
left: -6px;
top: -6px;
background: ${primaryLite};
color: ${primaryActive};
font-size: ${smallFontSize};
font-weight: bold;
border-radius: 100%;
padding: 4px;
z-index: 2;
min-width: 24px;
min-height: 24px;
box-sizing: border-box;
`

const StyledText = styled(Text)`
margin-top: 8px;
font-size: 24px;
font-weight: bold;
`

const NetworkLabelContainer = styled.div`
position: absolute;
top: ${lg};
right: ${lg};

& span {
bottom: auto;
}
`

const ValueSkeleton = <Skeleton variant="text" width={30} />

const SkeletonOverview = (
<Card>
<Grid container>
<Grid item xs={12}>
<IdenticonContainer>
<Skeleton variant="circle" width="48px" height="48px" />
</IdenticonContainer>

<Box mb={2}>
<Text size="xl" strong>
<Skeleton variant="text" height={28} />
</Text>
<Skeleton variant="text" height={21} />
</Box>
<NetworkLabelContainer>
<Skeleton variant="text" width="80px" />
</NetworkLabelContainer>
</Grid>
</Grid>
<Grid container>
<Grid item xs={3}>
<Text color="inputDefault" size="lg">
Tokens
</Text>
<StyledText size="xl">{ValueSkeleton}</StyledText>
</Grid>
<Grid item xs={3}>
<Text color="inputDefault" size="lg">
NFTs
</Text>
<StyledText size="xl">{ValueSkeleton}</StyledText>
</Grid>
</Grid>
</Card>
)

const Overview = (): ReactElement => {
const { address, name, owners, threshold, balances } = useSelector(currentSafeWithNames)
const chainId = useSelector(currentChainId)
const { shortName } = getChainById(chainId)
const loaded = useSelector(currentSafeLoaded)
const nftTokens = useSelector(nftTokensSelector)
const nftLoaded = useSelector(nftLoadedSelector)
const history = useHistory()

const handleOpenAssets = (): void => {
history.push(generateSafeRoute(SAFE_ROUTES.ASSETS_BALANCES, { safeAddress: address, shortName }))
}

return (
<WidgetContainer>
<DashboardTitle>Dashboard</DashboardTitle>
<WidgetBody>
{!loaded ? (
SkeletonOverview
) : (
<Card>
<Grid container>
<Grid item xs={12}>
<IdenticonContainer>
<SafeThreshold>
{threshold}/{owners.length}
</SafeThreshold>
<Identicon address={address} size="xl" />
</IdenticonContainer>
<Box mb={2} overflow="hidden">
<Text size="xl" strong>
{name}
</Text>
<PrefixedEthHashInfo hash={address} textSize="xl" textColor="placeHolder" />
</Box>
<NetworkLabelContainer>
<NetworkLabel />
</NetworkLabelContainer>
</Grid>
</Grid>
<Grid container>
<Grid item xs={3}>
<Text color="inputDefault" size="lg">
Tokens
</Text>
<StyledText size="xl">{balances.length}</StyledText>
</Grid>
<Grid item xs={3}>
<Text color="inputDefault" size="lg">
NFTs
</Text>
{nftTokens && <StyledText size="xl">{nftLoaded ? nftTokens.length : ValueSkeleton}</StyledText>}
</Grid>
<Grid item xs={6}>
<Box display="flex" height={1} alignItems="flex-end" justifyContent="flex-end">
<Button size="medium" variant="contained" color="primary" onClick={handleOpenAssets}>
View Assets
</Button>
</Box>
</Grid>
</Grid>
</Card>
)}
</WidgetBody>
</WidgetContainer>
)
}

export default Overview
90 changes: 90 additions & 0 deletions src/components/Dashboard/PendingTxs/PendingTxListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ReactElement } from 'react'
import { Text } from '@gnosis.pm/safe-react-components'
import { TransactionSummary } from '@gnosis.pm/safe-react-gateway-sdk'
import { Link } from 'react-router-dom'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import styled from 'styled-components'

import { useAssetInfo } from 'src/routes/safe/components/Transactions/TxList/hooks/useAssetInfo'
import { useKnownAddress } from 'src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress'
import { useTransactionType } from 'src/routes/safe/components/Transactions/TxList/hooks/useTransactionType'
import { getTxTo } from 'src/routes/safe/components/Transactions/TxList/utils'
import { boldFont, grey400, primary200, smallFontSize } from 'src/theme/variables'
import { isMultisigExecutionInfo } from 'src/logic/safe/store/models/types/gateway.d'
import Spacer from 'src/components/Spacer'
import { CustomIconText } from 'src/components/CustomIconText'
import { TxInfo } from 'src/routes/safe/components/Transactions/TxList/TxCollapsed'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'

const TransactionToConfirm = styled(Link)`
width: 100%;
display: grid;
align-items: center;
grid-template-columns: 36px 1fr 1fr auto;
gap: 4px;
margin: 0 auto;
padding: 8px 24px;
text-decoration: none;
background-color: ${({ theme }) => theme.colors.white};
border: 2px solid ${grey400};
color: ${({ theme }) => theme.colors.text};
border-radius: 8px;
box-sizing: border-box;
`

const StyledConfirmationsCount = styled.div`
padding: 8px 12px;
border-radius: 8px;
background-color: ${primary200};
font-weight: ${boldFont};
font-size: ${smallFontSize};
`

const TxConfirmations = styled.div`
display: flex;
align-items: center;
margin-left: auto;

& svg {
margin-left: 8px;
}
`

type PendingTxType = {
transaction: TransactionSummary
url: string
}

const PendingTx = ({ transaction, url }: PendingTxType): ReactElement => {
const info = useAssetInfo(transaction.txInfo)
const type = useTransactionType(transaction)
const toAddress = getTxTo(transaction)
const toInfo = useKnownAddress(toAddress)

return (
<TransactionToConfirm key={transaction.id} to={url}>
<Text color="text" size="lg" as="span">
{isMultisigExecutionInfo(transaction.executionInfo) && transaction.executionInfo.nonce}
</Text>
<CustomIconText
address={toAddress?.value || EMPTY_DATA}
iconUrl={type.icon || toInfo?.logoUri || undefined}
iconUrlFallback={type.fallbackIcon}
text={type.text || toInfo?.name || undefined}
/>
{info ? <TxInfo info={info} /> : <Spacer />}
<TxConfirmations>
{isMultisigExecutionInfo(transaction.executionInfo) ? (
<StyledConfirmationsCount>
{`${transaction.executionInfo.confirmationsSubmitted}/${transaction.executionInfo.confirmationsRequired}`}
</StyledConfirmationsCount>
) : (
<Spacer />
)}
<ChevronRightIcon color="secondary" />
</TxConfirmations>
</TransactionToConfirm>
)
}

export default PendingTx
Loading