Merge branch 'main' into Create-Logs

This commit is contained in:
Hristo Nedelkov 2023-12-15 13:05:32 +02:00
commit df194ee580
61 changed files with 1392 additions and 191 deletions

1
.gitignore vendored
View File

@ -36,3 +36,4 @@ dist-ssr
# vercel # vercel
/.vercel /.vercel
.vercel

View File

@ -1,8 +1,34 @@
# nimbus-gui # Nimbus GUI
A GUI for managing your [Nimbus](https://nimbus.team/) nodes. The goal of this project is to develop a management and monitoring GUI for the [Nimbus Ethereum client](https://nimbus.team).
## Deployed pages showing the project Right now, Nimbus is managed as a typical system service. It offers executables that can be launched from the command-line. It produces log output as the primary way to communicate information to the user and it's typically monitored through [Prometheus and Grafana](https://nimbus.guide/metrics-pretty-pictures.html). The user can interact with Nimbus through a standardized [REST](https://ethereum.github.io/beacon-APIs/) [APIs](https://ethereum.github.io/keymanager-APIs/) with some Nimbus-specific extensions.
Since the primary purpose of Nimbus is to enable the user to operate [Ethereum validators](https://ethereum.org/en/staking/), users typically also consult web-sites such as [beaconcha.in](https://beaconcha.in/), which provide up-to-date information about the network and the obtained rewards of each validator. The beaconcha.in web-site also offers a popular mobile application which can alert the user if their validator(s) are failing to perform their duties (which can happen if the Nimbus service is experiencing any technical issues).
At the moment, all of the above makes Nimbus accessible mostly to users with the sufficient technical skills to setup and integrate multiple software packages, often within a rented server running Linux in a remote data center.
We would like to make Nimbus much more accessible to non-technical users by developing GUI installers and GUI management and monitoring software. We have prepared a rough roadmap for this here:
https://github.com/status-im/nimbus-eth2/issues/3423
## Development Plan
The initial version of the management UI will be developed as a web application, communicating with a special service called the Logos Node Management Service.
As part of the [Logos](https://logos.co/) movement, Nimbus benefits from close ties to [Status](https://status.im/), a messanger that offers strong integration with Ethereum and also serves as a [mobile wallet](https://status.im/secure-wallet/) and a [DApp browser](https://zerion.io/blog/what-is-dapp-browser/). We can provide a simple interface for solo stakers who would be able to execute their validator deposits directly from the Status app in the future. The Nimbus management UI will be then embedded within the app and it will use the same design system as the app.
The Status UI team is currently developing the next iteration of the Status design system that will be used across its mobile and desktop products. To facilitate the future integration, we will use the same system during the development of the Nimbus GUI from the start.
## UX Designs (WIP)
Initial designs for the Nimbus management UI are being developed here:
https://www.figma.com/file/kUO8PyQCo89SyvCn3pFmNS/Nodes-Nimbus---New-Design-System?type=design&node-id=3%3A188588&t=npvkylewM1T5GUHG-1
Please note that all of the graphics are currently placeholders as the final artwords are still being prepared. The layout of the screens is likely to resemble the final design, although the content and the available functionality on the web-pages is still under review.
## Live Demos
We have a Storybook up at https://nimbus-gui.github.io/nimbus-gui/ which shows We have a Storybook up at https://nimbus-gui.github.io/nimbus-gui/ which shows
the components of the project. We also have a deployed version of the GUI up at the components of the project. We also have a deployed version of the GUI up at
@ -10,13 +36,13 @@ https://nimbus-gui.vercel.app/ which shows the GUI as it currently looks in the
`main` branch of the `main` branch of the
[`nimbus-gui/nimbus-gui`](https://github.com/nimbus-gui/nimbus-gui) repository. [`nimbus-gui/nimbus-gui`](https://github.com/nimbus-gui/nimbus-gui) repository.
## Development and running the project yourself ## How to Contribute
### Dependencies ### Install all dependencies
Run `yarn` in the root directory of the project in order to install dependencies. Run `yarn` in the root directory of the project in order to install dependencies.
### Running a development server ### Run a development server
If you want to run a development server to see what the GUI looks like you can If you want to run a development server to see what the GUI looks like you can
run the following command: run the following command:
@ -28,7 +54,7 @@ yarn dev
This will start the server on port 5173 and you can open https://localhost:5173 This will start the server on port 5173 and you can open https://localhost:5173
in order to see the page. in order to see the page.
### Running storybook locally ### Launch Storybook locally
If you want to run the Storybook locally you can simply run `yarn storybook` in If you want to run the Storybook locally you can simply run `yarn storybook` in
the root of the project. This is useful if you want to contribute a component the root of the project. This is useful if you want to contribute a component

View File

@ -16,12 +16,12 @@ import PairDevice from './pages/PairDevice/PairDevice'
import PinnedNotification from './components/General/PinnedNottification' import PinnedNotification from './components/General/PinnedNottification'
import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage' import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding' import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding'
import { ethereumRopsten, wcV2InitOptions, apiKey } from './constants'
import Dashboard from './pages/Dashboard/Dashboard' import Dashboard from './pages/Dashboard/Dashboard'
import ConnectExistingInstance from './pages/ConnectExistingInstance/ConnectExistingInstance' import ConnectExistingInstance from './pages/ConnectExistingInstance/ConnectExistingInstance'
import './App.css'
import ValidatorManagement from './pages/ValidatorManagement/ValidatorManagement' import ValidatorManagement from './pages/ValidatorManagement/ValidatorManagement'
import LogsPage from './pages/LogsPage/LogsPage' import LogsPage from './pages/LogsPage/LogsPage'
import { ethereumRopsten, wcV2InitOptions, apiKey } from './constants'
import './App.css'
const injected = injectedModule() const injected = injectedModule()
const walletConnect = walletConnectModule(wcV2InitOptions) const walletConnect = walletConnectModule(wcV2InitOptions)

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import AddCardsContainer from './AddCardsContainer' import AddCardsContainer from './AddCardsContainer'
const meta = { const meta = {
title: 'Dashboard/AddCardsContainer', title: 'General/AddCardsContainer',
component: AddCardsContainer, component: AddCardsContainer,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
@ -15,5 +15,13 @@ export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
export const Default: Story = { export const Default: Story = {
args: {}, args: {
cardsAmount: 2,
},
}
export const WithoutCards: Story = {
args: {
cardsAmount: 0,
},
} }

View File

@ -4,14 +4,19 @@ import AddCard from './AddCard'
import DashboardCardWrapper from '../../../pages/Dashboard/DashboardCardWrapper' import DashboardCardWrapper from '../../../pages/Dashboard/DashboardCardWrapper'
import { getHeightPercentages } from '../../../utilities' import { getHeightPercentages } from '../../../utilities'
const AddCardsContainer = () => { type AddCardsContainerProps = {
const cards = 2 cardsAmount: number
}
const AddCardsContainer = ({ cardsAmount }: AddCardsContainerProps) => {
return ( return (
<DashboardCardWrapper padding="0" minWidth="50px"> <DashboardCardWrapper padding="0" minWidth="50px">
<YStack height={'100%'}> <YStack height={'100%'}>
{Array.from({ length: cards }).map((_, index) => ( {Array.from({ length: cardsAmount }).map((_, index) => (
<AddCard key={index} style={{ padding: '56px', height: getHeightPercentages(cards) }} /> <AddCard
key={index}
style={{ padding: '40px', height: getHeightPercentages(cardsAmount) }}
/>
))} ))}
</YStack> </YStack>
</DashboardCardWrapper> </DashboardCardWrapper>

View File

@ -31,13 +31,13 @@ const CreateAvatar = () => {
}, [emojiRef]) }, [emojiRef])
return ( return (
<YStack my={16}> <YStack>
<XStack space> <XStack>
<LabelInputField labelText="Device Name" placeholderText="Stake and chips" /> <LabelInputField labelText="Device Name" placeholderText="Stake and chips" />
</XStack> </XStack>
<XStack my={10} justifyContent={'space-between'}> <XStack space={'$3'} justifyContent={'space-between'}>
<YStack mr={60}> <YStack>
<Text size={13} weight="regular" color={'#647084'}> <Text size={13} weight="semibold" color={'#647084'}>
Device Avatar Device Avatar
</Text> </Text>
<XStack my={10} alignItems={'end'}> <XStack my={10} alignItems={'end'}>
@ -64,8 +64,8 @@ const CreateAvatar = () => {
</div> </div>
</XStack> </XStack>
</YStack> </YStack>
<YStack flexWrap="wrap" width="80%"> <YStack flexWrap="wrap" width="73%">
<Text size={13} weight="regular" color={'#647084'}> <Text size={13} weight="semibold" color={'#647084'}>
Highlight Color Highlight Color
</Text> </Text>
<ColorPicker setChosenColor={setChosenColor} /> <ColorPicker setChosenColor={setChosenColor} />

View File

@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react'
import Header from './Header'
const meta = {
title: 'General/Header',
component: Header,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Header>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
selectedTag: 'pair',
},
}
export const CreateTag: Story = {
args: {
selectedTag: 'create',
},
}
export const ConnectTag: Story = {
args: {
selectedTag: 'connect',
},
}

View File

@ -1,4 +1,3 @@
import { XStack } from 'tamagui'
import NimbusLogo from '../Logos/NimbusLogo' import NimbusLogo from '../Logos/NimbusLogo'
import TagContainer from './TagContainer' import TagContainer from './TagContainer'
@ -8,10 +7,18 @@ type HeaderProps = {
const Header = ({ selectedTag }: HeaderProps) => { const Header = ({ selectedTag }: HeaderProps) => {
return ( return (
<XStack justifyContent="space-between" py={'25px'} mt={'4.4rem'}> <div
style={{
display: 'flex',
justifyContent: 'space-between',
paddingBottom: '25px',
marginTop: '4.4rem',
}}
className="header-container"
>
<NimbusLogo /> <NimbusLogo />
<TagContainer selectedTag={selectedTag} /> <TagContainer selectedTag={selectedTag} />
</XStack> </div>
) )
} }

View File

@ -10,7 +10,7 @@ type LabelInputProps = {
const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => { const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => {
return ( return (
<Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}> <Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}>
<Text size={13} weight="regular" color={'#647084'}> <Text size={13} weight="semibold" color={'#647084'}>
{labelText} {labelText}
</Text> </Text>
<div className="input-container"> <div className="input-container">

View File

@ -26,3 +26,15 @@
color: #0d1625; color: #0d1625;
font-size: 13px; font-size: 13px;
} }
@media (max-width: 1000px) {
.quick-start-bar {
top: 0;
width: 85%;
margin: 0 20px;
}
.quick-start-bar ul li {
font-size: 11px;
}
}

View File

@ -1,8 +1,8 @@
import './QuickStartBar.css' import styles from './QuickStartBar.module.css'
const QuickStartBar = () => { const QuickStartBar = () => {
return ( return (
<nav className="quick-start-bar"> <nav className={styles['quick-start-bar']}>
<span> <span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -2,44 +2,23 @@ import { Tabs } from '@status-im/components'
import { Stack } from 'tamagui' import { Stack } from 'tamagui'
import ValidatorsList from './ValidatorsList' import ValidatorsList from './ValidatorsList'
import { useMemo } from 'react' import { VALIDATOR_TABS_RIGHT_SIDEBAR } from '../../../../constants'
const ValidatorsTabs = () => { const ValidatorsTabs = () => {
const VALIDATOR_TABS = useMemo(
() => [
{
label: 'Active',
value: 'active',
children: <ValidatorsList />,
},
{
label: 'Pending',
value: 'pending',
children: <ValidatorsList />,
},
{
label: 'Inactive',
value: 'inactive',
children: <ValidatorsList />,
},
],
[],
)
return ( return (
<Tabs defaultValue="active"> <Tabs defaultValue={VALIDATOR_TABS_RIGHT_SIDEBAR[0]}>
<Stack style={{ cursor: 'pointer', width: 'fit-content' }}> <Stack style={{ cursor: 'pointer', width: 'fit-content' }}>
<Tabs.List size={32}> <Tabs.List size={32}>
{VALIDATOR_TABS.map(tab => ( {VALIDATOR_TABS_RIGHT_SIDEBAR.map(tab => (
<Tabs.Trigger key={tab.value} type="default" value={tab.value}> <Tabs.Trigger key={tab} type="default" value={tab}>
{tab.label} {tab}
</Tabs.Trigger> </Tabs.Trigger>
))} ))}
</Tabs.List> </Tabs.List>
</Stack> </Stack>
{VALIDATOR_TABS.map(tab => ( {VALIDATOR_TABS_RIGHT_SIDEBAR.map(tab => (
<Tabs.Content key={tab.value} value={tab.value} style={{ marginTop: '8px' }}> <Tabs.Content key={tab} value={tab} style={{ marginTop: '8px' }}>
{tab.children} <ValidatorsList />
</Tabs.Content> </Tabs.Content>
))} ))}
</Tabs> </Tabs>

View File

@ -1,15 +1,15 @@
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
import KeyGenerationSyncCard from './KeyGenerationSyncCard' import SyncStatusCard from './SyncStatusCard'
const meta = { const meta = {
title: 'ValidatorOnboarding/KeyGenerationSyncCard', title: 'General/SyncStatusCard',
component: KeyGenerationSyncCard, component: SyncStatusCard,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
}, },
tags: ['autodocs'], tags: ['autodocs'],
} satisfies Meta<typeof KeyGenerationSyncCard> } satisfies Meta<typeof SyncStatusCard>
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>

View File

@ -2,18 +2,18 @@ import { Stack, XStack, YStack } from 'tamagui'
import { InfoBadgeIcon } from '@status-im/icons' import { InfoBadgeIcon } from '@status-im/icons'
import { Text } from '@status-im/components' import { Text } from '@status-im/components'
import StandardGauge from '../../../../components/Charts/StandardGauge' import StandardGauge from '../Charts/StandardGauge'
import BorderBox from '../../../../components/General/BorderBox' import BorderBox from './BorderBox'
import { formatNumbersWithComa } from '../../../../utilities' import { formatNumbersWithComa } from '../../utilities'
type KeyGenerationSyncCardProps = { type SyncStatusCardProps = {
synced: number synced: number
total: number total: number
title: string title: string
color: string color: string
} }
const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyncCardProps) => { const SyncStatusCard = ({ synced, total, title, color }: SyncStatusCardProps) => {
return ( return (
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}> <BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
<XStack space={'$2'} alignItems="center"> <XStack space={'$2'} alignItems="center">
@ -54,4 +54,4 @@ const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyn
) )
} }
export default KeyGenerationSyncCard export default SyncStatusCard

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import TitleLogo from './TitleLogo' import TitleLogo from './TitleLogo'
const meta = { const meta = {
title: 'Dashboard/TitleLogo', title: 'General/TitleLogo',
component: TitleLogo, component: TitleLogo,
parameters: { parameters: {
layout: 'centered', layout: 'centered',
@ -15,5 +15,11 @@ export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
export const Default: Story = { export const Default: Story = {
args: {
subtitle: 'Node Management Dashboard',
},
}
export const WithoutSubtitle: Story = {
args: {}, args: {},
} }

View File

@ -1,7 +1,11 @@
import { Avatar, Text } from '@status-im/components' import { Avatar, Text } from '@status-im/components'
import { Stack, XStack, YStack } from 'tamagui' import { Stack, XStack, YStack } from 'tamagui'
const TitleLogo = () => { type TitleLogoProps = {
subtitle?: string
}
const TitleLogo = ({ subtitle }: TitleLogoProps) => {
return ( return (
<XStack space={'$2'}> <XStack space={'$2'}>
<Stack style={{ marginTop: '3px' }}> <Stack style={{ marginTop: '3px' }}>
@ -18,7 +22,7 @@ const TitleLogo = () => {
Nimbus Nimbus
</Text> </Text>
<Text size={19} color="#647084"> <Text size={19} color="#647084">
Node Management Dashboard {subtitle}
</Text> </Text>
</YStack> </YStack>
</XStack> </XStack>

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ValidatorProfile from './ValidatorProfile'
const meta = {
title: 'General/ValidatorProfile',
component: ValidatorProfile,
tags: ['autodocs'],
} satisfies Meta<typeof ValidatorProfile>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
number: 1,
address: 'zQ3asdf9d4Gs0',
},
}

View File

@ -0,0 +1,33 @@
import { Avatar, Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui'
import { getFormattedValidatorAddress } from '../../utilities'
type ValidatorProfileProps = {
number: number
address: string
}
const ValidatorProfile = ({ number, address }: ValidatorProfileProps) => {
return (
<XStack space={'$2'}>
<Avatar
type="user"
size={32}
src="/icons/validator-request.svg"
name={number.toString()}
indicator="online"
/>
<YStack>
<Text size={15} weight={'semibold'}>
Validator {number}
</Text>
<Text size={13} color="#647084">
{getFormattedValidatorAddress(address)}
</Text>
</YStack>
</XStack>
)
}
export default ValidatorProfile

View File

@ -1,8 +1,9 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import './layout.css'
import NimbusLogoMark from '../Logos/NimbusLogoMark'
import { useTheme } from 'tamagui' import { useTheme } from 'tamagui'
import NimbusLogoMark from '../Logos/NimbusLogoMark'
import './layout.css'
type PageWrapperShadowProps = { type PageWrapperShadowProps = {
breadcrumbBar?: ReactNode breadcrumbBar?: ReactNode
rightImageSrc?: string rightImageSrc?: string
@ -28,7 +29,6 @@ const PageWrapperShadow = ({
<div className="container-inner">{children}</div> <div className="container-inner">{children}</div>
</div> </div>
</section> </section>
<section className="layout-right"> <section className="layout-right">
<div className="image-container"> <div className="image-container">
<img <img

View File

@ -26,7 +26,6 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: end; justify-content: end;
height: 100%; height: 100%;
/* padding: 70px 0 0; */
} }
.container-inner { .container-inner {
max-width: 70%; max-width: 70%;
@ -83,3 +82,85 @@
.image-container .nimbus-logomark svg { .image-container .nimbus-logomark svg {
height: 73px; height: 73px;
} }
@media (max-width: 1000px) {
.layout {
height: auto;
}
.layout-left {
flex: 0 0 100%;
max-width: 100%;
order: 1;
}
.container {
justify-content: start;
padding: 20px;
}
.container-inner {
max-width: 100%;
flex: 1 0 100%;
}
.layout-right {
flex: 0 0 100%;
max-width: 100%;
order: 0;
margin-top: -10%;
margin-bottom: -72%;
}
.image-container {
margin: 0;
height: auto;
position: relative;
overflow: hidden;
}
.image-container .background-img {
width: 100%;
position: absolute;
top: 10%;
left: 50%;
transform: translateX(-50%) translateY(-5%);
clip-path: inset(0 0 85% 0);
height: auto;
}
.image-container .nimbus-logomark {
display: none;
}
.content,
.breadcrumbBar,
.other-elements {
margin: 0;
padding: 0;
}
.breadcrumbBar,
.some-other-element {
margin: 0;
padding: 0;
}
.image-container {
position: relative;
overflow: hidden;
padding: 0;
margin: 0;
}
.image-container .background-img {
clip-path: polygon(0 0, 100% 0, 100% 40%, 0 40%);
}
.image-container::after {
width: 100%;
right: 0;
left: 0;
background: linear-gradient(to top, rgba(255, 255, 255, 1) 62%, rgba(255, 255, 255, 0));
}
}

View File

@ -45,7 +45,6 @@ export const DEPOSIT_SUBTITLE = 'Connect you Wallet to stake required ETH for ne
export const CLIENT_SETUP_SUBTITLE = 'How many Validators would you like to run?' export const CLIENT_SETUP_SUBTITLE = 'How many Validators would you like to run?'
// Dashboard // Dashboard
export const years = [ export const years = [
'JAN', 'JAN',
'FEB', 'FEB',
@ -60,3 +59,57 @@ export const years = [
'NOV', 'NOV',
'DEC', 'DEC',
] ]
export const VALIDATOR_TABS_RIGHT_SIDEBAR = ['Active', 'Pending', 'Inactive']
// Validator Management
export const VALIDATOR_TABS_MANAGEMENT = [
'Active',
'Pending',
'Inactive',
'Exited',
'Withdraw',
'All',
]
export const VALIDATORS_DATA = [
{
number: 1,
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
proposals: '1/102',
attestations: '1/102',
effectiveness: 98,
status: 'Active',
},
{
number: 1,
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
proposals: '1/102',
attestations: '1/102',
effectiveness: 98,
status: 'Active',
},
{
number: 1,
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
proposals: '1/102',
attestations: '1/102',
effectiveness: 98,
status: 'Active',
},
{
number: 1,
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
proposals: '1/102',
attestations: '1/102',
effectiveness: 98,
status: 'Active',
},
]

View File

@ -62,7 +62,6 @@
body { body {
margin: 0; margin: 0;
display: flex; display: flex;
min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }
h1, h1,
@ -103,11 +102,13 @@ ul li {
} }
.transparent-scrollbar::-webkit-scrollbar { .transparent-scrollbar::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px;
} }
.transparent-scrollbar::-webkit-scrollbar-thumb { .transparent-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
border-radius: 10px; border-radius: 10px;
height: 8px;
} }
.transparent-scrollbar::-webkit-scrollbar-thumb:hover { .transparent-scrollbar::-webkit-scrollbar-thumb:hover {
@ -130,3 +131,16 @@ ul li {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
} }
@media screen and (max-width: 440px) {
.header-container {
flex-direction: column;
gap: 12px;
}
}
@media (max-width: 900px) {
.right-sidebar-wrapper {
display: none;
}
}

View File

@ -1,7 +1,7 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import BreadcrumbBar from '../../components/General/BreadcrumbBar/BreadcrumbBar' import BreadcrumbBar from '../../components/General/BreadcrumbBar/BreadcrumbBar'
import { Button as StatusButton, Text, Avatar, Checkbox } from '@status-im/components' import { Button as StatusButton, Text, Avatar, Checkbox } from '@status-im/components'
import { Label, Separator, XStack, YStack } from 'tamagui' import { Article, Label, Separator, Stack, XStack, YStack } from 'tamagui'
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow' import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
import Titles from '../../components/General/Titles' import Titles from '../../components/General/Titles'
import LabelInputField from '../../components/General/LabelInputField' import LabelInputField from '../../components/General/LabelInputField'
@ -11,6 +11,30 @@ import { NodeIcon } from '@status-im/icons'
const ConnectDevicePage = () => { const ConnectDevicePage = () => {
const [autoConnectChecked, setAutoConnectChecked] = useState(false) const [autoConnectChecked, setAutoConnectChecked] = useState(false)
const [portChecked, setPortChecked] = useState(false) const [portChecked, setPortChecked] = useState(false)
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const breakpoint = 768
const responsiveXStackStyle = {
width: '100%',
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: windowWidth <= breakpoint ? 'column' : 'row',
flexWrap: windowWidth <= breakpoint ? 'wrap' : 'nowrap',
}
const responsiveInputStyle = {
width: windowWidth <= breakpoint ? '100%' : '40%',
marginBottom: windowWidth <= breakpoint ? '1rem' : '0',
}
return ( return (
<PageWrapperShadow <PageWrapperShadow
@ -21,31 +45,23 @@ const ConnectDevicePage = () => {
<YStack space={'$3'}> <YStack space={'$3'}>
<Header selectedTag="connect" /> <Header selectedTag="connect" />
<article className="content"> <Article className="content">
<Titles <Titles
title="Connect Device" title="Connect Device"
subtitle="Configure your device to connect to the Nimbus Node Manager" subtitle="Configure your device to connect to the Nimbus Node Manager"
/> />
<YStack my={16}> <YStack my={16}>
<XStack <XStack style={responsiveXStackStyle}>
width={'100%'} <Stack style={responsiveInputStyle}>
alignItems="center"
justifyContent="space-between"
// media query
$lg={{
flexDirection: 'column',
flexWrap: 'nowrap',
}}
>
<XStack width={'40%'}>
<LabelInputField labelText="Beacon Address" placeholderText="something" /> <LabelInputField labelText="Beacon Address" placeholderText="something" />
</XStack> </Stack>
<XStack width={'25%'}> <Stack style={responsiveInputStyle}>
<LabelInputField labelText="Beacon Node Port" placeholderText="5052" /> <LabelInputField labelText="Beacon Node Port" placeholderText="5052" />
</XStack> </Stack>
<XStack width={'25%'}> <Stack style={responsiveInputStyle}>
<LabelInputField labelText="Client Validator Port" placeholderText="5052" /> <LabelInputField labelText="Client Validator Port" placeholderText="5052" />
</XStack> </Stack>
<YStack width={20}> <YStack width={20}>
<Checkbox <Checkbox
id="port-checkbox" id="port-checkbox"
@ -96,7 +112,7 @@ const ConnectDevicePage = () => {
<Separator alignSelf="stretch" borderColor={'#F0F2F5'} /> <Separator alignSelf="stretch" borderColor={'#F0F2F5'} />
</YStack> </YStack>
<StatusButton icon={<NodeIcon size={20} />}>Connect Device</StatusButton> <StatusButton icon={<NodeIcon size={20} />}>Connect Device</StatusButton>
</article> </Article>
</YStack> </YStack>
</PageWrapperShadow> </PageWrapperShadow>
) )

View File

@ -1,4 +1,5 @@
import { Stack, YStack } from 'tamagui' import { Stack, YStack, XStack } from 'tamagui'
import BasicInfoCards from './BasicInfoCards/BasicInfoCards' import BasicInfoCards from './BasicInfoCards/BasicInfoCards'
import AddCardsContainer from '../../components/General/AddCards/AddCardsContainer' import AddCardsContainer from '../../components/General/AddCards/AddCardsContainer'
import BalanceChartCard from './BalanceChartCard/BalanceChartCard' import BalanceChartCard from './BalanceChartCard/BalanceChartCard'
@ -6,15 +7,16 @@ import CPUCard from './CPULoad/CPUCard'
import ConsensusUptimeCard from './ConsensusUptime/ConsensusUptimeCard' import ConsensusUptimeCard from './ConsensusUptime/ConsensusUptimeCard'
import ExecutionUptime from './ExecutionUptime/ExecutionUptime' import ExecutionUptime from './ExecutionUptime/ExecutionUptime'
import DeviceUptime from './DeviceUptime/DeviceUptime' import DeviceUptime from './DeviceUptime/DeviceUptime'
import TitleLogo from './TitleLogo' import TitleLogo from '../../components/General/TitleLogo'
import StorageCard from './StorageCard/StorageCard' import StorageCard from './StorageCard/StorageCard'
import NetworkCard from './NetworkCard/NetworkCard' import NetworkCard from './NetworkCard/NetworkCard'
import SyncStatusCard from './SyncStatusCards/SyncStatusCards' import SyncStatusCards from './SyncStatusCards/SyncStatusCards'
import MemoryCard from './MemoryCard/MemoryCard' import MemoryCard from './MemoryCard/MemoryCard'
import { XStack } from 'tamagui'
type DashboardContentProps = { type DashboardContentProps = {
windowWidth: number windowWidth: number
} }
const DashboardContent = ({ windowWidth }: DashboardContentProps) => { const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
return ( return (
<YStack <YStack
@ -30,7 +32,7 @@ const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
}} }}
className={'transparent-scrollbar'} className={'transparent-scrollbar'}
> >
<TitleLogo /> <TitleLogo subtitle="Node Management Dashboard" />
<Stack <Stack
style={{ style={{
display: 'grid', display: 'grid',
@ -39,8 +41,8 @@ const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
gridAutoFlow: 'row', gridAutoFlow: 'row',
}} }}
> >
<SyncStatusCard /> <SyncStatusCards />
<AddCardsContainer /> <AddCardsContainer cardsAmount={2} />
{windowWidth < 1375 ? ( {windowWidth < 1375 ? (
<Stack style={{ gridColumn: '1 / span 2' }} width={'101%'}> <Stack style={{ gridColumn: '1 / span 2' }} width={'101%'}>
<BalanceChartCard /> <BalanceChartCard />

View File

@ -5,7 +5,7 @@ import DashboardCardWrapper from '../DashboardCardWrapper'
import ExecutionClientCard from './ExecutionClientCard' import ExecutionClientCard from './ExecutionClientCard'
import ConsensusCard from './ConsensusClientCard' import ConsensusCard from './ConsensusClientCard'
const SyncStatusCard = () => { const SyncStatusCards = () => {
return ( return (
<DashboardCardWrapper padding="0" minWidth="50px"> <DashboardCardWrapper padding="0" minWidth="50px">
<YStack space={'$2'}> <YStack space={'$2'}>
@ -24,4 +24,4 @@ const SyncStatusCard = () => {
) )
} }
export default SyncStatusCard export default SyncStatusCards

View File

@ -11,9 +11,28 @@ import DeviceNetworkHealth from '../../components/Charts/DeviceNetworkHealth'
import { CloseCircleIcon } from '@status-im/icons' import { CloseCircleIcon } from '@status-im/icons'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { RootState } from '../../redux/store' import { RootState } from '../../redux/store'
import { useEffect, useState } from 'react'
const DeviceHealthCheck = () => { const DeviceHealthCheck = () => {
const deviceHealthState = useSelector((state: RootState) => state.deviceHealth) const deviceHealthState = useSelector((state: RootState) => state.deviceHealth)
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const breakpoint = 768
const responsiveStyle = {
flexWrap: windowWidth <= breakpoint ? 'wrap' : 'nowrap',
flexDirection: windowWidth <= breakpoint ? 'column' : 'row',
alignItems: 'flex-start',
width: windowWidth <= breakpoint ? '200%' : '100%',
}
return ( return (
<PageWrapperShadow rightImageSrc="./background-images/eye-background.png" imgHeight="100%"> <PageWrapperShadow rightImageSrc="./background-images/eye-background.png" imgHeight="100%">
@ -33,14 +52,14 @@ const DeviceHealthCheck = () => {
subtitle="Configure your device to start Staking on Nimbus" subtitle="Configure your device to start Staking on Nimbus"
isAdvancedSettings={true} isAdvancedSettings={true}
/> />
<XStack space={'$4'} width={'100%'}> <XStack space={'$4'} style={responsiveStyle}>
<DeviceStorageHealth <DeviceStorageHealth
storage={deviceHealthState.storage} storage={deviceHealthState.storage}
maxStorage={deviceHealthState.maxMemory} maxStorage={deviceHealthState.maxMemory}
/> />
<DeviceCPULoad load={deviceHealthState.cpuLoad} /> <DeviceCPULoad load={deviceHealthState.cpuLoad} />
</XStack> </XStack>
<XStack space={'$4'} width={'100%'}> <XStack space={'$4'} style={responsiveStyle}>
<DeviceMemory <DeviceMemory
currentMemory={deviceHealthState.memory} currentMemory={deviceHealthState.memory}
maxMemory={deviceHealthState.maxMemory} maxMemory={deviceHealthState.maxMemory}

View File

@ -1,3 +0,0 @@
.landing-page {
height: 100%;
}

View File

@ -0,0 +1,13 @@
.landing-page {
height: 100%;
}
.landing-texts {
margin: 30vh 0 4vh;
}
@media (max-width: 1000px) {
.landing-texts {
margin-top: 20vh;
}
}

View File

@ -1,27 +1,28 @@
import './LandingPage.css' import { useNavigate } from 'react-router'
import { XStack, YStack } from 'tamagui' import { XStack, YStack } from 'tamagui'
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
import NimbusLogo from '../../components/Logos/NimbusLogo'
import { NodeIcon } from '@status-im/icons' import { NodeIcon } from '@status-im/icons'
import { Button as StatusButton, Text } from '@status-im/components' import { Button as StatusButton, Text } from '@status-im/components'
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
import QuickStartBar from '../../components/General/QuickStartBar/QuickStartBar' import QuickStartBar from '../../components/General/QuickStartBar/QuickStartBar'
import { useNavigate } from 'react-router' import NimbusLogo from '../../components/Logos/NimbusLogo'
import styles from './LandingPage.module.css'
const LandingPage = () => { const LandingPage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const getStartedHanlder = () => { const onGetStartedHandler = () => {
navigate('/pair-device') navigate('/pair-device')
} }
return ( return (
<> <>
<PageWrapperShadow rightImageSrc="./background-images/landing-page-bg.png" imgHeight="150%"> <PageWrapperShadow rightImageSrc="./background-images/landing-page-bg.png" imgHeight="150%">
<YStack className="landing-page"> <YStack className={styles['landing-page']}>
<XStack pt={'70px'}> <XStack pt={'70px'}>
<NimbusLogo /> <NimbusLogo />
</XStack> </XStack>
<YStack style={{ width: '100%', margin: '30vh 0 4vh' }} space={'16px'}> <YStack className={styles['landing-texts']} space={'16px'}>
<Text size={27} weight={'semibold'}> <Text size={27} weight={'semibold'}>
Light and performant clients, for all Ethereum validators. Light and performant clients, for all Ethereum validators.
</Text> </Text>
@ -30,9 +31,8 @@ const LandingPage = () => {
you wish to run in a completely trustless and decentralized manner. you wish to run in a completely trustless and decentralized manner.
</Text> </Text>
</YStack> </YStack>
<XStack> <XStack>
<StatusButton icon={<NodeIcon size={20} />} onPress={getStartedHanlder}> <StatusButton icon={<NodeIcon size={20} />} onPress={onGetStartedHandler}>
Get Started Get Started
</StatusButton> </StatusButton>
</XStack> </XStack>

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import GenerateId from './GenerateId' import GenerateId from './GenerateId'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = { const meta = {
title: 'Pair Device/GenerateId', title: 'Pair Device/GenerateId',

View File

@ -3,9 +3,11 @@ import { CompleteIdIcon, CopyIcon } from '@status-im/icons'
import { Text } from '@tamagui/web' import { Text } from '@tamagui/web'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Separator, XStack, YStack } from 'tamagui' import { Separator, YStack } from 'tamagui'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import styles from './pairDevice.module.css'
type GenerateIdProps = { type GenerateIdProps = {
isAwaitingPairing: boolean isAwaitingPairing: boolean
} }
@ -27,7 +29,10 @@ const GenerateId = ({ isAwaitingPairing }: GenerateIdProps) => {
return ( return (
<YStack space={'$2'}> <YStack space={'$2'}>
<XStack style={{ justifyContent: 'space-between' }}> <div
style={{ display: 'flex', justifyContent: 'space-between' }}
className={styles['regenerate-id-container']}
>
<StatusText size={19} weight={'semibold'}> <StatusText size={19} weight={'semibold'}>
Pair with Command line Pair with Command line
</StatusText> </StatusText>
@ -39,7 +44,7 @@ const GenerateId = ({ isAwaitingPairing }: GenerateIdProps) => {
> >
Regenerate ID Regenerate ID
</Button> </Button>
</XStack> </div>
<YStack space={'$2'}> <YStack space={'$2'}>
<StatusText size={15} color={'#647084'}> <StatusText size={15} color={'#647084'}>
Generated Pairing ID Input Generated Pairing ID Input

View File

@ -1,6 +1,8 @@
import { NodeIcon } from '@status-im/icons'
import { Separator, XStack, YStack } from 'tamagui' import { Separator, XStack, YStack } from 'tamagui'
import { useState } from 'react' import { useState } from 'react'
import { Button, Text } from '@status-im/components' import { Button, Text } from '@status-im/components'
import { useNavigate } from 'react-router-dom'
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow' import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
import SyncStatus from './SyncStatus' import SyncStatus from './SyncStatus'
@ -8,53 +10,65 @@ import Titles from '../../components/General/Titles'
import PairedSuccessfully from './PairedSuccessfully' import PairedSuccessfully from './PairedSuccessfully'
import CreateAvatar from '../../components/General/CreateAvatar/CreateAvatar' import CreateAvatar from '../../components/General/CreateAvatar/CreateAvatar'
import GenerateId from './GenerateId' import GenerateId from './GenerateId'
import { NodeIcon } from '@status-im/icons'
import Header from '../../components/General/Header' import Header from '../../components/General/Header'
import Icon from '../../components/General/Icon' import Icon from '../../components/General/Icon'
const PairDevice = () => { const PairDevice = () => {
const [isAwaitingPairing, setIsAwaitingPairing] = useState(false) const [isAwaitingPairing, setIsAwaitingPairing] = useState(false)
const navigate = useNavigate()
const isPaired = false const isPaired = false
const isPairing = true const isPairing = false
const changeSetIsAwaitingPairing = (result: boolean) => { const changeSetIsAwaitingPairing = (result: boolean) => {
setIsAwaitingPairing(result) setIsAwaitingPairing(result)
} }
const connectViaIpHandler = () => {
navigate('/connect-device')
}
return ( return (
<PageWrapperShadow rightImageSrc="./background-images/day-night-bg.png" rightImageLogo={true}> <PageWrapperShadow rightImageSrc="./background-images/day-night-bg.png" rightImageLogo={true}>
<YStack <YStack space={'$3'}>
space={'$3'}
style={{
maxWidth: '100%',
}}
>
<Header selectedTag="pair" /> <Header selectedTag="pair" />
<Titles title="Pair Device" subtitle="Pair your device to the Nimbus Node Manager" /> <Titles
title="Connect to existing Nimbus Instance"
subtitle="Pair your existing device to the Nimbus Node Manager"
/>
{isPaired ? <PairedSuccessfully /> : <GenerateId isAwaitingPairing={isAwaitingPairing} />} {isPaired ? <PairedSuccessfully /> : <GenerateId isAwaitingPairing={isAwaitingPairing} />}
{!isPaired && ( {isPaired === false && (
<SyncStatus <SyncStatus
isPairing={isPairing} isPairing={isPairing}
isAwaitingPairing={isAwaitingPairing} isAwaitingPairing={isAwaitingPairing}
changeSetIsAwaitingPairing={changeSetIsAwaitingPairing} changeSetIsAwaitingPairing={changeSetIsAwaitingPairing}
/> />
)} )}
{isPaired === false && (
<YStack space={'$3'}>
<Separator borderColor={'#e3e3e3'} /> <Separator borderColor={'#e3e3e3'} />
<YStack space={'$1'}>
<Text size={19} weight={'semibold'} color="#09101C"> <Text size={19} weight={'semibold'} color="#09101C">
Advanced Settings Advanced Settings
</Text> </Text>
<XStack space={'$4'}> <XStack>
<Button icon={<Icon src="/icons/connection-blue.svg" width={20} />} variant="outline"> <Button
icon={<Icon src="/icons/connection-blue.svg" width={20} />}
variant="outline"
onPress={connectViaIpHandler}
>
Connect via IP Connect via IP
</Button> </Button>
</XStack> </XStack>
</YStack>
</YStack>
)}
{isPaired && <CreateAvatar />} {isPaired && <CreateAvatar />}
<Separator borderColor={'#e3e3e3'} /> <Separator borderColor={'#e3e3e3'} />
<XStack> <div>
<Button icon={<NodeIcon size={20} />} disabled={!isPaired}> <Button icon={<NodeIcon size={20} />} disabled={!isPaired}>
Continue Continue
</Button> </Button>
</XStack> </div>
</YStack> </YStack>
</PageWrapperShadow> </PageWrapperShadow>
) )

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import SyncStatus from './SyncStatus' import SyncStatus from './SyncStatus'
import { withRouter } from 'storybook-addon-react-router-v6'
const meta = { const meta = {
title: 'Pair Device/SyncStatus', title: 'Pair Device/SyncStatus',

View File

@ -1,13 +1,10 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { XStack, YStack } from 'tamagui' import { XStack, YStack } from 'tamagui'
import { Button, IconButton, InformationBox, Text } from '@status-im/components' import { IconButton, InformationBox, Text } from '@status-im/components'
import { CloseCircleIcon } from '@status-im/icons' import { CloseCircleIcon } from '@status-im/icons'
import Icon from '../../components/General/Icon'
import ConnectionIcon from '/icons/connection.svg'
import { convertSecondsToTimerFormat } from '../../utilities'
import { RefreshIcon } from '@status-im/icons' import { RefreshIcon } from '@status-im/icons'
import { useNavigate } from 'react-router'
import { convertSecondsToTimerFormat } from '../../utilities'
type SyncStatusProps = { type SyncStatusProps = {
isPairing: boolean isPairing: boolean
@ -21,7 +18,6 @@ const SyncStatus = ({
changeSetIsAwaitingPairing, changeSetIsAwaitingPairing,
}: SyncStatusProps) => { }: SyncStatusProps) => {
const [elapsedTime, setElapsedTime] = useState(0) const [elapsedTime, setElapsedTime] = useState(0)
const navigate = useNavigate()
const resetTimer = () => { const resetTimer = () => {
setElapsedTime(0) setElapsedTime(0)
@ -33,7 +29,7 @@ const SyncStatus = ({
if (isPairing) { if (isPairing) {
timer = setInterval(() => { timer = setInterval(() => {
setElapsedTime(prevTime => prevTime + 65) setElapsedTime(prevTime => prevTime + 1000)
if (elapsedTime >= 180) { if (elapsedTime >= 180) {
changeSetIsAwaitingPairing(true) changeSetIsAwaitingPairing(true)
} }
@ -47,19 +43,15 @@ const SyncStatus = ({
const timer = convertSecondsToTimerFormat(elapsedTime) const timer = convertSecondsToTimerFormat(elapsedTime)
const connectViaIpHandler = () => {
navigate('/connect-device')
}
return ( return (
<YStack space={'$2'}> <YStack>
<XStack style={{ justifyContent: 'space-between' }}> <XStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Text size={11} color="#647084" weight="medium"> <Text size={11} color="#647084" weight="medium">
Device Sync Status Device Sync Status
</Text> </Text>
{isPairing && ( {isPairing && (
<Text <Text
size={isAwaitingPairing ? 15 : 11} size={13}
color={isAwaitingPairing ? '#EB5757' : '#647084'} color={isAwaitingPairing ? '#EB5757' : '#647084'}
weight={isAwaitingPairing && 'semibold'} weight={isAwaitingPairing && 'semibold'}
> >
@ -89,13 +81,6 @@ const SyncStatus = ({
icon={<CloseCircleIcon size={20} />} icon={<CloseCircleIcon size={20} />}
/> />
)} )}
{isAwaitingPairing && (
<XStack>
<Button icon={<Icon src={ConnectionIcon} />} size={40} onPress={connectViaIpHandler}>
Connect via IP
</Button>
</XStack>
)}
</YStack> </YStack>
) )
} }

View File

@ -0,0 +1,6 @@
@media screen and (max-width: 440px) {
.regenerate-id-container {
flex-direction: column;
gap: 12px;
}
}

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ManagementCard from './ManagementCard'
const meta = {
title: 'ValidatorManagement/ManagementCard',
component: ManagementCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementCard>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,31 @@
import { Text } from '@status-im/components'
import { Separator, Stack, YStack } from 'tamagui'
const ManagementCard = () => {
return (
<YStack
space={'$3'}
style={{ border: '1px solid #F0F2F5', borderRadius: '16px', minWidth: '33%' }}
>
<Stack style={{ padding: '12px 16px' }}>
<Text size={15} weight={'semibold'}>
Validators
</Text>
</Stack>
<Separator borderColor={'#F0F2F5'} />
<Stack style={{ padding: '12px 16px' }}>
<Text size={15} weight={'semibold'} color="#647084">
Total Balance
</Text>
</Stack>
<Separator borderColor={'#F0F2F5'} />
<Stack style={{ padding: '12px 16px', marginBottom: '16px' }}>
<Text size={15} weight={'semibold'} color="#647084">
Total Income
</Text>
</Stack>
</YStack>
)
}
export default ManagementCard

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ManagementHeader from './ManagementHeader'
const meta = {
title: 'ValidatorManagement/ManagementHeader',
component: ManagementHeader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementHeader>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,39 @@
import { XStack } from 'tamagui'
import TitleLogo from '../../components/General/TitleLogo'
import SyncStatusCard from '../../components/General/SyncStatusCard'
const ManagementHeader = () => {
return (
<XStack
style={{
width: '100%',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '16px',
}}
>
<TitleLogo subtitle="Validator Management" />
<XStack space={'$2'}>
<div className="sync-status-card-container-first">
<SyncStatusCard
synced={123.524}
total={172.503}
title="Execution Sync Status"
color="#2a4af5"
/>
</div>
<div className="sync-status-card-container-second">
<SyncStatusCard
synced={123.524}
total={172.503}
title="Consensus Sync Status"
color="#ff6161"
/>
</div>
</XStack>
</XStack>
)
}
export default ManagementHeader

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import DropdownFilter from './DropdownFilter'
const meta = {
title: 'ValidatorManagement/DropdownFilter',
component: DropdownFilter,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof DropdownFilter>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,37 @@
import { DropdownMenu } from '@status-im/components'
import { SortIcon } from '@status-im/icons'
import { Stack } from 'tamagui'
const DropdownFilter = () => {
return (
<DropdownMenu>
<Stack style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<SortIcon
size={20}
color="#647084"
style={{
border: '1px solid #DCE0E5',
borderRadius: '10px',
padding: '8px',
cursor: 'pointer',
}}
/>
<Stack
style={{
position: 'absolute',
right: -2,
top: -1.5,
width: '9px',
height: '9px',
borderRadius: '50%',
backgroundColor: '#1992D7',
border: '1.5px solid #fff',
}}
/>
</Stack>
<DropdownMenu.Content sideOffset={5} position="absolute" zIndex={999} />
</DropdownMenu>
)
}
export default DropdownFilter

View File

@ -0,0 +1,18 @@
table {
width: 100%;
border-spacing: 0;
margin: 20px 0;
font-size: 14px;
border: 1px solid #e7eaee;
border-radius: 16px;
}
th {
border-bottom: 1px solid #e7eaee;
}
th,
td {
padding: 9px 19px;
text-align: center;
}

View File

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import ManagementTable from './ManagementTable'
import { VALIDATOR_TABS_MANAGEMENT } from '../../../constants'
const meta = {
title: 'ValidatorManagement/ManagementTable',
component: ManagementTable,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementTable>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = () => {
const [searchValue, setSearchValue] = useState('')
const changeSearchValue = (os: string) => {
setSearchValue(os)
}
return (
<ManagementTable
tab={VALIDATOR_TABS_MANAGEMENT[0]}
searchValue={searchValue}
changeSearchValue={changeSearchValue}
/>
)
}
Default.args = {
tab: VALIDATOR_TABS_MANAGEMENT[0],
searchValue: '',
changeSearchValue: () => {},
}

View File

@ -0,0 +1,92 @@
import { useEffect, useMemo, useState } from 'react'
import { YStack, XStack } from 'tamagui'
import { VALIDATORS_DATA, VALIDATOR_TABS_MANAGEMENT } from '../../../constants'
import SearchManagement from './SearchManagement'
import DropdownFilter from './DropdownFilter'
import ManagementTableHeader from './ManagementTableHeader'
import ManagementTableBody from './ManagementTableBody'
import './ManagementTable.css'
type ManagementTableProps = {
tab: string
searchValue: string
changeSearchValue: (value: string) => void
}
export type Validator = {
number: number
address: string
balance: number
income: number
proposals: string
attestations: string
effectiveness: number
status: string
}
const isValidStatus = (validatorStatus: string, tabStatus: string) => {
if (
validatorStatus === tabStatus ||
tabStatus === VALIDATOR_TABS_MANAGEMENT[VALIDATOR_TABS_MANAGEMENT.length - 1]
) {
return true
}
return false
}
const isValidNumberOrAddress = (
validatorNumber: number,
validatorAddress: string,
searchValue: string,
) => {
if (validatorNumber.toString().includes(searchValue) || validatorAddress.includes(searchValue)) {
return true
}
return false
}
const ManagementTable = ({ tab, searchValue, changeSearchValue }: ManagementTableProps) => {
const [validators, setValidators] = useState<Validator[]>([])
const [isAllSelected, setIsAllSelected] = useState(false)
useEffect(() => {
setValidators(VALIDATORS_DATA)
}, [])
useEffect(() => {
setIsAllSelected(false)
}, [validators, tab, searchValue])
const filteredValidators = useMemo(() => {
return validators
.filter(validator => isValidStatus(validator.status, tab))
.filter(validator => isValidNumberOrAddress(validator.number, validator.address, searchValue))
}, [validators, tab, searchValue])
const handleSelectAll = () => {
setIsAllSelected(state => !state)
}
return (
<YStack>
<XStack space={'$3'} justifyContent="space-between" alignItems="center">
<SearchManagement searchValue={searchValue} changeSearchValue={changeSearchValue} />
<DropdownFilter />
</XStack>
<table>
<ManagementTableHeader
validatorsAmount={filteredValidators.length}
isAllSelected={isAllSelected}
handleSelectAll={handleSelectAll}
/>
<ManagementTableBody
filteredValidators={filteredValidators}
isAllSelected={isAllSelected}
/>
</table>
</YStack>
)
}
export default ManagementTable

View File

@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react'
import ManagementTableBody from './ManagementTableBody'
import { VALIDATORS_DATA } from '../../../constants'
const meta = {
title: 'ValidatorManagement/ManagementTableBody',
component: ManagementTableBody,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementTableBody>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
filteredValidators: VALIDATORS_DATA,
isAllSelected: false,
},
}
export const AllSelected: Story = {
args: {
filteredValidators: VALIDATORS_DATA,
isAllSelected: true,
},
}

View File

@ -0,0 +1,34 @@
import { Text } from '@status-im/components'
import { Validator } from './ManagementTable'
import ManagementTableRow from './ManagementTableRow'
type ManagementTableBodyProps = {
filteredValidators: Validator[]
isAllSelected: boolean
}
const ManagementTableBody = ({ filteredValidators, isAllSelected }: ManagementTableBodyProps) => {
return (
<tbody>
{filteredValidators.map(validator => (
<ManagementTableRow
key={validator.address}
validator={validator}
isAllSelected={isAllSelected}
/>
))}
{filteredValidators.length === 0 && (
<tr>
<td colSpan={11}>
<Text size={15} color={'#647084'} weight={'semibold'}>
No validators
</Text>
</td>
</tr>
)}
</tbody>
)
}
export default ManagementTableBody

View File

@ -0,0 +1,38 @@
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'
import ManagementTableHeader from './ManagementTableHeader'
const meta = {
title: 'ValidatorManagement/ManagementTableHeader',
component: ManagementTableHeader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementTableHeader>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = () => {
const [isAllSelected, setIsAllSelected] = useState(false)
const handleSelectAll = () => {
setIsAllSelected(state => !state)
}
return (
<ManagementTableHeader
validatorsAmount={4}
isAllSelected={isAllSelected}
handleSelectAll={handleSelectAll}
/>
)
}
Default.args = {
isAllSelected: false,
validatorsAmount: 4,
handleSelectAll: () => {},
}

View File

@ -0,0 +1,66 @@
import { Checkbox, Text } from '@status-im/components'
type ManagementTableHeaderProps = {
validatorsAmount: number
isAllSelected: boolean
handleSelectAll: () => void
}
const ManagementTableHeader = ({
validatorsAmount,
isAllSelected,
handleSelectAll,
}: ManagementTableHeaderProps) => {
return (
<thead>
<tr>
<th>
<Checkbox
id="table"
variant="outline"
selected={isAllSelected}
onCheckedChange={handleSelectAll}
/>
</th>
<th>
<Text size={15} color={'#647084'}>
{validatorsAmount} Validators
</Text>
</th>
<th>
<Text size={15} color={'#647084'}>
Balance
</Text>
</th>
<th>
<Text size={15} color={'#647084'}>
Income
</Text>
</th>
<th>
<Text size={15} color={'#647084'}>
Proposals
</Text>
</th>
<th>
<Text size={15} color={'#647084'}>
Attestations
</Text>
</th>
<th>
<Text size={15} color={'#647084'}>
Effectiveness
</Text>
</th>
<th>
<Text size={15} color={'#647084'}>
Status
</Text>
</th>
<th />
</tr>
</thead>
)
}
export default ManagementTableHeader

View File

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react'
import ManagementTableRow from './ManagementTableRow'
import { VALIDATORS_DATA } from '../../../constants'
const meta = {
title: 'ValidatorManagement/ManagementTableRow',
component: ManagementTableRow,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementTableRow>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
validator: VALIDATORS_DATA[0],
isAllSelected: false,
},
}

View File

@ -0,0 +1,74 @@
import { useEffect, useState } from 'react'
import { Checkbox, Text } from '@status-im/components'
import { OptionsIcon } from '@status-im/icons'
import ValidatorProfile from '../../../components/General/ValidatorProfile'
import { Validator } from './ManagementTable'
type ManagementTableRowProps = {
validator: Validator
isAllSelected: boolean
}
const ManagementTableRow = ({ validator, isAllSelected }: ManagementTableRowProps) => {
const [isSelected, setIsSelected] = useState(false)
useEffect(() => {
setIsSelected(isAllSelected)
}, [isAllSelected])
const handleChangeIsSelected = () => {
setIsSelected(state => !state)
}
return (
<tr>
<td>
<Checkbox
id={validator.address}
variant="outline"
selected={isSelected}
onCheckedChange={handleChangeIsSelected}
/>
</td>
<td>
<ValidatorProfile number={validator.number} address={validator.address} />
</td>
<td>
<Text size={15} color={'#647084'} weight={'semibold'}>
{validator.balance}
</Text>
</td>
<td>
<Text size={15} color={'#647084'} weight={'semibold'}>
{validator.income}
</Text>
</td>
<td>
<Text size={15} color={'#647084'}>
{validator.proposals}
</Text>
</td>
<td>
<Text size={15} color={'#647084'}>
{validator.attestations}
</Text>
</td>
<td>
<Text size={15} color={'#647084'}>
{validator.effectiveness}%
</Text>
</td>
<td>
<Text size={15} color={'#2F80ED'} weight={'semibold'}>
{validator.status}
</Text>
</td>
<td>
<OptionsIcon size={20} color="#647084" style={{ cursor: 'pointer' }} />
</td>
</tr>
)
}
export default ManagementTableRow

View File

@ -0,0 +1,26 @@
import { useState } from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import SearchManagement from './SearchManagement'
const meta = {
title: 'ValidatorManagement/SearchManagement',
component: SearchManagement,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof SearchManagement>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = (args: { searchValue: string }) => {
const [searchValue, setSearchValue] = useState(args.searchValue)
return <SearchManagement searchValue={searchValue} changeSearchValue={setSearchValue} />
}
Default.args = {
searchValue: '',
}

View File

@ -0,0 +1,24 @@
import { Input } from '@status-im/components'
import { SearchIcon } from '@status-im/icons'
type SearchManagementProps = {
searchValue: string
changeSearchValue: (value: string) => void
}
const SearchManagement = ({ searchValue, changeSearchValue }: SearchManagementProps) => {
return (
<div style={{ width: '100%' }}>
<Input
placeholder="Filter Validators"
value={searchValue}
onChangeText={changeSearchValue}
icon={<SearchIcon size={20} />}
onClear={() => changeSearchValue('')}
size={40}
/>
</div>
)
}
export default SearchManagement

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ManagementTabs from './ManagementTabs'
const meta = {
title: 'ValidatorManagement/ManagementTabs',
component: ManagementTabs,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ManagementTabs>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,43 @@
import { Tabs } from '@status-im/components'
import { Stack } from 'tamagui'
import { useState } from 'react'
import ManagementTable from './ManagementTable/ManagementTable'
import { VALIDATOR_TABS_MANAGEMENT } from '../../constants'
const ManagementTabs = () => {
const [searchValue, setSearchValue] = useState('')
const changeSearchValue = (value: string) => {
setSearchValue(value)
}
return (
<div style={{ width: '100%' }}>
<Tabs defaultValue={VALIDATOR_TABS_MANAGEMENT[0]}>
<div className="tabs transparent-scrollbar">
<Stack maxWidth={'120px'} style={{ cursor: 'pointer', margin: '8px 0' }}>
<Tabs.List size={32}>
{VALIDATOR_TABS_MANAGEMENT.map(tab => (
<Tabs.Trigger key={tab} type="default" value={tab}>
{tab}
</Tabs.Trigger>
))}
</Tabs.List>
</Stack>
</div>
{VALIDATOR_TABS_MANAGEMENT.map(tab => (
<Tabs.Content key={tab} value={tab} style={{ marginTop: '8px' }}>
<ManagementTable
tab={tab}
searchValue={searchValue}
changeSearchValue={changeSearchValue}
/>
</Tabs.Content>
))}
</Tabs>
</div>
)
}
export default ManagementTabs

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ValidatorManagement from './ValidatorManagement'
const meta = {
title: 'Pages/ValidatorManagement',
component: ValidatorManagement,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ValidatorManagement>
export default meta
type Story = StoryObj<typeof meta>
export const Page: Story = {
args: {},
}

View File

@ -1,7 +1,20 @@
import { YStack } from 'tamagui' import { XStack } from 'tamagui'
import ValidatorManagementContent from './ValidatorManagementContent'
import LeftSidebar from '../../components/General/LeftSidebar/LeftSidebar'
import RightSidebar from '../../components/General/RightSideBar/RightSidebar'
import './validatorManagement.css'
const ValidatorManagement = () => { const ValidatorManagement = () => {
return <YStack></YStack> return (
<XStack style={{ height: '100vh' }}>
<LeftSidebar />
<ValidatorManagementContent />
<div className="right-sidebar-wrapper">
<RightSidebar />
</div>
</XStack>
)
} }
export default ValidatorManagement export default ValidatorManagement

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ValidatorManagementContent from './ValidatorManagementContent'
const meta = {
title: 'ValidatorManagement/ValidatorManagementContent',
component: ValidatorManagementContent,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ValidatorManagementContent>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@ -0,0 +1,44 @@
import { Text } from '@status-im/components'
import { YStack } from 'tamagui'
import ManagementTabs from './ManagementTabs'
import AddCardsContainer from '../../components/General/AddCards/AddCardsContainer'
import ManagementHeader from './ManagementHeader'
import ManagementCard from './ManagementCard'
const ValidatorManagementContent = () => {
return (
<YStack
space="$4"
alignItems="start"
px="24px"
style={{
flexGrow: '1',
overflowY: 'auto',
}}
className="transparent-scrollbar"
>
<ManagementHeader />
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
width: '100%',
gap: '16px',
}}
className="cards"
>
<ManagementCard />
<ManagementCard />
<AddCardsContainer cardsAmount={2} />
</div>
<Text size={27} weight={'semibold'}>
Validators
</Text>
<ManagementTabs />
</YStack>
)
}
export default ValidatorManagementContent

View File

@ -0,0 +1,94 @@
@media (max-width: 1130px) {
.sync-status-card-container-first {
display: none;
}
}
@media (max-width: 900px) and (min-width: 810px) {
.sync-status-card-container-first {
display: block;
}
}
@media (max-width: 590px) {
.sync-status-card-container-second {
display: none;
}
}
@media (max-width: 600px) {
.tabs {
overflow-x: auto;
overflow-y: none;
}
}
@media (max-width: 600px) {
.cards {
flex-direction: column;
}
}
/* Hide Effectiveness */
@media (max-width: 1300px) {
th:nth-child(7),
td:nth-child(7) {
display: none;
}
}
/* Hide the Attestations */
@media (max-width: 1200px) {
th:nth-child(6),
td:nth-child(6) {
display: none;
}
}
/* Hide the Proposals */
@media (max-width: 1100px) {
th:nth-child(5),
td:nth-child(5) {
display: none;
}
}
/* Hide and show Proposals */
@media (max-width: 900px) and (min-width: 800px) {
th:nth-child(5),
td:nth-child(5) {
display: table-cell;
}
}
/* Hide the Income */
@media (max-width: 1000px) {
th:nth-child(4),
td:nth-child(4) {
display: none;
}
}
/* Hide and show Income */
@media (max-width: 900px) and (min-width: 700px) {
th:nth-child(4),
td:nth-child(4) {
display: table-cell;
}
}
/* Hide Status */
@media (max-width: 560px) {
th:nth-child(8),
td:nth-child(8) {
display: none;
}
}
/* Hide Balance */
@media (max-width: 475px) {
th:nth-child(3),
td:nth-child(3) {
display: none;
}
}

View File

@ -1,8 +1,8 @@
import { Avatar, DividerLine, Text } from '@status-im/components' import { DividerLine, Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui' import { XStack, YStack } from 'tamagui'
import { getFormattedValidatorAddress } from '../../../../utilities'
import TransactionStatus from './TransactionStatus' import TransactionStatus from './TransactionStatus'
import ValidatorProfile from '../../../../components/General/ValidatorProfile'
type ValidatorRequestProps = { type ValidatorRequestProps = {
number: number number: number
@ -17,23 +17,7 @@ const ValidatorRequest = ({ number, isTransactionConfirmation }: ValidatorReques
<YStack space={'$3'} style={{ width: '100%' }}> <YStack space={'$3'} style={{ width: '100%' }}>
<XStack style={{ justifyContent: 'space-between', width: '100%', alignItems: 'center' }}> <XStack style={{ justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
<XStack style={{ justifyContent: 'space-between', width: '44%', alignItems: 'center' }}> <XStack style={{ justifyContent: 'space-between', width: '44%', alignItems: 'center' }}>
<XStack space={'$2'}> <ValidatorProfile number={number} address={'zQ3asdf9d4Gs0'} />
<Avatar
type="user"
size={32}
src="/icons/validator-request.svg"
name={number.toString()}
indicator="online"
/>
<YStack>
<Text size={13} weight={'semibold'}>
Validator {number}
</Text>
<Text size={13} color="#647084">
{getFormattedValidatorAddress('zQ3asdf9d4Gs0')}
</Text>
</YStack>
</XStack>
<Text size={13} color="#647084" weight={'semibold'}> <Text size={13} color="#647084" weight={'semibold'}>
Keys Generated Keys Generated
</Text> </Text>

View File

@ -1,6 +1,6 @@
import { XStack } from 'tamagui' import { XStack } from 'tamagui'
import KeyGenerationSyncCard from './KeyGenerationSyncCard' import SyncStatusCard from '../../../../components/General/SyncStatusCard'
import KeyGenerationTitle from '../KeyGenerationTitle' import KeyGenerationTitle from '../KeyGenerationTitle'
const KeyGenerationHeader = () => { const KeyGenerationHeader = () => {
@ -8,13 +8,13 @@ const KeyGenerationHeader = () => {
<XStack style={{ width: '100%', justifyContent: 'space-between' }}> <XStack style={{ width: '100%', justifyContent: 'space-between' }}>
<KeyGenerationTitle /> <KeyGenerationTitle />
<XStack space={'$2'}> <XStack space={'$2'}>
<KeyGenerationSyncCard <SyncStatusCard
synced={123.524} synced={123.524}
total={172.503} total={172.503}
title="Execution Sync Status" title="Execution Sync Status"
color="#2a4af5" color="#2a4af5"
/> />
<KeyGenerationSyncCard <SyncStatusCard
synced={123.524} synced={123.524}
total={172.503} total={172.503}
title="Consensus Sync Status" title="Consensus Sync Status"