From bb21941692115421228df0cefa0132aa8f4004ad Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 2 May 2022 16:49:52 +0200 Subject: [PATCH] feat: mining dashboard (#108) * Set up the Mining dashboard layout * Add handling mining node states * Add default mining box style and config * Fix typo and mining header styling --- Cargo.lock | 52 ++-- .../launchpad_v2/src/components/Box/index.tsx | 14 +- .../launchpad_v2/src/components/Box/types.ts | 1 + .../src/components/Button/styles.ts | 2 +- .../src/components/CoinsList/index.tsx | 49 ++++ .../src/components/CoinsList/styles.ts | 14 ++ .../src/components/CoinsList/types.ts | 11 + .../src/components/NodeBox/NodeBox.test.tsx | 19 ++ .../src/components/NodeBox/index.tsx | 66 +++++ .../src/components/NodeBox/styles.ts | 20 ++ .../src/components/NodeBox/types.ts | 19 ++ .../src/components/Switch/styles.ts | 10 +- .../launchpad_v2/src/components/Tag/index.tsx | 10 +- .../launchpad_v2/src/components/Tag/types.ts | 2 +- .../Dashboard/DashboardContainer/index.tsx | 13 +- .../Dashboard/DashboardContainer/styles.ts | 3 +- .../MiningContainer/MiningBox/index.tsx | 236 ++++++++++++++++++ .../MiningContainer/MiningBox/styles.ts | 9 + .../MiningContainer/MiningBox/types.ts | 17 ++ .../MiningContainer/MiningBoxMerged/index.tsx | 7 + .../MiningContainer/MiningBoxTari/index.tsx | 7 + .../MiningContainer/MiningHeaderTip/index.tsx | 30 +++ .../MiningContainer/MiningHeaderTip/styles.ts | 7 + .../MiningViewActions/index.tsx | 26 ++ .../src/containers/MiningContainer/index.tsx | 60 +++-- .../src/containers/MiningContainer/styles.ts | 16 ++ applications/launchpad_v2/src/custom.d.ts | 4 + .../src/layouts/MainLayout/index.tsx | 4 +- .../launchpad_v2/src/locales/common.ts | 7 + .../launchpad_v2/src/locales/mining.ts | 19 ++ applications/launchpad_v2/src/store/index.ts | 6 +- .../launchpad_v2/src/store/mining/index.ts | 95 +++++++ .../src/store/mining/selectors.ts | 23 ++ .../launchpad_v2/src/store/mining/thunks.ts | 28 +++ .../launchpad_v2/src/store/mining/types.ts | 57 +++++ .../launchpad_v2/src/styles/themes/dark.ts | 4 + .../launchpad_v2/src/styles/themes/light.ts | 6 +- applications/launchpad_v2/src/types/.keep | 0 .../launchpad_v2/src/types/general.ts | 14 ++ 39 files changed, 931 insertions(+), 56 deletions(-) create mode 100644 applications/launchpad_v2/src/components/CoinsList/index.tsx create mode 100644 applications/launchpad_v2/src/components/CoinsList/styles.ts create mode 100644 applications/launchpad_v2/src/components/CoinsList/types.ts create mode 100644 applications/launchpad_v2/src/components/NodeBox/NodeBox.test.tsx create mode 100644 applications/launchpad_v2/src/components/NodeBox/index.tsx create mode 100644 applications/launchpad_v2/src/components/NodeBox/styles.ts create mode 100644 applications/launchpad_v2/src/components/NodeBox/types.ts create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningBox/index.tsx create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningBox/styles.ts create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningBox/types.ts create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningBoxMerged/index.tsx create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningBoxTari/index.tsx create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/index.tsx create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/styles.ts create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/MiningViewActions/index.tsx create mode 100644 applications/launchpad_v2/src/containers/MiningContainer/styles.ts create mode 100644 applications/launchpad_v2/src/store/mining/index.ts create mode 100644 applications/launchpad_v2/src/store/mining/selectors.ts create mode 100644 applications/launchpad_v2/src/store/mining/thunks.ts create mode 100644 applications/launchpad_v2/src/store/mining/types.ts delete mode 100644 applications/launchpad_v2/src/types/.keep create mode 100644 applications/launchpad_v2/src/types/general.ts diff --git a/Cargo.lock b/Cargo.lock index 89d473a590..1c343eeb68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "TariLaunchpad" +version = "0.1.0" +dependencies = [ + "bollard", + "config", + "derivative", + "env_logger 0.9.0", + "futures 0.3.21", + "log", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "strum 0.23.0", + "strum_macros 0.23.1", + "tari_app_utilities", + "tari_common", + "tari_comms", + "tauri", + "tauri-build", + "thiserror", + "tokio 1.17.0", + "tor-hash-passwd", +] + [[package]] name = "adler" version = "1.0.2" @@ -153,32 +179,6 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "bollard", - "config", - "derivative", - "env_logger 0.9.0", - "futures 0.3.21", - "log", - "rand 0.8.5", - "regex", - "serde", - "serde_json", - "strum 0.23.0", - "strum_macros 0.23.1", - "tari_app_utilities", - "tari_common", - "tari_comms", - "tauri", - "tauri-build", - "thiserror", - "tokio 1.17.0", - "tor-hash-passwd", -] - [[package]] name = "arc-swap" version = "0.4.8" diff --git a/applications/launchpad_v2/src/components/Box/index.tsx b/applications/launchpad_v2/src/components/Box/index.tsx index 512e4b7c5a..3692d53686 100644 --- a/applications/launchpad_v2/src/components/Box/index.tsx +++ b/applications/launchpad_v2/src/components/Box/index.tsx @@ -15,7 +15,13 @@ import { BoxProps } from './types' * @prop {string} end - color on gradient end * @prop {number} rotation - gradient rotation in degress (45 by default) */ -const Box = ({ children, gradient, border, style: inlineStyle }: BoxProps) => { +const Box = ({ + children, + gradient, + border, + style: inlineStyle, + testId = 'box-cmp', +}: BoxProps) => { const style = { border: border === false ? 'none' : undefined, background: @@ -29,7 +35,11 @@ const Box = ({ children, gradient, border, style: inlineStyle }: BoxProps) => { ...inlineStyle, } - return {children} + return ( + + {children} + + ) } export default Box diff --git a/applications/launchpad_v2/src/components/Box/types.ts b/applications/launchpad_v2/src/components/Box/types.ts index 55cb5aedbf..97256051ee 100644 --- a/applications/launchpad_v2/src/components/Box/types.ts +++ b/applications/launchpad_v2/src/components/Box/types.ts @@ -11,4 +11,5 @@ export type BoxProps = { border?: boolean style?: CSSProperties gradient?: Gradient + testId?: string } diff --git a/applications/launchpad_v2/src/components/Button/styles.ts b/applications/launchpad_v2/src/components/Button/styles.ts index a77af2e0ce..1bb323314f 100644 --- a/applications/launchpad_v2/src/components/Button/styles.ts +++ b/applications/launchpad_v2/src/components/Button/styles.ts @@ -45,7 +45,7 @@ export const StyledButton = styled.button< return `1px solid ${theme.accent}` }}; box-shadow: none; - padding: ${({ theme }) => theme.spacingVertical()} + padding: ${({ theme }) => theme.spacingVertical(0.6)} ${({ theme }) => theme.spacingHorizontal()}; cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; background: ${getButtonBackgroundColor}; diff --git a/applications/launchpad_v2/src/components/CoinsList/index.tsx b/applications/launchpad_v2/src/components/CoinsList/index.tsx new file mode 100644 index 0000000000..e532313e09 --- /dev/null +++ b/applications/launchpad_v2/src/components/CoinsList/index.tsx @@ -0,0 +1,49 @@ +import Loading from '../Loading' +import Text from '../Text' + +import { CoinsListItem, StyledCoinsList } from './styles' +import { CoinsListProps } from './types' + +/** + * Render the list of coins with amount. + * @param {CoinProps[]} coins - the list of coins + * @param {string} [color = 'inherit'] - the text color + * + * @typedef {CoinProps} + * @param {string} amount - the amount + * @param {string} unit - the unit, ie. xtr + * @param {string} [suffixText] - the latter text after the amount and unit + * @param {boolean} [loading] - is value being loaded + */ +const CoinsList = ({ coins, color }: CoinsListProps) => { + return ( + + {coins.map((c, idx) => ( + + {c.loading ? ( + + ) : null} + {c.amount} + + {c.unit} + + {c.suffixText ? ( + + {c.suffixText} + + ) : null} + + ))} + + ) +} + +export default CoinsList diff --git a/applications/launchpad_v2/src/components/CoinsList/styles.ts b/applications/launchpad_v2/src/components/CoinsList/styles.ts new file mode 100644 index 0000000000..7676662e4b --- /dev/null +++ b/applications/launchpad_v2/src/components/CoinsList/styles.ts @@ -0,0 +1,14 @@ +import styled from 'styled-components' + +export const StyledCoinsList = styled.ul<{ color?: string }>` + color: ${({ color }) => (color ? color : 'inherit')}; + list-style: none; + padding-left: 0; + margin-top: 0; +` + +export const CoinsListItem = styled.li<{ loading?: boolean }>` + opacity: ${({ loading }) => (loading ? 0.64 : 1)}; + display: flex; + align-items: center; +` diff --git a/applications/launchpad_v2/src/components/CoinsList/types.ts b/applications/launchpad_v2/src/components/CoinsList/types.ts new file mode 100644 index 0000000000..64fa2a4cc6 --- /dev/null +++ b/applications/launchpad_v2/src/components/CoinsList/types.ts @@ -0,0 +1,11 @@ +export interface CoinProps { + amount: string + unit: string + suffixText?: string + loading?: boolean +} + +export interface CoinsListProps { + coins: CoinProps[] + color?: string +} diff --git a/applications/launchpad_v2/src/components/NodeBox/NodeBox.test.tsx b/applications/launchpad_v2/src/components/NodeBox/NodeBox.test.tsx new file mode 100644 index 0000000000..c340e32f69 --- /dev/null +++ b/applications/launchpad_v2/src/components/NodeBox/NodeBox.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from '@testing-library/react' +import { ThemeProvider } from 'styled-components' + +import themes from '../../styles/themes' + +import NodeBox from '.' + +describe('NodeBox', () => { + it('should render without crashing', async () => { + render( + + + , + ) + + const el = screen.getByTestId('node-box-cmp') + expect(el).toBeInTheDocument() + }) +}) diff --git a/applications/launchpad_v2/src/components/NodeBox/index.tsx b/applications/launchpad_v2/src/components/NodeBox/index.tsx new file mode 100644 index 0000000000..edc94f6131 --- /dev/null +++ b/applications/launchpad_v2/src/components/NodeBox/index.tsx @@ -0,0 +1,66 @@ +import Box from '../Box' +import Tag from '../Tag' +import Text from '../Text' + +import { BoxHeader, BoxContent, NodeBoxPlacholder } from './styles' +import { NodeBoxContentPlaceholderProps, NodeBoxProps } from './types' + +/** + * The advanced Box component handling: + * - custom title + * - header tag + * - background depending on the status prop + * + * Used for the UI representation of the Node (Docker container) as a Box component. + * + * @param {string} [title] - the box heading + * @param {{ text: string; type?: TagType }} [tag = 'inactive'] - the status of the box/node + * @param {CSSWithSpring} [style] - the box style + * @param {CSSWithSpring} [titleStyle] - the title style + * @param {CSSWithSpring} [contentStyle] - the content style + * @param {ReactNode} [children] - the box heading + */ +const NodeBox = ({ + title, + tag, + style, + titleStyle, + contentStyle, + children, +}: NodeBoxProps) => { + return ( + + + {tag ? ( + + {tag.text} + + ) : null} + + {title ? ( + + {title} + + ) : null} + {children} + + ) +} + +/** + * Simple placholder container for the node box that provides default spacing and layout. + * @param {string | ReactNode} children - the content + */ +export const NodeBoxContentPlaceholder = ({ + children, +}: NodeBoxContentPlaceholderProps) => { + let content = children + + if (typeof children === 'string') { + content = {children} + } + + return {content} +} + +export default NodeBox diff --git a/applications/launchpad_v2/src/components/NodeBox/styles.ts b/applications/launchpad_v2/src/components/NodeBox/styles.ts new file mode 100644 index 0000000000..9e9b888589 --- /dev/null +++ b/applications/launchpad_v2/src/components/NodeBox/styles.ts @@ -0,0 +1,20 @@ +import styled from 'styled-components' + +export const BoxHeader = styled.div` + height: 36px; +` + +export const BoxContent = styled.div` + padding-top: ${({ theme }) => theme.spacingVertical(1)}; + padding-bottom: ${({ theme }) => theme.spacingVertical(1)}; + min-height: 136px; + display: flex; + flex-direction: column; +` + +export const NodeBoxPlacholder = styled.div` + display: flex; + flex: 1; + padding-top: ${({ theme }) => theme.spacingVertical(1)}; + padding-bottom: ${({ theme }) => theme.spacingVertical(1)}; +` diff --git a/applications/launchpad_v2/src/components/NodeBox/types.ts b/applications/launchpad_v2/src/components/NodeBox/types.ts new file mode 100644 index 0000000000..b6a3e5dc44 --- /dev/null +++ b/applications/launchpad_v2/src/components/NodeBox/types.ts @@ -0,0 +1,19 @@ +import { ReactNode } from 'react' +import { CSSWithSpring } from '../../types/general' +import { TagType } from '../Tag/types' + +export interface NodeBoxProps { + title?: string + tag?: { + text: string + type?: TagType + } + style?: CSSWithSpring + titleStyle?: CSSWithSpring + contentStyle?: CSSWithSpring + children?: ReactNode +} + +export interface NodeBoxContentPlaceholderProps { + children: string | ReactNode +} diff --git a/applications/launchpad_v2/src/components/Switch/styles.ts b/applications/launchpad_v2/src/components/Switch/styles.ts index c2b49dd338..962e5f0c17 100644 --- a/applications/launchpad_v2/src/components/Switch/styles.ts +++ b/applications/launchpad_v2/src/components/Switch/styles.ts @@ -11,12 +11,15 @@ export const SwitchContainer = styled.label` export const SwitchController = styled(animated.div)` height: 14px; width: 24px; - border: 1.5px solid ${colors.dark.primary}; + border: 1px solid ${colors.dark.primary}; border-radius: 6px; position: relative; box-sizing: border-box; box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.08); cursor: pointer; + -webkit-box-shadow: 0px 0px 2px -1px ${colors.dark.primary}; + -moz-box-shadow: 0px 0px 2px -1px ${colors.dark.primary}; + box-shadow: 0px 0px 2px -1px ${colors.dark.primary}; ` export const SwitchCircle = styled(animated.div)` @@ -28,7 +31,10 @@ export const SwitchCircle = styled(animated.div)` border-radius: 6px; box-sizing: border-box; background: #fff; - border: 1.5px solid ${colors.dark.primary}; + border: 1px solid ${colors.dark.primary}; + -webkit-box-shadow: 0px 0px 2px -1px ${colors.dark.primary}; + -moz-box-shadow: 0px 0px 2px -1px ${colors.dark.primary}; + box-shadow: 0px 0px 2px -1px ${colors.dark.primary}; ` export const LabelText = styled(animated.span)` diff --git a/applications/launchpad_v2/src/components/Tag/index.tsx b/applications/launchpad_v2/src/components/Tag/index.tsx index 3f83771af2..fb414de2ce 100644 --- a/applications/launchpad_v2/src/components/Tag/index.tsx +++ b/applications/launchpad_v2/src/components/Tag/index.tsx @@ -12,7 +12,7 @@ import { TagContainer, IconWrapper } from './styles' * * @prop {ReactNode} [children] - text content to display * @prop {CSSProperties} [style] - optional component styles - * @prop {'info' | 'running' | 'warning' | 'expert'} [type] - tag types to determine color settings + * @prop {'info' | 'running' | 'warning' | 'expert' | 'light'} [type] - tag types to determine color settings * @prop {ReactNode} [icon] - optional SVG icon * @prop {ReactNode} [subText] - optional additional tag text * @@ -62,6 +62,14 @@ const Tag = ({ color: 'transparent', } break + case 'light': + baseStyle = { + backgroundColor: theme.lightTag, + } + textStyle = { + color: theme.lightTagText, + } + break // info tag type is default default: baseStyle = { diff --git a/applications/launchpad_v2/src/components/Tag/types.ts b/applications/launchpad_v2/src/components/Tag/types.ts index 2e1a2e7048..0baad8435d 100644 --- a/applications/launchpad_v2/src/components/Tag/types.ts +++ b/applications/launchpad_v2/src/components/Tag/types.ts @@ -2,7 +2,7 @@ import { ReactNode } from 'react' import { CSSProperties } from 'styled-components' export type TagVariantType = 'small' | 'large' -export type TagType = 'info' | 'running' | 'warning' | 'expert' +export type TagType = 'info' | 'running' | 'warning' | 'expert' | 'light' /** * @typedef TagProps diff --git a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx index 6bb44c896c..9aeb99a2f3 100644 --- a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx +++ b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx @@ -1,4 +1,6 @@ import { useSelector } from 'react-redux' +import { CSSProperties } from 'styled-components' +import { SpringValue } from 'react-spring' import { DashboardContent, DashboardLayout } from './styles' @@ -14,7 +16,14 @@ import { selectView } from '../../../store/app/selectors' /** * Dashboard view containing three main tabs: Mining, Wallet and BaseNode */ -const DashboardContainer = () => { +const DashboardContainer = ({ + style, +}: { + style?: + | CSSProperties + | Record> + | Record> +}) => { const currentPage = useSelector(selectView) const renderPage = () => { @@ -31,7 +40,7 @@ const DashboardContainer = () => { } return ( - + {renderPage()} diff --git a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/styles.ts b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/styles.ts index e4d0f67971..91e387b47f 100644 --- a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/styles.ts +++ b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/styles.ts @@ -1,6 +1,7 @@ +import { animated } from 'react-spring' import styled from 'styled-components' -export const DashboardLayout = styled.div` +export const DashboardLayout = styled(animated.div)` display: flex; flex-direction: column; height: 100%; diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/index.tsx b/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/index.tsx new file mode 100644 index 0000000000..4d05e476a3 --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/index.tsx @@ -0,0 +1,236 @@ +import { CSSProperties, useTheme } from 'styled-components' + +import Button from '../../../components/Button' +import NodeBox, { NodeBoxContentPlaceholder } from '../../../components/NodeBox' +import { TagType } from '../../../components/Tag/types' + +import { useAppDispatch, useAppSelector } from '../../../store/hooks' + +import { actions } from '../../../store/mining' +import { + selectLastSession, + selectMiningNode, +} from '../../../store/mining/selectors' +import { + MiningNodesStatus, + MiningNodeStates, + MiningSession, +} from '../../../store/mining/types' + +import t from '../../../locales' + +import { MiningBoxProps } from './types' +import { MiningBoxContent } from './styles' +import CoinsList from '../../../components/CoinsList' + +interface Config { + title?: string + tag?: { + text: string + type?: TagType + } + boxStyle?: CSSProperties + titleStyle?: CSSProperties + contentStyle?: CSSProperties +} + +const parseLastSessionToCoins = (lastSession: MiningSession | undefined) => { + if (lastSession && lastSession.total) { + return Object.keys(lastSession.total).map(coin => ({ + unit: coin, + amount: + lastSession.total && lastSession.total[coin] + ? lastSession.total[coin] + : '0', + loading: true, + suffixText: t.mining.minedInLastSession, + })) + } + + return [] +} + +/** + * Generic component providing NodeBox-based UI, reading from global state + * and handling basic actions. + * + * The `node` param determines which record in the global mining state + * will be observed. The component will try automatically cast the found data + * to the UI. + * + * The container handles `MiningNodesStatus` states automatically, but specific states + * should be overwritten with two params: + * - `statuses` - customi UI for a given node status + * - `children` - override the content of the node box. Use this for statuses like `SETUP_REQUIRED` to provide + * details and steps how to resolve this status. + * + * The general approach is: + * 1. Create parent container for specific node (ie. Tari Mining) + * 2. Import and render this MiningBox Container with minimal config (ie. `{ node: 'tari' }`) + * 3. Add in parent container any custom logic that will evaluate the correct status. If it's needed to provide + * custom component and logic for a given status, push children component (it will override generic component and behaviour). + * + * @param {MiningNodeType} node - ie. tari, merged + * @param {Record} [statuses] - the optional config overriding specific states. + * @param {ReactNode} [children] - component overriding the generic one composed by this container for a given status./ + */ +const MiningBox = ({ node, children }: MiningBoxProps) => { + const dispatch = useAppDispatch() + const theme = useTheme() + + const nodeState: MiningNodeStates = useAppSelector(state => + selectMiningNode(state, node), + ) + + const lastSession: MiningSession | undefined = useAppSelector(state => + selectLastSession(state, node), + ) + + const coins = parseLastSessionToCoins(lastSession) + + // Is there any outgoing action, so the buttons should be disabled? + const disableActions = nodeState.pending + + const defaultConfig: Config = { + title: `${node.substring(0, 1).toUpperCase() + node.substring(1)} ${ + t.common.nouns.mining + }`, + boxStyle: { + color: theme.primary, + background: theme.background, + }, + titleStyle: { + color: theme.primary, + }, + contentStyle: { + color: theme.secondary, + }, + } + + const defaultStates: Partial<{ + [key in keyof typeof MiningNodesStatus]: Config + }> = { + UNKNOWN: {}, + SETUP_REQUIRED: { + tag: { + text: t.common.phrases.startHere, + }, + }, + BLOCKED: { + tag: { + text: t.common.phrases.actionRequired, + type: 'warning', + }, + }, + PAUSED: { + tag: { + text: t.common.adjectives.paused, + type: 'light', + }, + }, + RUNNING: { + tag: { + text: t.common.adjectives.running, + type: 'running', + }, + boxStyle: { + background: theme.tariGradient, + }, + titleStyle: { + color: theme.inverted.primary, + }, + contentStyle: { + color: theme.inverted.secondary, + }, + }, + ERROR: { + tag: { + text: t.common.nouns.problem, + type: 'warning', + }, + }, + } + + const currentState = { + ...defaultConfig, + ...defaultStates[nodeState.status], + } + + const componentForCurrentStatus = () => { + if (children) { + return children + } + + switch (nodeState.status) { + case 'UNKNOWN': + return ( + + {t.mining.placeholders.statusUnknown} + + ) + case 'SETUP_REQUIRED': + return ( + + {t.mining.placeholders.statusSetupRequired} + + ) + case 'BLOCKED': + return ( + + {t.mining.placeholders.statusBlocked} + + ) + case 'ERROR': + return ( + + {t.mining.placeholders.statusError} + + ) + case 'PAUSED': + return ( + + {coins ? : null} + + + ) + case 'RUNNING': + return ( + + {coins ? ( + + ) : null} + + + ) + } + } + + const content = componentForCurrentStatus() + + return ( + + {content} + + ) +} + +export default MiningBox diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/styles.ts b/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/styles.ts new file mode 100644 index 0000000000..47d468b12a --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/styles.ts @@ -0,0 +1,9 @@ +import styled from 'styled-components' + +export const MiningBoxContent = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + flex: 1; +` diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/types.ts b/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/types.ts new file mode 100644 index 0000000000..d3e05d4209 --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningBox/types.ts @@ -0,0 +1,17 @@ +import { ReactNode } from 'react' +import { TagType } from '../../../components/Tag/types' +import { MiningNodesStatus } from '../../../store/mining/types' +import { MiningNodeType } from '../../../types/general' + +export interface NodeBoxStatusConfig { + tag: { + text: string + type: TagType + } +} + +export interface MiningBoxProps { + node: MiningNodeType + statuses?: Record + children?: ReactNode +} diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningBoxMerged/index.tsx b/applications/launchpad_v2/src/containers/MiningContainer/MiningBoxMerged/index.tsx new file mode 100644 index 0000000000..3aba2da3a6 --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningBoxMerged/index.tsx @@ -0,0 +1,7 @@ +import MiningBox from '../MiningBox' + +const MiningBoxMerged = () => { + return +} + +export default MiningBoxMerged diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningBoxTari/index.tsx b/applications/launchpad_v2/src/containers/MiningContainer/MiningBoxTari/index.tsx new file mode 100644 index 0000000000..b2b89fcd08 --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningBoxTari/index.tsx @@ -0,0 +1,7 @@ +import MiningBox from '../MiningBox' + +const MiningBoxTari = () => { + return +} + +export default MiningBoxTari diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/index.tsx b/applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/index.tsx new file mode 100644 index 0000000000..938576e6f9 --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/index.tsx @@ -0,0 +1,30 @@ +import t from '../../../locales' + +import Button from '../../../components/Button' +import Text from '../../../components/Text' + +import SvgStar from '../../../styles/Icons/Star' +import SvgInfo1 from '../../../styles/Icons/Info1' +import { StyledMiningHeaderTip } from './styles' + +/** + * @TODO - draft - add other states + */ +const MiningHeaderTip = () => { + return ( + + + {t.mining.headerTips.oneStepAway} + + + ) +} + +export default MiningHeaderTip diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/styles.ts b/applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/styles.ts new file mode 100644 index 0000000000..16ca77e707 --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningHeaderTip/styles.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components' + +export const StyledMiningHeaderTip = styled.div` + display: flex; + margin-top: ${({ theme }) => theme.spacingVertical(3)}; + margin-bottom: ${({ theme }) => theme.spacingVertical(2.392)}; +` diff --git a/applications/launchpad_v2/src/containers/MiningContainer/MiningViewActions/index.tsx b/applications/launchpad_v2/src/containers/MiningContainer/MiningViewActions/index.tsx new file mode 100644 index 0000000000..06dbbc944c --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/MiningViewActions/index.tsx @@ -0,0 +1,26 @@ +import Button from '../../../components/Button' +import t from '../../../locales' +import SvgChart from '../../../styles/Icons/Chart' +import SvgClock from '../../../styles/Icons/Clock' +import SvgSetting2 from '../../../styles/Icons/Setting2' + +/** + * Renders set of links/actions in Mining dashboard + */ +const MiningViewActions = () => { + return ( +
+ + + +
+ ) +} + +export default MiningViewActions diff --git a/applications/launchpad_v2/src/containers/MiningContainer/index.tsx b/applications/launchpad_v2/src/containers/MiningContainer/index.tsx index c5842a5189..f4feb8b303 100644 --- a/applications/launchpad_v2/src/containers/MiningContainer/index.tsx +++ b/applications/launchpad_v2/src/containers/MiningContainer/index.tsx @@ -1,36 +1,64 @@ -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import Switch from '../../components/Switch' import Text from '../../components/Text' import SvgSun from '../../styles/Icons/Sun' import SvgMoon from '../../styles/Icons/Moon' +import MiningHeaderTip from './MiningHeaderTip' +import MiningViewActions from './MiningViewActions' + import { setTheme } from '../../store/app' import { selectTheme } from '../../store/app/selectors' +import { NodesContainer } from './styles' +import MiningBoxTari from './MiningBoxTari' +import MiningBoxMerged from './MiningBoxMerged' +import { actions } from '../../store/wallet' +import { useAppDispatch } from '../../store/hooks' + /** - * @TODO move user-facing text to i18n file when implementing + * The Mining dashboard */ - const MiningContainer = () => { - const dispatch = useDispatch() + const dispatch = useAppDispatch() const currentTheme = useSelector(selectTheme) return (
-

Mining

- + + - -
- Select theme - } - rightLabel={} - value={currentTheme === 'dark'} - onClick={v => dispatch(setTheme(v ? 'dark' : 'light'))} - /> + +
+ + +
+ Select theme + } + rightLabel={} + value={currentTheme === 'dark'} + onClick={v => dispatch(setTheme(v ? 'dark' : 'light'))} + /> +
) diff --git a/applications/launchpad_v2/src/containers/MiningContainer/styles.ts b/applications/launchpad_v2/src/containers/MiningContainer/styles.ts new file mode 100644 index 0000000000..f5dd8d3edf --- /dev/null +++ b/applications/launchpad_v2/src/containers/MiningContainer/styles.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components' + +export const NodesContainer = styled.div` + display: flex; + flex-wrap: wrap; + + & > div { + margin: ${({ theme }) => theme.spacing(0.34)}; + } + & > div:first-child { + margin-left: 0; + } + & > div:last-child { + margin-right: 0; + } +` diff --git a/applications/launchpad_v2/src/custom.d.ts b/applications/launchpad_v2/src/custom.d.ts index 77e2acb07f..63ef126e1f 100644 --- a/applications/launchpad_v2/src/custom.d.ts +++ b/applications/launchpad_v2/src/custom.d.ts @@ -43,6 +43,8 @@ declare module 'styled-components' { warningText: string expert: string expertText: string + lightTag: string + lightTagText: string placeholderText: string inverted: { @@ -65,6 +67,8 @@ declare module 'styled-components' { warningText: string expert: string expertText: string + lightTag: string + lightTagText: string } } } diff --git a/applications/launchpad_v2/src/layouts/MainLayout/index.tsx b/applications/launchpad_v2/src/layouts/MainLayout/index.tsx index 042872497b..f1d12c52f2 100644 --- a/applications/launchpad_v2/src/layouts/MainLayout/index.tsx +++ b/applications/launchpad_v2/src/layouts/MainLayout/index.tsx @@ -57,6 +57,8 @@ const MainLayout = ({ drawerViewWidth = '50%' }: MainLayoutProps) => { */ const mainContainerStyle = useSpring({ width: expertView === 'open' ? invertedExpertViewSize : '100%', + }) + const dashboardContainerStyle = useSpring({ paddingLeft: expertView === 'open' || tightSpace ? 40 : 100, paddingRight: expertView === 'open' || tightSpace ? 40 : 100, }) @@ -78,7 +80,7 @@ const MainLayout = ({ drawerViewWidth = '50%' }: MainLayoutProps) => { ...mainContainerStyle, }} > - + {/* Background overlay: */} diff --git a/applications/launchpad_v2/src/locales/common.ts b/applications/launchpad_v2/src/locales/common.ts index 7e34126c47..d27caabb6a 100644 --- a/applications/launchpad_v2/src/locales/common.ts +++ b/applications/launchpad_v2/src/locales/common.ts @@ -4,16 +4,23 @@ const translations: { [key: string]: { [key: string]: string } } = { cancel: 'Cancel', stop: 'Stop', start: 'Start', + pause: 'Pause', continue: 'Continue', }, nouns: { baseNode: 'Base Node', mining: 'Mining', + problem: 'Problem', settings: 'Settings', wallet: 'Wallet', }, adjectives: { running: 'Running', + paused: 'Paused', + }, + phrases: { + actionRequired: 'Action required', + startHere: 'Start here', }, } diff --git a/applications/launchpad_v2/src/locales/mining.ts b/applications/launchpad_v2/src/locales/mining.ts index 8a44a1d3dd..e34f6dc7bb 100644 --- a/applications/launchpad_v2/src/locales/mining.ts +++ b/applications/launchpad_v2/src/locales/mining.ts @@ -1,5 +1,24 @@ const translations = { set_up_mining_hours: 'Set up mining hours', + minedInLastSession: 'mined in last session', + headerTips: { + oneStepAway: 'You are one step away from staring mining.', + wantToKnowMore: 'Want to know more', + }, + actions: { + startMining: 'Start mining', + }, + viewActions: { + setUpMiningHours: 'Set up mining hours', + miningSettings: 'Mining settings', + statistics: 'Statistics', + }, + placeholders: { + statusUnknown: 'The node status is unknown.', + statusBlocked: 'The node cannot be started.', + statusSetupRequired: 'The node requires further configuration.', + statusError: 'The node failed.', + }, } export default translations diff --git a/applications/launchpad_v2/src/store/index.ts b/applications/launchpad_v2/src/store/index.ts index 66d0ebc324..091ec7afbd 100644 --- a/applications/launchpad_v2/src/store/index.ts +++ b/applications/launchpad_v2/src/store/index.ts @@ -1,14 +1,16 @@ import { configureStore } from '@reduxjs/toolkit' -import baseNodeReducer from './baseNode' -import walletReducer from './wallet' import appReducer from './app' import settingsReducer from './settings' +import baseNodeReducer from './baseNode' +import miningReducer from './mining' +import walletReducer from './wallet' // exported for tests export const rootReducer = { app: appReducer, baseNode: baseNodeReducer, + mining: miningReducer, wallet: walletReducer, settings: settingsReducer, } diff --git a/applications/launchpad_v2/src/store/mining/index.ts b/applications/launchpad_v2/src/store/mining/index.ts new file mode 100644 index 0000000000..edcf705d8e --- /dev/null +++ b/applications/launchpad_v2/src/store/mining/index.ts @@ -0,0 +1,95 @@ +import { createSlice } from '@reduxjs/toolkit' +import { MiningNodeType } from '../../types/general' +import { startMiningNode, stopMiningNode } from './thunks' + +import { MiningNodesStatus, MiningState } from './types' + +export const initialState: MiningState = { + tari: { + pending: false, + status: MiningNodesStatus.PAUSED, + sessions: [ + { + total: { + xtr: '1000', + }, + }, + { + total: { + xtr: '2000', + }, + }, + ], + }, + merged: { + pending: false, + status: MiningNodesStatus.PAUSED, + sessions: [ + { + total: { + xtr: '1000', + xmr: '1001', + }, + }, + { + total: { + xtr: '2000', + xmr: '2001', + }, + }, + ], + }, +} + +const miningSlice = createSlice({ + name: 'mining', + initialState, + reducers: { + setNodeStatus( + state, + { + payload, + }: { payload: { node: MiningNodeType; status: MiningNodesStatus } }, + ) { + state[payload.node].status = payload.status + }, + }, + extraReducers: builder => { + builder + .addCase(startMiningNode.pending, (state, action) => { + const node = action.meta.arg.node + if (node in state) { + state[node].pending = true + } + }) + .addCase(startMiningNode.fulfilled, (state, action) => { + const node = action.meta.arg.node + if (node in state) { + state[node].pending = false + state[node].status = MiningNodesStatus.RUNNING + } + }) + .addCase(stopMiningNode.pending, (state, action) => { + const node = action.meta.arg.node + if (node in state) { + state[node].pending = true + } + }) + .addCase(stopMiningNode.fulfilled, (state, action) => { + const node = action.meta.arg.node + if (node in state) { + state[node].pending = false + state[node].status = MiningNodesStatus.PAUSED + } + }) + }, +}) + +const { setNodeStatus } = miningSlice.actions +export const actions = { + setNodeStatus, + startMiningNode, + stopMiningNode, +} + +export default miningSlice.reducer diff --git a/applications/launchpad_v2/src/store/mining/selectors.ts b/applications/launchpad_v2/src/store/mining/selectors.ts new file mode 100644 index 0000000000..d81af79ced --- /dev/null +++ b/applications/launchpad_v2/src/store/mining/selectors.ts @@ -0,0 +1,23 @@ +import { createSelector } from '@reduxjs/toolkit' +import { MiningNodeType } from '../../types/general' + +/** + * Get Redux state of the given mining node + * @example + * const miningState = useAppSelector(state => selectMiningNode(state, 'merged')) + */ +export const selectMiningNode = createSelector( + [state => state.mining, (_, node: MiningNodeType) => node], + (miningState, node) => miningState[node], +) + +/** + * Get stats for last/current session + */ +export const selectLastSession = createSelector( + [state => state.mining, (_, node: MiningNodeType) => node], + (miningState, node) => + miningState[node].sessions && miningState[node].sessions.length > 0 + ? miningState[node].sessions[miningState[node].sessions.length - 1] + : undefined, +) diff --git a/applications/launchpad_v2/src/store/mining/thunks.ts b/applications/launchpad_v2/src/store/mining/thunks.ts new file mode 100644 index 0000000000..3142ae371a --- /dev/null +++ b/applications/launchpad_v2/src/store/mining/thunks.ts @@ -0,0 +1,28 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import { MiningNodeType } from '../../types/general' + +/** + * Start given mining node + * @prop {NodeType} node - the node name, ie. 'tari', 'merged' + * @returns {Promise} + */ +export const startMiningNode = createAsyncThunk( + 'mining/startNode', + async ({ node }) => { + console.log(`starting ${node} node`) + return await new Promise(resolve => setTimeout(resolve, 2000)) + }, +) + +/** + * Stop given mining node + * @prop {NodeType} node - the node name, ie. 'tari', 'merged' + * @returns {Promise} + */ +export const stopMiningNode = createAsyncThunk( + 'mining/stopNode', + async ({ node }) => { + console.log(`stopping ${node} node`) + return await new Promise(resolve => setTimeout(resolve, 2000)) + }, +) diff --git a/applications/launchpad_v2/src/store/mining/types.ts b/applications/launchpad_v2/src/store/mining/types.ts new file mode 100644 index 0000000000..650721a866 --- /dev/null +++ b/applications/launchpad_v2/src/store/mining/types.ts @@ -0,0 +1,57 @@ +/** + * @TODO - the list of possible statuses may change. + * If so, then MiningBox and Mining Container may need to be changed as well. + * UNKNOWN - the status of the container/node is unknown, ie. on app launch it can be default status + * SETUP_REQUIRED - node/container cannot be run because of missing configuration (merge with BLOCKED?) + * BLOCKED - node/container cannot be run because some requirement is not satisfied, ie. mining node needs base node running + * PAUSED - node/container is not running. NOTE: node and container are not necessary the same. Ie. Docker container can be live, but process running inside the container can be stopped. So maybe we should split this also into PAUSED and STOPPED? + * RUNNING - node/container is running and healthy + * ERROR - node/container is failed + */ +export enum MiningNodesStatus { + 'UNKNOWN' = 'UNKNOWN', + 'SETUP_REQUIRED' = 'SETUP_REQUIRED', + 'BLOCKED' = 'BLOCKED', + 'PAUSED' = 'PAUSED', + 'RUNNING' = 'RUNNING', + 'ERROR' = 'ERROR', +} + +export enum CodesOfTariMining { + 'MISSING_WALLET_ADDRESS', +} + +export enum CodesOfMergedMining { + 'MISSING_WALLET_ADDRESS', + 'MISSING_MONERO_ADDRESS', +} + +export interface MiningSession { + startedAt?: string // UTC timestamp + finishedAt?: string + id?: string // uuid (?) + total?: Record // i,e { xtr: 1000 bignumber (?) } + history?: { + timestamp?: string // UTC timestamp + amount?: string // bignumber (?) + chain?: string // ie. xtr, xmr aka coin/currency? + type?: string // to enum, ie. mined, earned, received, sent + }[] +} + +export interface MiningNodeState { + pending: boolean + status: MiningNodesStatus + disabledReason?: TDisabledReason + sessions?: MiningSession[] +} + +export type TariMiningNodeState = MiningNodeState +export type MergedMiningNodeState = MiningNodeState + +export type MiningNodeStates = TariMiningNodeState | MergedMiningNodeState + +export interface MiningState { + tari: TariMiningNodeState + merged: MergedMiningNodeState +} diff --git a/applications/launchpad_v2/src/styles/themes/dark.ts b/applications/launchpad_v2/src/styles/themes/dark.ts index 600cd33062..94ab46d81a 100644 --- a/applications/launchpad_v2/src/styles/themes/dark.ts +++ b/applications/launchpad_v2/src/styles/themes/dark.ts @@ -25,6 +25,8 @@ const darkTheme = { warningText: styles.colors.secondary.warningText, expert: 'rgba(147, 48, 255, 0.05)', expertText: styles.gradients.tari, + lightTag: styles.colors.light.backgroundImage, + lightTagText: styles.colors.dark.secondary, placeholderText: styles.colors.dark.placeholder, inverted: { @@ -47,6 +49,8 @@ const darkTheme = { warningText: styles.colors.secondary.warningText, expert: 'rgba(147, 48, 255, 0.05)', expertText: styles.gradients.tari, + lightTag: styles.colors.light.backgroundImage, + lightTagText: styles.colors.dark.secondary, borderColor: styles.colors.light.backgroundImage, borderColorLight: styles.colors.secondary.borderLight, controlBackground: 'transparent', diff --git a/applications/launchpad_v2/src/styles/themes/light.ts b/applications/launchpad_v2/src/styles/themes/light.ts index 8ae9711e8e..19ac3a2266 100644 --- a/applications/launchpad_v2/src/styles/themes/light.ts +++ b/applications/launchpad_v2/src/styles/themes/light.ts @@ -28,11 +28,13 @@ const lightTheme = { warningText: styles.colors.secondary.warningText, expert: 'rgba(147, 48, 255, 0.05)', expertText: styles.gradients.tari, + lightTag: styles.colors.light.backgroundImage, + lightTagText: styles.colors.dark.secondary, placeholderText: styles.colors.dark.placeholder, inverted: { primary: styles.colors.light.primary, - secondary: styles.colors.dark.secondary, + secondary: styles.colors.light.textSecondary, tertiary: styles.colors.dark.tertiary, background: styles.colors.darkMode.modalBackgroundSecondary, backgroundSecondary: styles.colors.darkMode.modalBackground, @@ -50,6 +52,8 @@ const lightTheme = { warningText: styles.colors.secondary.warningText, expert: 'rgba(147, 48, 255, 0.05)', expertText: styles.gradients.tari, + lightTag: styles.colors.light.backgroundImage, + lightTagText: styles.colors.dark.secondary, borderColor: styles.colors.secondary.borderLight, borderColorLight: styles.colors.secondary.borderLight, actionBackground: styles.colors.secondary.actionBackground, diff --git a/applications/launchpad_v2/src/types/.keep b/applications/launchpad_v2/src/types/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/applications/launchpad_v2/src/types/general.ts b/applications/launchpad_v2/src/types/general.ts new file mode 100644 index 0000000000..989db3d16d --- /dev/null +++ b/applications/launchpad_v2/src/types/general.ts @@ -0,0 +1,14 @@ +import { CSSProperties } from 'react' +import { SpringValue } from 'react-spring' + +export type CoinType = 'xtr' | ' xmr' + +export type MiningNodeType = 'tari' | 'merged' + +/** + * Style types + */ +export type CSSWithSpring = + | CSSProperties + | Record> + | Record>