Merge branch 'main' into Create-Logs
This commit is contained in:
commit
df194ee580
|
@ -36,3 +36,4 @@ dist-ssr
|
|||
|
||||
# vercel
|
||||
/.vercel
|
||||
.vercel
|
||||
|
|
40
README.md
40
README.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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>
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.landing-page {
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
.landing-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.landing-texts {
|
||||
margin: 30vh 0 4vh;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.landing-texts {
|
||||
margin-top: 20vh;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
@media screen and (max-width: 440px) {
|
||||
.regenerate-id-container {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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: () => {},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: () => {},
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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: '',
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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: {},
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue