From 353414c84d7dc8ffc8fb110413990bb7ee097ef8 Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Wed, 20 Jul 2022 18:49:47 +0200 Subject: [PATCH 1/8] Added first approach of the Safe Widgets POC --- .../Dashboard/CustomizableDashboard.tsx | 186 ++++++++++++++++++ .../Dashboard/SafeWidget/SafeWidget.tsx | 171 ++++++++++++++++ src/routes/Home/index.tsx | 2 + src/widgets/ClaimTokenWidget.tsx | 59 ++++++ src/widgets/GasPriceWidget.tsx | 68 +++++++ 5 files changed, 486 insertions(+) create mode 100644 src/components/Dashboard/CustomizableDashboard.tsx create mode 100644 src/components/Dashboard/SafeWidget/SafeWidget.tsx create mode 100644 src/widgets/ClaimTokenWidget.tsx create mode 100644 src/widgets/GasPriceWidget.tsx diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx new file mode 100644 index 0000000000..616b13c7ca --- /dev/null +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -0,0 +1,186 @@ +import { ReactElement } from 'react' +import styled from 'styled-components' +import SettingsIcon from '@material-ui/icons/Settings' +import { useMediaQuery, IconButton } from '@material-ui/core' + +import { black500, extraLargeFontSize } from 'src/theme/variables' +import SafeWidget, { WidgetCellType, WidgetType } from './SafeWidget/SafeWidget' + +const COLUMN_CELL_SIZE = 50 // pixels +const ROW_CELL_SIZE = 50 // pixels +const WIDGET_GAP = 5 // pixels + +const gasPriceWidget: WidgetType = { + widgetId: 1, + widgetType: 'gasPriceWidget', + widgetEndointUrl: + 'https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=W7N7ISIDY1JFPYUI2D2HWVMMD3RF88QCCD', + pollingTime: 14000, + desktopSize: { + columnCells: 4, + rowCells: 2, + }, + mobileSize: { + columnCells: 4, + rowCells: 2, + }, +} + +const claimCowTokens: WidgetType = { + widgetId: 2, + widgetType: 'claimTokensWidget', + widgetEndointUrl: 'http://localhost:3001/api', + desktopSize: { + columnCells: 5, + rowCells: 4, + }, + mobileSize: { + columnCells: 4, + rowCells: 4, + }, + widgetProps: { + iconUrl: 'https://cowswap.exchange/static/media/cow_v2.00b93700.svg', + tokenName: 'CoW Protocol Token', + tokenSymbol: 'COW', + tokenAddress: '0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB', + }, +} + +const claimSafeTokens: WidgetType = { + widgetId: 2, + widgetType: 'claimTokensWidget', + widgetEndointUrl: 'http://localhost:3002/api', + desktopSize: { + columnCells: 5, + rowCells: 4, + }, + mobileSize: { + columnCells: 4, + rowCells: 4, + }, + widgetProps: { + iconUrl: + 'https://safe-transaction-assets.gnosis-safe.io/tokens/logos/0x5aFE3855358E112B5647B952709E6165e1c1eEEe.png', + tokenName: 'Safe Token', + tokenSymbol: 'SAFE', + tokenAddress: '0x5aFE3855358E112B5647B952709E6165e1c1eEEe', + }, +} + +const cowSwapWidget: WidgetType = { + widgetId: 3, + widgetType: 'iframe', + widgetIframeUrl: 'https://cowswap.exchange/', + desktopSize: { + columnCells: 8, + rowCells: 16, + }, + mobileSize: { + columnCells: 6, + rowCells: 10, + }, +} + +const uniSwapWidget: WidgetType = { + widgetId: 3, + widgetType: 'iframe', + widgetIframeUrl: 'https://app.uniswap.org', + desktopSize: { + columnCells: 7, + rowCells: 10, + }, + mobileSize: { + columnCells: 6, + rowCells: 9, + }, +} + +const rampWidget: WidgetType = { + widgetId: 3, + widgetType: 'iframe', + widgetIframeUrl: 'https://apps.gnosis-safe.io/ramp-network', + desktopSize: { + columnCells: 7, + rowCells: 12, + }, + mobileSize: { + columnCells: 7, + rowCells: 10, + }, +} + +const widgets: WidgetType[] = [ + gasPriceWidget, + gasPriceWidget, + claimCowTokens, + cowSwapWidget, + gasPriceWidget, + gasPriceWidget, + uniSwapWidget, + claimSafeTokens, + rampWidget, +] + +const CustomizableDashboard = (): ReactElement => { + const isMobile = useMediaQuery('(max-width:600px)') + + return ( + + + customizable Dashboard POC + + + + + + + {widgets.map((widget) => { + const { rowCells, columnCells } = isMobile ? widget.mobileSize : widget.desktopSize + + return ( + + + + ) + })} + + + ) +} + +export default CustomizableDashboard + +const StyledGridContainer = styled.div` + max-width: 1300px; + margin: 0 auto; +` + +const DashboardHeader = styled.div` + display: flex; + justify-content: space-between; + padding-right: 12px; +` + +const StyledIcon = styled(IconButton)` + height: 42px; + width: 42px; +` + +export const DashboardTitle = styled.h1` + color: ${black500}; + font-size: ${extraLargeFontSize}; +` + +const DashboardGrid = styled.div` + display: grid; + justify-content: center; + grid-template-columns: repeat(auto-fit, ${WIDGET_GAP}px); + grid-auto-rows: ${WIDGET_GAP}px; +` +const DashboardItem = styled.div<{ columnCells: WidgetCellType; rowCells: WidgetCellType }>` + grid-column: span ${({ columnCells }) => columnCells * (COLUMN_CELL_SIZE / WIDGET_GAP) + 2}; + grid-row: span ${({ rowCells }) => rowCells * (ROW_CELL_SIZE / WIDGET_GAP) + 2}; + + margin: ${WIDGET_GAP}px ${WIDGET_GAP}px; + outline: 2px dashed green; +` diff --git a/src/components/Dashboard/SafeWidget/SafeWidget.tsx b/src/components/Dashboard/SafeWidget/SafeWidget.tsx new file mode 100644 index 0000000000..82c7c932a1 --- /dev/null +++ b/src/components/Dashboard/SafeWidget/SafeWidget.tsx @@ -0,0 +1,171 @@ +import axios from 'axios' +import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +import { getChainById } from 'src/config' +import { currentChainId } from 'src/logic/config/store/selectors' +import { currentSafeLoaded, currentSafeWithNames } from 'src/logic/safe/store/selectors' +import GasPriceWidget from 'src/widgets/GasPriceWidget' +import ClaimTokenWidget from 'src/widgets/ClaimTokenWidget' +import AppFrame from 'src/routes/safe/components/Apps/components/AppFrame' + +type SafeWidgetProps = { + widget: WidgetType +} + +export type WidgetCellType = DesktopCellsType | MobileCellsType + +// TODO: REFINE SIZE TYPES +// width: 50px, 150px, 200px ... 500px +// heigh: 10px, 20px, ... 990px +export type DesktopCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 +export type MobileCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + +// TODO: Create different types for iframeWidgetType & componentWidgetType +export type WidgetType = { + widgetId: number + widgetType: string + desktopSize: { + columnCells: DesktopCellsType + rowCells: DesktopCellsType + } + mobileSize: { + columnCells: MobileCellsType + rowCells: MobileCellsType + } + widgetIframeUrl?: string + widgetEndointUrl?: string + pollingTime?: number + widgetProps?: Record +} + +export type SafeWidgetComponentProps = { + widget: WidgetType + // TODO: refine safeInfo type + safeInfo: Record + data: Record + appUrl?: string + isLoading: boolean +} + +const availableWidgets = { + gasPriceWidget: GasPriceWidget, + claimTokensWidget: ClaimTokenWidget, + iframe: AppFrame, +} + +const SafeWidget = ({ widget }: SafeWidgetProps): ReactElement => { + const { widgetType, widgetIframeUrl, widgetEndointUrl, pollingTime } = widget + + const [data, setData] = useState({}) + const [isLoading, setIsLoading] = useState(!!widgetEndointUrl) + + // TODO: Refine safeInfo + const { address, name, owners, threshold, balances } = useSelector(currentSafeWithNames) + const chainId = useSelector(currentChainId) + const { shortName } = getChainById(chainId) + const loaded = useSelector(currentSafeLoaded) + const safeInfo = { + address, + name, + owners, + threshold, + balances, + chain: { + chainId, + shortName, + }, + loaded, + } + + const callEndpoint = useCallback(async () => { + if (widgetEndointUrl) { + // --------------- REMOVE THIS FAKE CALL + + if (widgetEndointUrl === 'http://localhost:3001/api') { + setData({ + totalAmount: 1200, + claimedAmount: 30, + }) + return + } + + if (widgetEndointUrl === 'http://localhost:3002/api') { + setData({ + totalAmount: 3200, + claimedAmount: 0, + }) + return + } + + // -------------------- + + const { data } = await axios.get(widgetEndointUrl) + setData(data) + setIsLoading(false) + } + }, [widgetEndointUrl]) + + // call on mount widget + useEffect(() => { + callEndpoint() + }, [callEndpoint]) + + // polling calls + useEffect(() => { + let intervalId + if (pollingTime) { + intervalId = setInterval(() => { + callEndpoint() + }, pollingTime) + } + + return () => { + intervalId && clearInterval(intervalId) + } + }, [callEndpoint, pollingTime]) + + const WidgetComponent = useMemo(() => availableWidgets[widgetType], [widgetType]) + + return ( + + + + ) +} + +export default SafeWidget + +const Wrapper = styled.div<{ widgetType: string }>` + height: 100%; + width: 100%; + + ${({ widgetType }) => + widgetType === 'iframe' && + ` + padding: 0; + + && > div { + margin: 0; + height: 100%; + } + + && > div > div { + border-radius: 8px; + } + + && > div > div > iframe { + border-radius: 8px; + + } + + + `} +` diff --git a/src/routes/Home/index.tsx b/src/routes/Home/index.tsx index 473aa778ff..da30a54f79 100644 --- a/src/routes/Home/index.tsx +++ b/src/routes/Home/index.tsx @@ -2,11 +2,13 @@ import { ReactElement } from 'react' import Page from 'src/components/layout/Page' import { Box } from '@material-ui/core' import Dashboard from 'src/components/Dashboard' +import CustomizableDashboard from 'src/components/Dashboard/CustomizableDashboard' const Home = (): ReactElement => { return ( + diff --git a/src/widgets/ClaimTokenWidget.tsx b/src/widgets/ClaimTokenWidget.tsx new file mode 100644 index 0000000000..9965ec5b52 --- /dev/null +++ b/src/widgets/ClaimTokenWidget.tsx @@ -0,0 +1,59 @@ +import { ReactElement } from 'react' +import styled from 'styled-components' +import { Button } from '@gnosis.pm/safe-react-components' +import { Card } from '@gnosis.pm/safe-react-components' + +import { black500, extraLargeFontSize, largeFontSize } from 'src/theme/variables' +import { SafeWidgetComponentProps } from 'src/components/Dashboard/SafeWidget/SafeWidget' + +function ClaimTokenWidget({ data, widget }: SafeWidgetComponentProps): ReactElement { + const { iconUrl, tokenSymbol } = widget.widgetProps as any + const { totalAmount } = data || {} + return ( + +
+ +
+ + {totalAmount} {tokenSymbol} + +
+ +
+
+ ) +} + +export default ClaimTokenWidget + +const WidgetCard = styled(Card)` + height: 100%; + margin-top: 0; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + + text-align: center; + color: ${black500}; + font-size: ${largeFontSize}; +` + +export const AmountLabel = styled.p` + color: ${black500}; + font-size: ${extraLargeFontSize}; + margin-top: 0; + margin-bottom: 8px; +` + +export const TokenLogo = styled.img` + height: 60px; + margin-bottom: 8px; +` diff --git a/src/widgets/GasPriceWidget.tsx b/src/widgets/GasPriceWidget.tsx new file mode 100644 index 0000000000..9fdd95db92 --- /dev/null +++ b/src/widgets/GasPriceWidget.tsx @@ -0,0 +1,68 @@ +import styled from 'styled-components' +import { Card } from '@gnosis.pm/safe-react-components' +import LocalGasStationIcon from '@material-ui/icons/LocalGasStation' + +import { black400, black500, extraLargeFontSize, largeFontSize, mediumFontSize } from 'src/theme/variables' +import { ReactElement } from 'react' +import { SafeWidgetComponentProps } from 'src/components/Dashboard/SafeWidget/SafeWidget' + +function GasPriceWidget({ data, isLoading }: SafeWidgetComponentProps): ReactElement { + const result = data?.result || {} + const { suggestBaseFee, ProposeGasPrice, SafeGasPrice, FastGasPrice } = result + + return ( + + Gas Price + {isLoading ? ( + 'Loading...' + ) : ( + <> + + {~~suggestBaseFee} Gwei + + + Low: {SafeGasPrice} | Avg: {ProposeGasPrice} | High: {FastGasPrice} + + + )} + + ) +} + +export default GasPriceWidget + +const WidgetCard = styled(Card)` + height: 100%; + margin-top: 0; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + + text-align: center; + color: ${black500}; + font-size: ${largeFontSize}; +` + +const WidgetTitle = styled.h2` + color: ${black500}; + font-size: ${largeFontSize}; + margin: 0; +` + +const GasPriceLabel = styled.div` + display: flex; + justify-content: center; + font-size: ${extraLargeFontSize}; + margin: 8px 0; +` + +const GasStationIcon = styled(LocalGasStationIcon)` + margin-right: 4px; +` + +const GasPriceInfoLabel = styled.p` + color: ${black400}; + font-size: ${mediumFontSize}; + margin: 0; +` From 52f908a7d4fff5ff27691fd6da1f1c9c81f66512 Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Thu, 21 Jul 2022 11:21:15 +0200 Subject: [PATCH 2/8] added Overview Safe Component --- .../Dashboard/CustomizableDashboard.tsx | 72 +++++---- .../Dashboard/SafeWidget/SafeWidget.tsx | 14 +- src/components/Dashboard/styled.tsx | 1 - src/widgets/ClaimTokenWidget.tsx | 2 +- src/widgets/GasPriceWidget.tsx | 4 +- src/widgets/OverviewSafeWidget.tsx | 151 ++++++++++++++++++ 6 files changed, 209 insertions(+), 35 deletions(-) create mode 100644 src/widgets/OverviewSafeWidget.tsx diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx index 616b13c7ca..f554f0c263 100644 --- a/src/components/Dashboard/CustomizableDashboard.tsx +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -1,4 +1,4 @@ -import { ReactElement } from 'react' +import { ReactElement, useState } from 'react' import styled from 'styled-components' import SettingsIcon from '@material-ui/icons/Settings' import { useMediaQuery, IconButton } from '@material-ui/core' @@ -10,17 +10,30 @@ const COLUMN_CELL_SIZE = 50 // pixels const ROW_CELL_SIZE = 50 // pixels const WIDGET_GAP = 5 // pixels +const overviewSafeWidget: WidgetType = { + widgetId: 0, + widgetType: 'overviewSafeWidget', + md: { + columnCells: 10, + rowCells: 5, + }, + xs: { + columnCells: 6, + rowCells: 5, + }, +} + const gasPriceWidget: WidgetType = { widgetId: 1, widgetType: 'gasPriceWidget', widgetEndointUrl: 'https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=W7N7ISIDY1JFPYUI2D2HWVMMD3RF88QCCD', pollingTime: 14000, - desktopSize: { + md: { columnCells: 4, rowCells: 2, }, - mobileSize: { + xs: { columnCells: 4, rowCells: 2, }, @@ -30,11 +43,11 @@ const claimCowTokens: WidgetType = { widgetId: 2, widgetType: 'claimTokensWidget', widgetEndointUrl: 'http://localhost:3001/api', - desktopSize: { + md: { columnCells: 5, rowCells: 4, }, - mobileSize: { + xs: { columnCells: 4, rowCells: 4, }, @@ -47,14 +60,14 @@ const claimCowTokens: WidgetType = { } const claimSafeTokens: WidgetType = { - widgetId: 2, + widgetId: 3, widgetType: 'claimTokensWidget', widgetEndointUrl: 'http://localhost:3002/api', - desktopSize: { + md: { columnCells: 5, rowCells: 4, }, - mobileSize: { + xs: { columnCells: 4, rowCells: 4, }, @@ -68,74 +81,78 @@ const claimSafeTokens: WidgetType = { } const cowSwapWidget: WidgetType = { - widgetId: 3, + widgetId: 4, widgetType: 'iframe', - widgetIframeUrl: 'https://cowswap.exchange/', - desktopSize: { + widgetIframeUrl: 'https://cowswap.exchange/?widget=1', + + md: { columnCells: 8, - rowCells: 16, + rowCells: 17, }, - mobileSize: { + xs: { columnCells: 6, rowCells: 10, }, } const uniSwapWidget: WidgetType = { - widgetId: 3, + widgetId: 5, widgetType: 'iframe', widgetIframeUrl: 'https://app.uniswap.org', - desktopSize: { + md: { columnCells: 7, - rowCells: 10, + rowCells: 9, }, - mobileSize: { + xs: { columnCells: 6, rowCells: 9, }, } const rampWidget: WidgetType = { - widgetId: 3, + widgetId: 6, widgetType: 'iframe', widgetIframeUrl: 'https://apps.gnosis-safe.io/ramp-network', - desktopSize: { + md: { columnCells: 7, - rowCells: 12, + rowCells: 11, }, - mobileSize: { + xs: { columnCells: 7, rowCells: 10, }, } const widgets: WidgetType[] = [ - gasPriceWidget, + overviewSafeWidget, gasPriceWidget, claimCowTokens, + claimSafeTokens, cowSwapWidget, - gasPriceWidget, - gasPriceWidget, uniSwapWidget, - claimSafeTokens, rampWidget, ] const CustomizableDashboard = (): ReactElement => { + const [editMode, setEditMode] = useState(false) + const isMobile = useMediaQuery('(max-width:600px)') + console.log('Edit mode: ', editMode) + return ( customizable Dashboard POC - + {/* TODO: Add tooltip */} + setEditMode(true)}> {widgets.map((widget) => { - const { rowCells, columnCells } = isMobile ? widget.mobileSize : widget.desktopSize + const { rowCells, columnCells } = isMobile ? widget.xs : widget.md return ( @@ -182,5 +199,4 @@ const DashboardItem = styled.div<{ columnCells: WidgetCellType; rowCells: Widget grid-row: span ${({ rowCells }) => rowCells * (ROW_CELL_SIZE / WIDGET_GAP) + 2}; margin: ${WIDGET_GAP}px ${WIDGET_GAP}px; - outline: 2px dashed green; ` diff --git a/src/components/Dashboard/SafeWidget/SafeWidget.tsx b/src/components/Dashboard/SafeWidget/SafeWidget.tsx index 82c7c932a1..6527717748 100644 --- a/src/components/Dashboard/SafeWidget/SafeWidget.tsx +++ b/src/components/Dashboard/SafeWidget/SafeWidget.tsx @@ -9,6 +9,8 @@ import { currentSafeLoaded, currentSafeWithNames } from 'src/logic/safe/store/se import GasPriceWidget from 'src/widgets/GasPriceWidget' import ClaimTokenWidget from 'src/widgets/ClaimTokenWidget' import AppFrame from 'src/routes/safe/components/Apps/components/AppFrame' +import { nftLoadedSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors' +import OverviewSafeWidget from 'src/widgets/OverviewSafeWidget' type SafeWidgetProps = { widget: WidgetType @@ -19,18 +21,18 @@ export type WidgetCellType = DesktopCellsType | MobileCellsType // TODO: REFINE SIZE TYPES // width: 50px, 150px, 200px ... 500px // heigh: 10px, 20px, ... 990px -export type DesktopCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 +export type DesktopCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 export type MobileCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 // TODO: Create different types for iframeWidgetType & componentWidgetType export type WidgetType = { widgetId: number widgetType: string - desktopSize: { + md: { columnCells: DesktopCellsType rowCells: DesktopCellsType } - mobileSize: { + xs: { columnCells: MobileCellsType rowCells: MobileCellsType } @@ -50,6 +52,7 @@ export type SafeWidgetComponentProps = { } const availableWidgets = { + overviewSafeWidget: OverviewSafeWidget, gasPriceWidget: GasPriceWidget, claimTokensWidget: ClaimTokenWidget, iframe: AppFrame, @@ -66,17 +69,22 @@ const SafeWidget = ({ widget }: SafeWidgetProps): ReactElement => { const chainId = useSelector(currentChainId) const { shortName } = getChainById(chainId) const loaded = useSelector(currentSafeLoaded) + const nftTokens = useSelector(nftTokensSelector) + const nftLoaded = useSelector(nftLoadedSelector) + const safeInfo = { address, name, owners, threshold, balances, + nftTokens, chain: { chainId, shortName, }, loaded, + nftLoaded, } const callEndpoint = useCallback(async () => { diff --git a/src/components/Dashboard/styled.tsx b/src/components/Dashboard/styled.tsx index 5a608b76f3..b26fbe6f0d 100644 --- a/src/components/Dashboard/styled.tsx +++ b/src/components/Dashboard/styled.tsx @@ -7,7 +7,6 @@ import { xs, lg, black500, extraLargeFontSize, largeFontSize } from 'src/theme/v export const WidgetContainer = styled.section` display: flex; flex-direction: column; - height: 100%; ` export const DashboardTitle = styled.h1` diff --git a/src/widgets/ClaimTokenWidget.tsx b/src/widgets/ClaimTokenWidget.tsx index 9965ec5b52..a5d03bfb9d 100644 --- a/src/widgets/ClaimTokenWidget.tsx +++ b/src/widgets/ClaimTokenWidget.tsx @@ -6,7 +6,7 @@ import { Card } from '@gnosis.pm/safe-react-components' import { black500, extraLargeFontSize, largeFontSize } from 'src/theme/variables' import { SafeWidgetComponentProps } from 'src/components/Dashboard/SafeWidget/SafeWidget' -function ClaimTokenWidget({ data, widget }: SafeWidgetComponentProps): ReactElement { +const ClaimTokenWidget = ({ data, widget }: SafeWidgetComponentProps): ReactElement => { const { iconUrl, tokenSymbol } = widget.widgetProps as any const { totalAmount } = data || {} return ( diff --git a/src/widgets/GasPriceWidget.tsx b/src/widgets/GasPriceWidget.tsx index 9fdd95db92..83e09fffa4 100644 --- a/src/widgets/GasPriceWidget.tsx +++ b/src/widgets/GasPriceWidget.tsx @@ -1,12 +1,12 @@ +import { ReactElement } from 'react' import styled from 'styled-components' import { Card } from '@gnosis.pm/safe-react-components' import LocalGasStationIcon from '@material-ui/icons/LocalGasStation' import { black400, black500, extraLargeFontSize, largeFontSize, mediumFontSize } from 'src/theme/variables' -import { ReactElement } from 'react' import { SafeWidgetComponentProps } from 'src/components/Dashboard/SafeWidget/SafeWidget' -function GasPriceWidget({ data, isLoading }: SafeWidgetComponentProps): ReactElement { +const GasPriceWidget = ({ data, isLoading }: SafeWidgetComponentProps): ReactElement => { const result = data?.result || {} const { suggestBaseFee, ProposeGasPrice, SafeGasPrice, FastGasPrice } = result diff --git a/src/widgets/OverviewSafeWidget.tsx b/src/widgets/OverviewSafeWidget.tsx new file mode 100644 index 0000000000..b6c13c5fd1 --- /dev/null +++ b/src/widgets/OverviewSafeWidget.tsx @@ -0,0 +1,151 @@ +import { ReactElement, useMemo } from 'react' +import { Skeleton } from '@material-ui/lab' +import styled from 'styled-components' +import { Link } from 'react-router-dom' +import { Text, Identicon } from '@gnosis.pm/safe-react-components' +import { Box, Grid } from '@material-ui/core' + +import { SafeWidgetComponentProps } from 'src/components/Dashboard/SafeWidget/SafeWidget' +import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' +import Threshold from 'src/components/AppLayout/Sidebar/Threshold' +import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' +import { Card, WidgetBody, WidgetContainer } from 'src/components/Dashboard/styled' +import Button from 'src/components/layout/Button' +import { md, lg } from 'src/theme/variables' +import { generateSafeRoute, SAFE_ROUTES } from 'src/routes/routes' + +const OverviewSafeWidget = ({ safeInfo }: SafeWidgetComponentProps): ReactElement => { + const { loaded, name, address, chain, owners, threshold, balances, nftTokens, nftLoaded } = safeInfo + const { shortName } = chain + + const assetsLink = generateSafeRoute(SAFE_ROUTES.ASSETS_BALANCES, { safeAddress: address, shortName }) + const nftsLink = generateSafeRoute(SAFE_ROUTES.ASSETS_BALANCES_COLLECTIBLES, { safeAddress: address, shortName }) + + // Native token is always returned even when its balance is 0 + const tokenCount = useMemo(() => balances.filter((token) => token.tokenBalance !== '0').length, [balances]) + + return ( + + + {!loaded ? ( + SkeletonOverview + ) : ( + + + + + + + + + + {name} + + + + + + + + + + + + + + Tokens + + {tokenCount} + + + + + + + NFTs + + {nftTokens && {nftLoaded ? nftTokens.length : ValueSkeleton}} + + + + + + + + + + + + + )} + + + ) +} + +export default OverviewSafeWidget + +const ValueSkeleton = + +const IdenticonContainer = styled.div` + position: relative; + margin-bottom: ${md}; +` + +const StyledText = styled(Text)` + margin-top: 8px; + font-size: 24px; + font-weight: bold; +` + +const StyledLink = styled(Link)` + text-decoration: none; +` + +const NetworkLabelContainer = styled.div` + position: absolute; + top: ${lg}; + right: ${lg}; + + & span { + bottom: auto; + } +` + +const SkeletonOverview = ( + + + + + + + + + + + + + + + + + + + + + + Tokens + + {ValueSkeleton} + + + + NFTs + + {ValueSkeleton} + + + +) From 20d7e72072cc4ca7a6f384a1de47a3ae4a8c15b4 Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Tue, 26 Jul 2022 17:20:45 +0200 Subject: [PATCH 3/8] Added first approach of customizable dashboard --- package.json | 1 + .../Dashboard/CustomizableDashboard.tsx | 239 ++++++------------ .../Dashboard/SafeWidget/SafeWidget.tsx | 97 ++++--- src/index.tsx | 3 + src/logic/hooks/useSafeWidgets.tsx | 140 ++++++++++ src/widgets/ClaimTokenWidget.tsx | 7 +- src/widgets/GasPriceWidget.tsx | 7 +- yarn.lock | 46 ++++ 8 files changed, 337 insertions(+), 203 deletions(-) create mode 100644 src/logic/hooks/useSafeWidgets.tsx diff --git a/package.json b/package.json index 9d3291c88a..6d3ca83085 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "react-dom": "17.0.2", "react-final-form": "^6.5.3", "react-final-form-listeners": "^1.0.2", + "react-grid-layout": "^1.3.4", "react-gtm-module": "^2.0.11", "react-hook-form": "^7.29.0", "react-intersection-observer": "^8.32.0", diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx index f554f0c263..f5aad27b4c 100644 --- a/src/components/Dashboard/CustomizableDashboard.tsx +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -1,166 +1,100 @@ import { ReactElement, useState } from 'react' import styled from 'styled-components' import SettingsIcon from '@material-ui/icons/Settings' -import { useMediaQuery, IconButton } from '@material-ui/core' +import { IconButton } from '@material-ui/core' +import RGL, { WidthProvider } from 'react-grid-layout' +import { Tooltip, Button, Card } from '@gnosis.pm/safe-react-components' -import { black500, extraLargeFontSize } from 'src/theme/variables' -import SafeWidget, { WidgetCellType, WidgetType } from './SafeWidget/SafeWidget' - -const COLUMN_CELL_SIZE = 50 // pixels -const ROW_CELL_SIZE = 50 // pixels -const WIDGET_GAP = 5 // pixels - -const overviewSafeWidget: WidgetType = { - widgetId: 0, - widgetType: 'overviewSafeWidget', - md: { - columnCells: 10, - rowCells: 5, - }, - xs: { - columnCells: 6, - rowCells: 5, - }, -} - -const gasPriceWidget: WidgetType = { - widgetId: 1, - widgetType: 'gasPriceWidget', - widgetEndointUrl: - 'https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=W7N7ISIDY1JFPYUI2D2HWVMMD3RF88QCCD', - pollingTime: 14000, - md: { - columnCells: 4, - rowCells: 2, - }, - xs: { - columnCells: 4, - rowCells: 2, - }, -} - -const claimCowTokens: WidgetType = { - widgetId: 2, - widgetType: 'claimTokensWidget', - widgetEndointUrl: 'http://localhost:3001/api', - md: { - columnCells: 5, - rowCells: 4, - }, - xs: { - columnCells: 4, - rowCells: 4, - }, - widgetProps: { - iconUrl: 'https://cowswap.exchange/static/media/cow_v2.00b93700.svg', - tokenName: 'CoW Protocol Token', - tokenSymbol: 'COW', - tokenAddress: '0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB', - }, -} - -const claimSafeTokens: WidgetType = { - widgetId: 3, - widgetType: 'claimTokensWidget', - widgetEndointUrl: 'http://localhost:3002/api', - md: { - columnCells: 5, - rowCells: 4, - }, - xs: { - columnCells: 4, - rowCells: 4, - }, - widgetProps: { - iconUrl: - 'https://safe-transaction-assets.gnosis-safe.io/tokens/logos/0x5aFE3855358E112B5647B952709E6165e1c1eEEe.png', - tokenName: 'Safe Token', - tokenSymbol: 'SAFE', - tokenAddress: '0x5aFE3855358E112B5647B952709E6165e1c1eEEe', - }, -} - -const cowSwapWidget: WidgetType = { - widgetId: 4, - widgetType: 'iframe', - widgetIframeUrl: 'https://cowswap.exchange/?widget=1', - - md: { - columnCells: 8, - rowCells: 17, - }, - xs: { - columnCells: 6, - rowCells: 10, - }, -} +const ReactGridLayout = WidthProvider(RGL) -const uniSwapWidget: WidgetType = { - widgetId: 5, - widgetType: 'iframe', - widgetIframeUrl: 'https://app.uniswap.org', - md: { - columnCells: 7, - rowCells: 9, - }, - xs: { - columnCells: 6, - rowCells: 9, - }, -} - -const rampWidget: WidgetType = { - widgetId: 6, - widgetType: 'iframe', - widgetIframeUrl: 'https://apps.gnosis-safe.io/ramp-network', - md: { - columnCells: 7, - rowCells: 11, - }, - xs: { - columnCells: 7, - rowCells: 10, - }, -} - -const widgets: WidgetType[] = [ - overviewSafeWidget, - gasPriceWidget, - claimCowTokens, - claimSafeTokens, - cowSwapWidget, - uniSwapWidget, - rampWidget, -] +import { black500, extraLargeFontSize } from 'src/theme/variables' +import SafeWidget, { ROW_HEIGHT } from './SafeWidget/SafeWidget' +import useSafeWidgets from 'src/logic/hooks/useSafeWidgets' const CustomizableDashboard = (): ReactElement => { - const [editMode, setEditMode] = useState(false) - - const isMobile = useMediaQuery('(max-width:600px)') - - console.log('Edit mode: ', editMode) + const { widgets, updateWidgets } = useSafeWidgets() + + const [isEditMode, setIsEditMode] = useState(false) + const [editedWidgets, setEditedWidgets] = useState(widgets) + + const onSaveEditDashboard = () => { + updateWidgets(editedWidgets) + setIsEditMode(false) + } + + const onCancelEditDashboard = () => { + setEditedWidgets([...widgets]) + setIsEditMode(false) + } + + const onLayoutChange = (layouts) => { + const newWidgets = editedWidgets.map((currentWidget) => { + const layout = layouts.find(({ i }) => i === currentWidget.widgetId) + return { + ...currentWidget, + widgetLayout: { + row: layout.y, + column: layout.x, + width: layout.w, + height: layout.h, + }, + } + }) + + setEditedWidgets(newWidgets) + } return ( customizable Dashboard POC - {/* TODO: Add tooltip */} - setEditMode(true)}> - - - - + {!isEditMode && ( + + setIsEditMode(true)}> + + + + )} + + {isEditMode && ( + <> + Widget Catalog + + Save + + + Cancel + + + )} + ({ + i: widget.widgetId, + x: widget.widgetLayout.column, + y: widget.widgetLayout.row, + w: widget.widgetLayout.width, + h: widget.widgetLayout.height, + }))} + isDraggable={isEditMode} + isResizable={isEditMode} + > {widgets.map((widget) => { - const { rowCells, columnCells } = isMobile ? widget.xs : widget.md - return ( - - - +
+ +
) })} -
+
) } @@ -188,15 +122,10 @@ export const DashboardTitle = styled.h1` font-size: ${extraLargeFontSize}; ` -const DashboardGrid = styled.div` - display: grid; - justify-content: center; - grid-template-columns: repeat(auto-fit, ${WIDGET_GAP}px); - grid-auto-rows: ${WIDGET_GAP}px; +const WidgetCatalog = styled(Card)` + margin: 8px; ` -const DashboardItem = styled.div<{ columnCells: WidgetCellType; rowCells: WidgetCellType }>` - grid-column: span ${({ columnCells }) => columnCells * (COLUMN_CELL_SIZE / WIDGET_GAP) + 2}; - grid-row: span ${({ rowCells }) => rowCells * (ROW_CELL_SIZE / WIDGET_GAP) + 2}; - margin: ${WIDGET_GAP}px ${WIDGET_GAP}px; +const StyledBtn = styled(Button)` + margin-left: 8px; ` diff --git a/src/components/Dashboard/SafeWidget/SafeWidget.tsx b/src/components/Dashboard/SafeWidget/SafeWidget.tsx index 6527717748..f6b03f8c48 100644 --- a/src/components/Dashboard/SafeWidget/SafeWidget.tsx +++ b/src/components/Dashboard/SafeWidget/SafeWidget.tsx @@ -2,6 +2,8 @@ import axios from 'axios' import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' +import { Tooltip, Card } from '@gnosis.pm/safe-react-components' +import DragIndicatorIcon from '@material-ui/icons/DragIndicator' import { getChainById } from 'src/config' import { currentChainId } from 'src/logic/config/store/selectors' @@ -11,31 +13,28 @@ import ClaimTokenWidget from 'src/widgets/ClaimTokenWidget' import AppFrame from 'src/routes/safe/components/Apps/components/AppFrame' import { nftLoadedSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors' import OverviewSafeWidget from 'src/widgets/OverviewSafeWidget' +import { black300 } from 'src/theme/variables' type SafeWidgetProps = { widget: WidgetType + isEditWidgetEnabled: boolean } -export type WidgetCellType = DesktopCellsType | MobileCellsType +export const ROW_HEIGHT = 10 -// TODO: REFINE SIZE TYPES -// width: 50px, 150px, 200px ... 500px -// heigh: 10px, 20px, ... 990px -export type DesktopCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 -export type MobileCellsType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 +// TODO: REFINE SIZE TYPES (add min width and height) +type WidgetLayout = { + column: number + row: number + width: number + height: number +} // TODO: Create different types for iframeWidgetType & componentWidgetType export type WidgetType = { - widgetId: number + widgetId: string widgetType: string - md: { - columnCells: DesktopCellsType - rowCells: DesktopCellsType - } - xs: { - columnCells: MobileCellsType - rowCells: MobileCellsType - } + widgetLayout: WidgetLayout widgetIframeUrl?: string widgetEndointUrl?: string pollingTime?: number @@ -58,7 +57,7 @@ const availableWidgets = { iframe: AppFrame, } -const SafeWidget = ({ widget }: SafeWidgetProps): ReactElement => { +const SafeWidget = ({ widget, isEditWidgetEnabled }: SafeWidgetProps): ReactElement => { const { widgetType, widgetIframeUrl, widgetEndointUrl, pollingTime } = widget const [data, setData] = useState({}) @@ -137,43 +136,65 @@ const SafeWidget = ({ widget }: SafeWidgetProps): ReactElement => { const WidgetComponent = useMemo(() => availableWidgets[widgetType], [widgetType]) return ( - - - + + {isEditWidgetEnabled && ( + + + + + + )} + + + + ) } export default SafeWidget -const Wrapper = styled.div<{ widgetType: string }>` +const WidgetCard = styled(Card)` height: 100%; - width: 100%; + margin-top: 0; + padding: 0; + position: relative; +` +const WidgetHeader = styled.div` + position: absolute; + padding: 8px; + cursor: grab; + z-index: 1000; +` + +const DragAndDropIndicatorIcon = styled(DragIndicatorIcon)` + color: ${black300}; +` + +const WidgetBody = styled.div<{ widgetType: string }>` + height: 100%; ${({ widgetType }) => widgetType === 'iframe' && ` - padding: 0; + padding: 0; - && > div { + && > div { margin: 0; height: 100%; - } + } - && > div > div { + && > div > div { border-radius: 8px; - } + } - && > div > div > iframe { + && > div > div > iframe { border-radius: 8px; - - } - - - `} + } +`} ` diff --git a/src/index.tsx b/src/index.tsx index de06e550a3..92e4f4cb27 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,6 +6,9 @@ import Root from 'src/components/Root' import { SENTRY_DSN } from './utils/constants' import { disableMMAutoRefreshWarning } from './utils/mm_warnings' +import 'react-grid-layout/css/styles.css' +import 'react-resizable/css/styles.css' + disableMMAutoRefreshWarning() BigNumber.set({ EXPONENTIAL_AT: [-7, 255] }) diff --git a/src/logic/hooks/useSafeWidgets.tsx b/src/logic/hooks/useSafeWidgets.tsx new file mode 100644 index 0000000000..572e99d5ee --- /dev/null +++ b/src/logic/hooks/useSafeWidgets.tsx @@ -0,0 +1,140 @@ +import { useCallback, useEffect, useState } from 'react' + +import { WidgetType } from 'src/components/Dashboard/SafeWidget/SafeWidget' +import { loadFromStorage, saveToStorage } from 'src/utils/storage' + +const overviewSafeWidget: WidgetType = { + widgetId: '0', + widgetType: 'overviewSafeWidget', + widgetLayout: { + column: 0, // x + row: 0, // y + width: 6, // w + height: 12, // h + }, +} + +const gasPriceWidget: WidgetType = { + widgetId: '1', + widgetType: 'gasPriceWidget', + widgetEndointUrl: + 'https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=W7N7ISIDY1JFPYUI2D2HWVMMD3RF88QCCD', + pollingTime: 14000, + widgetLayout: { + column: 8, // x + row: 0, // y + width: 2, // w + height: 6, // h + }, +} + +const claimCowTokens: WidgetType = { + widgetId: '2', + widgetType: 'claimTokensWidget', + widgetEndointUrl: 'http://localhost:3001/api', + widgetLayout: { + column: 6, // x + row: 0, // y + width: 2, // w + height: 9, // h + }, + widgetProps: { + iconUrl: 'https://cowswap.exchange/static/media/cow_v2.00b93700.svg', + tokenName: 'CoW Protocol Token', + tokenSymbol: 'COW', + tokenAddress: '0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB', + }, +} + +const claimSafeTokens: WidgetType = { + widgetId: '3', + widgetType: 'claimTokensWidget', + widgetEndointUrl: 'http://localhost:3002/api', + widgetLayout: { + column: 10, // x + row: 0, // y + width: 2, // w + height: 9, // h + }, + widgetProps: { + iconUrl: + 'https://safe-transaction-assets.gnosis-safe.io/tokens/logos/0x5aFE3855358E112B5647B952709E6165e1c1eEEe.png', + tokenName: 'Safe Token', + tokenSymbol: 'SAFE', + tokenAddress: '0x5aFE3855358E112B5647B952709E6165e1c1eEEe', + }, +} + +const cowSwapWidget: WidgetType = { + widgetId: '4', + widgetType: 'iframe', + widgetIframeUrl: 'https://cowswap.exchange/?widget=1', + widgetLayout: { + row: 12, // y + column: 0, // x + width: 4, // w + height: 43, // h + }, +} + +const uniSwapWidget: WidgetType = { + widgetId: '5', + widgetType: 'iframe', + widgetIframeUrl: 'https://app.uniswap.org', + widgetLayout: { + row: 12, // y + column: 4, // x + width: 4, // w + height: 24, // h + }, +} + +const rampWidget: WidgetType = { + widgetId: '6', + widgetType: 'iframe', + widgetIframeUrl: 'https://apps.gnosis-safe.io/ramp-network', + widgetLayout: { + row: 12, // y + column: 8, // x + width: 4, // w + height: 30, // h + }, +} + +const defaultWidgets: WidgetType[] = [ + overviewSafeWidget, + gasPriceWidget, + claimCowTokens, + claimSafeTokens, + cowSwapWidget, + uniSwapWidget, + rampWidget, +] + +const SAFE_WIDGETS = 'SAFE_WIDGETS' + +type ReturnType = { + widgets: WidgetType[] + updateWidgets: (newWidgets: WidgetType[]) => void +} + +const useSafeWidgets = (): ReturnType => { + const [widgets, setWidgets] = useState(defaultWidgets) + + useEffect(() => { + const safeWidgets = loadFromStorage(SAFE_WIDGETS) || defaultWidgets + setWidgets([...safeWidgets]) + }, []) + + const updateWidgets = useCallback((newWidgets: WidgetType[]) => { + setWidgets([...newWidgets]) + saveToStorage(SAFE_WIDGETS, newWidgets) + }, []) + + return { + widgets, + updateWidgets, + } +} + +export default useSafeWidgets diff --git a/src/widgets/ClaimTokenWidget.tsx b/src/widgets/ClaimTokenWidget.tsx index a5d03bfb9d..b6458d27d6 100644 --- a/src/widgets/ClaimTokenWidget.tsx +++ b/src/widgets/ClaimTokenWidget.tsx @@ -1,7 +1,6 @@ import { ReactElement } from 'react' import styled from 'styled-components' import { Button } from '@gnosis.pm/safe-react-components' -import { Card } from '@gnosis.pm/safe-react-components' import { black500, extraLargeFontSize, largeFontSize } from 'src/theme/variables' import { SafeWidgetComponentProps } from 'src/components/Dashboard/SafeWidget/SafeWidget' @@ -33,13 +32,11 @@ const ClaimTokenWidget = ({ data, widget }: SafeWidgetComponentProps): ReactElem export default ClaimTokenWidget -const WidgetCard = styled(Card)` - height: 100%; - margin-top: 0; - padding: 0; +const WidgetCard = styled.div` display: flex; flex-direction: column; justify-content: center; + height: 100%; text-align: center; color: ${black500}; diff --git a/src/widgets/GasPriceWidget.tsx b/src/widgets/GasPriceWidget.tsx index 83e09fffa4..3c4f60b0ac 100644 --- a/src/widgets/GasPriceWidget.tsx +++ b/src/widgets/GasPriceWidget.tsx @@ -1,6 +1,5 @@ import { ReactElement } from 'react' import styled from 'styled-components' -import { Card } from '@gnosis.pm/safe-react-components' import LocalGasStationIcon from '@material-ui/icons/LocalGasStation' import { black400, black500, extraLargeFontSize, largeFontSize, mediumFontSize } from 'src/theme/variables' @@ -31,13 +30,11 @@ const GasPriceWidget = ({ data, isLoading }: SafeWidgetComponentProps): ReactEle export default GasPriceWidget -const WidgetCard = styled(Card)` - height: 100%; - margin-top: 0; - padding: 0; +const WidgetCard = styled.div` display: flex; flex-direction: column; justify-content: center; + height: 100%; text-align: center; color: ${black500}; diff --git a/yarn.lock b/yarn.lock index 7d2eb912e2..89ee50b311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5842,6 +5842,11 @@ clsx@^1.0.4, clsx@^1.1.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -11839,6 +11844,11 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= +lodash.isequal@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -14372,6 +14382,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +prop-types@15.x, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -14753,6 +14772,14 @@ react-dom@17.0.2, react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-draggable@^4.0.0, react-draggable@^4.0.3: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c" + integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g== + dependencies: + clsx "^1.1.1" + prop-types "^15.8.1" + react-error-boundary@^3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.3.tgz#276bfa05de8ac17b863587c9e0647522c25e2a0b" @@ -14784,6 +14811,17 @@ react-final-form@^6.5.3: dependencies: "@babel/runtime" "^7.15.4" +react-grid-layout@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff" + integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw== + dependencies: + clsx "^1.1.1" + lodash.isequal "^4.0.0" + prop-types "^15.8.1" + react-draggable "^4.0.0" + react-resizable "^3.0.4" + react-gtm-module@^2.0.11: version "2.0.11" resolved "https://registry.yarnpkg.com/react-gtm-module/-/react-gtm-module-2.0.11.tgz#14484dac8257acd93614e347c32da9c5ac524206" @@ -14868,6 +14906,14 @@ react-refresh@^0.8.3: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== +react-resizable@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.4.tgz#aa20108eff28c52c6fddaa49abfbef8abf5e581b" + integrity sha512-StnwmiESiamNzdRHbSSvA65b0ZQJ7eVQpPusrSmcpyGKzC0gojhtO62xxH6YOBmepk9dQTBi9yxidL3W4s3EBA== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-router-dom@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" From fd1fab477e2cfd98b14b78ed211426e1a837d1b3 Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Tue, 26 Jul 2022 17:39:25 +0200 Subject: [PATCH 4/8] Added responsive dashboard --- .../Dashboard/CustomizableDashboard.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx index f5aad27b4c..e2de876f42 100644 --- a/src/components/Dashboard/CustomizableDashboard.tsx +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -2,10 +2,10 @@ import { ReactElement, useState } from 'react' import styled from 'styled-components' import SettingsIcon from '@material-ui/icons/Settings' import { IconButton } from '@material-ui/core' -import RGL, { WidthProvider } from 'react-grid-layout' +import { Responsive, WidthProvider } from 'react-grid-layout' import { Tooltip, Button, Card } from '@gnosis.pm/safe-react-components' -const ReactGridLayout = WidthProvider(RGL) +const ResponsiveGridLayout = WidthProvider(Responsive) import { black500, extraLargeFontSize } from 'src/theme/variables' import SafeWidget, { ROW_HEIGHT } from './SafeWidget/SafeWidget' @@ -68,22 +68,22 @@ const CustomizableDashboard = (): ReactElement => { )} - ({ - i: widget.widgetId, - x: widget.widgetLayout.column, - y: widget.widgetLayout.row, - w: widget.widgetLayout.width, - h: widget.widgetLayout.height, - }))} + layouts={{ + lg: widgets.map((widget) => ({ + i: widget.widgetId, + x: widget.widgetLayout.column, + y: widget.widgetLayout.row, + w: widget.widgetLayout.width, + h: widget.widgetLayout.height, + })), + }} isDraggable={isEditMode} isResizable={isEditMode} > @@ -94,7 +94,7 @@ const CustomizableDashboard = (): ReactElement => { ) })} - +
) } From eecd3c44e03bd153c74b0d1c1bf5154032beccfd Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Tue, 26 Jul 2022 17:46:53 +0200 Subject: [PATCH 5/8] Added more units --- src/components/Dashboard/SafeWidget/SafeWidget.tsx | 5 ++++- src/logic/hooks/useSafeWidgets.tsx | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/Dashboard/SafeWidget/SafeWidget.tsx b/src/components/Dashboard/SafeWidget/SafeWidget.tsx index f6b03f8c48..865b94f1e5 100644 --- a/src/components/Dashboard/SafeWidget/SafeWidget.tsx +++ b/src/components/Dashboard/SafeWidget/SafeWidget.tsx @@ -22,12 +22,15 @@ type SafeWidgetProps = { export const ROW_HEIGHT = 10 -// TODO: REFINE SIZE TYPES (add min width and height) type WidgetLayout = { column: number row: number width: number height: number + minW?: number + maxW?: number + minH?: number + maxH?: number } // TODO: Create different types for iframeWidgetType & componentWidgetType diff --git a/src/logic/hooks/useSafeWidgets.tsx b/src/logic/hooks/useSafeWidgets.tsx index 572e99d5ee..47d7bef125 100644 --- a/src/logic/hooks/useSafeWidgets.tsx +++ b/src/logic/hooks/useSafeWidgets.tsx @@ -10,7 +10,9 @@ const overviewSafeWidget: WidgetType = { column: 0, // x row: 0, // y width: 6, // w + minW: 6, height: 12, // h + minH: 12, }, } @@ -25,6 +27,8 @@ const gasPriceWidget: WidgetType = { row: 0, // y width: 2, // w height: 6, // h + minW: 2, + minH: 6, }, } @@ -37,6 +41,8 @@ const claimCowTokens: WidgetType = { row: 0, // y width: 2, // w height: 9, // h + minW: 2, + minH: 9, }, widgetProps: { iconUrl: 'https://cowswap.exchange/static/media/cow_v2.00b93700.svg', @@ -55,6 +61,8 @@ const claimSafeTokens: WidgetType = { row: 0, // y width: 2, // w height: 9, // h + minW: 2, + minH: 9, }, widgetProps: { iconUrl: @@ -74,6 +82,7 @@ const cowSwapWidget: WidgetType = { column: 0, // x width: 4, // w height: 43, // h + minH: 43, }, } @@ -86,6 +95,7 @@ const uniSwapWidget: WidgetType = { column: 4, // x width: 4, // w height: 24, // h + minH: 24, }, } @@ -98,6 +108,7 @@ const rampWidget: WidgetType = { column: 8, // x width: 4, // w height: 30, // h + minH: 30, }, } From aaaff1f0c7f2d2a08effe0b1a575a44a341dc537 Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Tue, 26 Jul 2022 18:00:42 +0200 Subject: [PATCH 6/8] Added min and max size --- .../Dashboard/CustomizableDashboard.tsx | 11 +++++++++- .../Dashboard/SafeWidget/SafeWidget.tsx | 10 +++++---- src/logic/hooks/useSafeWidgets.tsx | 22 +++++++++---------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx index e2de876f42..bd159b549d 100644 --- a/src/components/Dashboard/CustomizableDashboard.tsx +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -36,7 +36,11 @@ const CustomizableDashboard = (): ReactElement => { row: layout.y, column: layout.x, width: layout.w, + minWidth: layout.minW, + maxWidth: layout.maxW, + minHeight: layout.minH, height: layout.h, + maxHeight: layout.maxH, }, } }) @@ -74,14 +78,19 @@ const CustomizableDashboard = (): ReactElement => { onLayoutChange={onLayoutChange} key={isEditMode} rowHeight={ROW_HEIGHT} - compactType={'horizontal'} + // compactType={'horizontal'} + verticalCompact={false} layouts={{ lg: widgets.map((widget) => ({ i: widget.widgetId, x: widget.widgetLayout.column, y: widget.widgetLayout.row, + minW: widget.widgetLayout.minWidth, w: widget.widgetLayout.width, + maxW: widget.widgetLayout.maxWidth, + minH: widget.widgetLayout.minHeight, h: widget.widgetLayout.height, + maxH: widget.widgetLayout.maxHeight, })), }} isDraggable={isEditMode} diff --git a/src/components/Dashboard/SafeWidget/SafeWidget.tsx b/src/components/Dashboard/SafeWidget/SafeWidget.tsx index 865b94f1e5..ea4fd00206 100644 --- a/src/components/Dashboard/SafeWidget/SafeWidget.tsx +++ b/src/components/Dashboard/SafeWidget/SafeWidget.tsx @@ -25,12 +25,14 @@ export const ROW_HEIGHT = 10 type WidgetLayout = { column: number row: number + + minWidth?: number width: number + maxWidth?: number + + minHeight?: number height: number - minW?: number - maxW?: number - minH?: number - maxH?: number + maxHeight?: number } // TODO: Create different types for iframeWidgetType & componentWidgetType diff --git a/src/logic/hooks/useSafeWidgets.tsx b/src/logic/hooks/useSafeWidgets.tsx index 47d7bef125..f395363d4d 100644 --- a/src/logic/hooks/useSafeWidgets.tsx +++ b/src/logic/hooks/useSafeWidgets.tsx @@ -10,9 +10,9 @@ const overviewSafeWidget: WidgetType = { column: 0, // x row: 0, // y width: 6, // w - minW: 6, + minWidth: 6, height: 12, // h - minH: 12, + minHeight: 12, }, } @@ -27,8 +27,8 @@ const gasPriceWidget: WidgetType = { row: 0, // y width: 2, // w height: 6, // h - minW: 2, - minH: 6, + minWidth: 2, + minHeight: 6, }, } @@ -41,8 +41,8 @@ const claimCowTokens: WidgetType = { row: 0, // y width: 2, // w height: 9, // h - minW: 2, - minH: 9, + minWidth: 2, + minHeight: 9, }, widgetProps: { iconUrl: 'https://cowswap.exchange/static/media/cow_v2.00b93700.svg', @@ -61,8 +61,8 @@ const claimSafeTokens: WidgetType = { row: 0, // y width: 2, // w height: 9, // h - minW: 2, - minH: 9, + minWidth: 2, + minHeight: 9, }, widgetProps: { iconUrl: @@ -82,7 +82,7 @@ const cowSwapWidget: WidgetType = { column: 0, // x width: 4, // w height: 43, // h - minH: 43, + minHeight: 43, }, } @@ -95,7 +95,7 @@ const uniSwapWidget: WidgetType = { column: 4, // x width: 4, // w height: 24, // h - minH: 24, + minHeight: 24, }, } @@ -108,7 +108,7 @@ const rampWidget: WidgetType = { column: 8, // x width: 4, // w height: 30, // h - minH: 30, + minHeight: 30, }, } From 21707d52e7b6a07389770483cc704022b0c64bfd Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Tue, 26 Jul 2022 18:05:40 +0200 Subject: [PATCH 7/8] set compactType to vertical --- src/components/Dashboard/CustomizableDashboard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx index bd159b549d..0f42f73b35 100644 --- a/src/components/Dashboard/CustomizableDashboard.tsx +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -78,8 +78,7 @@ const CustomizableDashboard = (): ReactElement => { onLayoutChange={onLayoutChange} key={isEditMode} rowHeight={ROW_HEIGHT} - // compactType={'horizontal'} - verticalCompact={false} + compactType={'vertical'} layouts={{ lg: widgets.map((widget) => ({ i: widget.widgetId, From 5e82c0b7da23cf916049ab82e21f506d23dbed4b Mon Sep 17 00:00:00 2001 From: Daniel Somoza Date: Wed, 27 Jul 2022 17:54:57 +0200 Subject: [PATCH 8/8] removed vertical compact --- src/components/Dashboard/CustomizableDashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dashboard/CustomizableDashboard.tsx b/src/components/Dashboard/CustomizableDashboard.tsx index 0f42f73b35..03ded3ed68 100644 --- a/src/components/Dashboard/CustomizableDashboard.tsx +++ b/src/components/Dashboard/CustomizableDashboard.tsx @@ -78,7 +78,7 @@ const CustomizableDashboard = (): ReactElement => { onLayoutChange={onLayoutChange} key={isEditMode} rowHeight={ROW_HEIGHT} - compactType={'vertical'} + compactType={null} layouts={{ lg: widgets.map((widget) => ({ i: widget.widgetId,