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

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
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
[`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.
### 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
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
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
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 CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding'
import { ethereumRopsten, wcV2InitOptions, apiKey } from './constants'
import Dashboard from './pages/Dashboard/Dashboard'
import ConnectExistingInstance from './pages/ConnectExistingInstance/ConnectExistingInstance'
import './App.css'
import ValidatorManagement from './pages/ValidatorManagement/ValidatorManagement'
import LogsPage from './pages/LogsPage/LogsPage'
import { ethereumRopsten, wcV2InitOptions, apiKey } from './constants'
import './App.css'
const injected = injectedModule()
const walletConnect = walletConnectModule(wcV2InitOptions)

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import AddCardsContainer from './AddCardsContainer'
const meta = {
title: 'Dashboard/AddCardsContainer',
title: 'General/AddCardsContainer',
component: AddCardsContainer,
parameters: {
layout: 'centered',
@ -15,5 +15,13 @@ export default meta
type Story = StoryObj<typeof meta>
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 { getHeightPercentages } from '../../../utilities'
const AddCardsContainer = () => {
const cards = 2
type AddCardsContainerProps = {
cardsAmount: number
}
const AddCardsContainer = ({ cardsAmount }: AddCardsContainerProps) => {
return (
<DashboardCardWrapper padding="0" minWidth="50px">
<YStack height={'100%'}>
{Array.from({ length: cards }).map((_, index) => (
<AddCard key={index} style={{ padding: '56px', height: getHeightPercentages(cards) }} />
{Array.from({ length: cardsAmount }).map((_, index) => (
<AddCard
key={index}
style={{ padding: '40px', height: getHeightPercentages(cardsAmount) }}
/>
))}
</YStack>
</DashboardCardWrapper>

View File

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

View File

@ -10,7 +10,7 @@ type LabelInputProps = {
const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => {
return (
<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}
</Text>
<div className="input-container">

View File

@ -26,3 +26,15 @@
color: #0d1625;
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 = () => {
return (
<nav className="quick-start-bar">
<nav className={styles['quick-start-bar']}>
<span>
<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 ValidatorsList from './ValidatorsList'
import { useMemo } from 'react'
import { VALIDATOR_TABS_RIGHT_SIDEBAR } from '../../../../constants'
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 (
<Tabs defaultValue="active">
<Tabs defaultValue={VALIDATOR_TABS_RIGHT_SIDEBAR[0]}>
<Stack style={{ cursor: 'pointer', width: 'fit-content' }}>
<Tabs.List size={32}>
{VALIDATOR_TABS.map(tab => (
<Tabs.Trigger key={tab.value} type="default" value={tab.value}>
{tab.label}
{VALIDATOR_TABS_RIGHT_SIDEBAR.map(tab => (
<Tabs.Trigger key={tab} type="default" value={tab}>
{tab}
</Tabs.Trigger>
))}
</Tabs.List>
</Stack>
{VALIDATOR_TABS.map(tab => (
<Tabs.Content key={tab.value} value={tab.value} style={{ marginTop: '8px' }}>
{tab.children}
{VALIDATOR_TABS_RIGHT_SIDEBAR.map(tab => (
<Tabs.Content key={tab} value={tab} style={{ marginTop: '8px' }}>
<ValidatorsList />
</Tabs.Content>
))}
</Tabs>

View File

@ -1,15 +1,15 @@
import type { Meta, StoryObj } from '@storybook/react'
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
import SyncStatusCard from './SyncStatusCard'
const meta = {
title: 'ValidatorOnboarding/KeyGenerationSyncCard',
component: KeyGenerationSyncCard,
title: 'General/SyncStatusCard',
component: SyncStatusCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof KeyGenerationSyncCard>
} satisfies Meta<typeof SyncStatusCard>
export default 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 { Text } from '@status-im/components'
import StandardGauge from '../../../../components/Charts/StandardGauge'
import BorderBox from '../../../../components/General/BorderBox'
import { formatNumbersWithComa } from '../../../../utilities'
import StandardGauge from '../Charts/StandardGauge'
import BorderBox from './BorderBox'
import { formatNumbersWithComa } from '../../utilities'
type KeyGenerationSyncCardProps = {
type SyncStatusCardProps = {
synced: number
total: number
title: string
color: string
}
const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyncCardProps) => {
const SyncStatusCard = ({ synced, total, title, color }: SyncStatusCardProps) => {
return (
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
<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'
const meta = {
title: 'Dashboard/TitleLogo',
title: 'General/TitleLogo',
component: TitleLogo,
parameters: {
layout: 'centered',
@ -15,5 +15,11 @@ export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
subtitle: 'Node Management Dashboard',
},
}
export const WithoutSubtitle: Story = {
args: {},
}

View File

@ -1,7 +1,11 @@
import { Avatar, Text } from '@status-im/components'
import { Stack, XStack, YStack } from 'tamagui'
const TitleLogo = () => {
type TitleLogoProps = {
subtitle?: string
}
const TitleLogo = ({ subtitle }: TitleLogoProps) => {
return (
<XStack space={'$2'}>
<Stack style={{ marginTop: '3px' }}>
@ -18,7 +22,7 @@ const TitleLogo = () => {
Nimbus
</Text>
<Text size={19} color="#647084">
Node Management Dashboard
{subtitle}
</Text>
</YStack>
</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 './layout.css'
import NimbusLogoMark from '../Logos/NimbusLogoMark'
import { useTheme } from 'tamagui'
import NimbusLogoMark from '../Logos/NimbusLogoMark'
import './layout.css'
type PageWrapperShadowProps = {
breadcrumbBar?: ReactNode
rightImageSrc?: string
@ -28,7 +29,6 @@ const PageWrapperShadow = ({
<div className="container-inner">{children}</div>
</div>
</section>
<section className="layout-right">
<div className="image-container">
<img

View File

@ -26,7 +26,6 @@
flex-wrap: wrap;
justify-content: end;
height: 100%;
/* padding: 70px 0 0; */
}
.container-inner {
max-width: 70%;
@ -83,3 +82,85 @@
.image-container .nimbus-logomark svg {
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?'
// Dashboard
export const years = [
'JAN',
'FEB',
@ -60,3 +59,57 @@ export const years = [
'NOV',
'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 {
margin: 0;
display: flex;
min-width: 320px;
min-height: 100vh;
}
h1,
@ -103,11 +102,13 @@ ul li {
}
.transparent-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.transparent-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 10px;
height: 8px;
}
.transparent-scrollbar::-webkit-scrollbar-thumb:hover {
@ -130,3 +131,16 @@ ul li {
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 { 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 Titles from '../../components/General/Titles'
import LabelInputField from '../../components/General/LabelInputField'
@ -11,6 +11,30 @@ import { NodeIcon } from '@status-im/icons'
const ConnectDevicePage = () => {
const [autoConnectChecked, setAutoConnectChecked] = 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 (
<PageWrapperShadow
@ -21,31 +45,23 @@ const ConnectDevicePage = () => {
<YStack space={'$3'}>
<Header selectedTag="connect" />
<article className="content">
<Article className="content">
<Titles
title="Connect Device"
subtitle="Configure your device to connect to the Nimbus Node Manager"
/>
<YStack my={16}>
<XStack
width={'100%'}
alignItems="center"
justifyContent="space-between"
// media query
$lg={{
flexDirection: 'column',
flexWrap: 'nowrap',
}}
>
<XStack width={'40%'}>
<XStack style={responsiveXStackStyle}>
<Stack style={responsiveInputStyle}>
<LabelInputField labelText="Beacon Address" placeholderText="something" />
</XStack>
<XStack width={'25%'}>
</Stack>
<Stack style={responsiveInputStyle}>
<LabelInputField labelText="Beacon Node Port" placeholderText="5052" />
</XStack>
<XStack width={'25%'}>
</Stack>
<Stack style={responsiveInputStyle}>
<LabelInputField labelText="Client Validator Port" placeholderText="5052" />
</XStack>
</Stack>
<YStack width={20}>
<Checkbox
id="port-checkbox"
@ -96,7 +112,7 @@ const ConnectDevicePage = () => {
<Separator alignSelf="stretch" borderColor={'#F0F2F5'} />
</YStack>
<StatusButton icon={<NodeIcon size={20} />}>Connect Device</StatusButton>
</article>
</Article>
</YStack>
</PageWrapperShadow>
)

View File

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

View File

@ -5,7 +5,7 @@ import DashboardCardWrapper from '../DashboardCardWrapper'
import ExecutionClientCard from './ExecutionClientCard'
import ConsensusCard from './ConsensusClientCard'
const SyncStatusCard = () => {
const SyncStatusCards = () => {
return (
<DashboardCardWrapper padding="0" minWidth="50px">
<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 { useSelector } from 'react-redux'
import { RootState } from '../../redux/store'
import { useEffect, useState } from 'react'
const DeviceHealthCheck = () => {
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 (
<PageWrapperShadow rightImageSrc="./background-images/eye-background.png" imgHeight="100%">
@ -33,14 +52,14 @@ const DeviceHealthCheck = () => {
subtitle="Configure your device to start Staking on Nimbus"
isAdvancedSettings={true}
/>
<XStack space={'$4'} width={'100%'}>
<XStack space={'$4'} style={responsiveStyle}>
<DeviceStorageHealth
storage={deviceHealthState.storage}
maxStorage={deviceHealthState.maxMemory}
/>
<DeviceCPULoad load={deviceHealthState.cpuLoad} />
</XStack>
<XStack space={'$4'} width={'100%'}>
<XStack space={'$4'} style={responsiveStyle}>
<DeviceMemory
currentMemory={deviceHealthState.memory}
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 PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
import NimbusLogo from '../../components/Logos/NimbusLogo'
import { NodeIcon } from '@status-im/icons'
import { Button as StatusButton, Text } from '@status-im/components'
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
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 navigate = useNavigate()
const getStartedHanlder = () => {
const onGetStartedHandler = () => {
navigate('/pair-device')
}
return (
<>
<PageWrapperShadow rightImageSrc="./background-images/landing-page-bg.png" imgHeight="150%">
<YStack className="landing-page">
<YStack className={styles['landing-page']}>
<XStack pt={'70px'}>
<NimbusLogo />
</XStack>
<YStack style={{ width: '100%', margin: '30vh 0 4vh' }} space={'16px'}>
<YStack className={styles['landing-texts']} space={'16px'}>
<Text size={27} weight={'semibold'}>
Light and performant clients, for all Ethereum validators.
</Text>
@ -30,9 +31,8 @@ const LandingPage = () => {
you wish to run in a completely trustless and decentralized manner.
</Text>
</YStack>
<XStack>
<StatusButton icon={<NodeIcon size={20} />} onPress={getStartedHanlder}>
<StatusButton icon={<NodeIcon size={20} />} onPress={onGetStartedHandler}>
Get Started
</StatusButton>
</XStack>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,10 @@
import { useEffect, useState } from 'react'
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 Icon from '../../components/General/Icon'
import ConnectionIcon from '/icons/connection.svg'
import { convertSecondsToTimerFormat } from '../../utilities'
import { RefreshIcon } from '@status-im/icons'
import { useNavigate } from 'react-router'
import { convertSecondsToTimerFormat } from '../../utilities'
type SyncStatusProps = {
isPairing: boolean
@ -21,7 +18,6 @@ const SyncStatus = ({
changeSetIsAwaitingPairing,
}: SyncStatusProps) => {
const [elapsedTime, setElapsedTime] = useState(0)
const navigate = useNavigate()
const resetTimer = () => {
setElapsedTime(0)
@ -33,7 +29,7 @@ const SyncStatus = ({
if (isPairing) {
timer = setInterval(() => {
setElapsedTime(prevTime => prevTime + 65)
setElapsedTime(prevTime => prevTime + 1000)
if (elapsedTime >= 180) {
changeSetIsAwaitingPairing(true)
}
@ -47,19 +43,15 @@ const SyncStatus = ({
const timer = convertSecondsToTimerFormat(elapsedTime)
const connectViaIpHandler = () => {
navigate('/connect-device')
}
return (
<YStack space={'$2'}>
<XStack style={{ justifyContent: 'space-between' }}>
<YStack>
<XStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Text size={11} color="#647084" weight="medium">
Device Sync Status
</Text>
{isPairing && (
<Text
size={isAwaitingPairing ? 15 : 11}
size={13}
color={isAwaitingPairing ? '#EB5757' : '#647084'}
weight={isAwaitingPairing && 'semibold'}
>
@ -89,13 +81,6 @@ const SyncStatus = ({
icon={<CloseCircleIcon size={20} />}
/>
)}
{isAwaitingPairing && (
<XStack>
<Button icon={<Icon src={ConnectionIcon} />} size={40} onPress={connectViaIpHandler}>
Connect via IP
</Button>
</XStack>
)}
</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 = () => {
return <YStack></YStack>
return (
<XStack style={{ height: '100vh' }}>
<LeftSidebar />
<ValidatorManagementContent />
<div className="right-sidebar-wrapper">
<RightSidebar />
</div>
</XStack>
)
}
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 { getFormattedValidatorAddress } from '../../../../utilities'
import TransactionStatus from './TransactionStatus'
import ValidatorProfile from '../../../../components/General/ValidatorProfile'
type ValidatorRequestProps = {
number: number
@ -17,23 +17,7 @@ const ValidatorRequest = ({ number, isTransactionConfirmation }: ValidatorReques
<YStack space={'$3'} style={{ width: '100%' }}>
<XStack style={{ justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
<XStack style={{ justifyContent: 'space-between', width: '44%', alignItems: 'center' }}>
<XStack space={'$2'}>
<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>
<ValidatorProfile number={number} address={'zQ3asdf9d4Gs0'} />
<Text size={13} color="#647084" weight={'semibold'}>
Keys Generated
</Text>

View File

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