mirror of
https://github.com/status-im/nimbus-gui.git
synced 2025-02-28 02:50:45 +00:00
Merge branch 'main' into Create-Logs
This commit is contained in:
commit
df194ee580
1
.gitignore
vendored
1
.gitignore
vendored
@ -36,3 +36,4 @@ dist-ssr
|
|||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
/.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
|
We have a Storybook up at https://nimbus-gui.github.io/nimbus-gui/ which shows
|
||||||
the components of the project. We also have a deployed version of the GUI up at
|
the components of the project. We also have a deployed version of the GUI up at
|
||||||
@ -10,13 +36,13 @@ https://nimbus-gui.vercel.app/ which shows the GUI as it currently looks in the
|
|||||||
`main` branch of the
|
`main` branch of the
|
||||||
[`nimbus-gui/nimbus-gui`](https://github.com/nimbus-gui/nimbus-gui) repository.
|
[`nimbus-gui/nimbus-gui`](https://github.com/nimbus-gui/nimbus-gui) repository.
|
||||||
|
|
||||||
## Development and running the project yourself
|
## How to Contribute
|
||||||
|
|
||||||
### Dependencies
|
### Install all dependencies
|
||||||
|
|
||||||
Run `yarn` in the root directory of the project in order to install dependencies.
|
Run `yarn` in the root directory of the project in order to install dependencies.
|
||||||
|
|
||||||
### Running a development server
|
### Run a development server
|
||||||
|
|
||||||
If you want to run a development server to see what the GUI looks like you can
|
If you want to run a development server to see what the GUI looks like you can
|
||||||
run the following command:
|
run the following command:
|
||||||
@ -28,7 +54,7 @@ yarn dev
|
|||||||
This will start the server on port 5173 and you can open https://localhost:5173
|
This will start the server on port 5173 and you can open https://localhost:5173
|
||||||
in order to see the page.
|
in order to see the page.
|
||||||
|
|
||||||
### Running storybook locally
|
### Launch Storybook locally
|
||||||
|
|
||||||
If you want to run the Storybook locally you can simply run `yarn storybook` in
|
If you want to run the Storybook locally you can simply run `yarn storybook` in
|
||||||
the root of the project. This is useful if you want to contribute a component
|
the root of the project. This is useful if you want to contribute a component
|
||||||
|
@ -16,12 +16,12 @@ import PairDevice from './pages/PairDevice/PairDevice'
|
|||||||
import PinnedNotification from './components/General/PinnedNottification'
|
import PinnedNotification from './components/General/PinnedNottification'
|
||||||
import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
|
import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
|
||||||
import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding'
|
import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding'
|
||||||
import { ethereumRopsten, wcV2InitOptions, apiKey } from './constants'
|
|
||||||
import Dashboard from './pages/Dashboard/Dashboard'
|
import Dashboard from './pages/Dashboard/Dashboard'
|
||||||
import ConnectExistingInstance from './pages/ConnectExistingInstance/ConnectExistingInstance'
|
import ConnectExistingInstance from './pages/ConnectExistingInstance/ConnectExistingInstance'
|
||||||
import './App.css'
|
|
||||||
import ValidatorManagement from './pages/ValidatorManagement/ValidatorManagement'
|
import ValidatorManagement from './pages/ValidatorManagement/ValidatorManagement'
|
||||||
import LogsPage from './pages/LogsPage/LogsPage'
|
import LogsPage from './pages/LogsPage/LogsPage'
|
||||||
|
import { ethereumRopsten, wcV2InitOptions, apiKey } from './constants'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
const injected = injectedModule()
|
const injected = injectedModule()
|
||||||
const walletConnect = walletConnectModule(wcV2InitOptions)
|
const walletConnect = walletConnectModule(wcV2InitOptions)
|
||||||
|
@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
|
|||||||
import AddCardsContainer from './AddCardsContainer'
|
import AddCardsContainer from './AddCardsContainer'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Dashboard/AddCardsContainer',
|
title: 'General/AddCardsContainer',
|
||||||
component: AddCardsContainer,
|
component: AddCardsContainer,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
@ -15,5 +15,13 @@ export default meta
|
|||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {},
|
args: {
|
||||||
|
cardsAmount: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutCards: Story = {
|
||||||
|
args: {
|
||||||
|
cardsAmount: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,19 @@ import AddCard from './AddCard'
|
|||||||
import DashboardCardWrapper from '../../../pages/Dashboard/DashboardCardWrapper'
|
import DashboardCardWrapper from '../../../pages/Dashboard/DashboardCardWrapper'
|
||||||
import { getHeightPercentages } from '../../../utilities'
|
import { getHeightPercentages } from '../../../utilities'
|
||||||
|
|
||||||
const AddCardsContainer = () => {
|
type AddCardsContainerProps = {
|
||||||
const cards = 2
|
cardsAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddCardsContainer = ({ cardsAmount }: AddCardsContainerProps) => {
|
||||||
return (
|
return (
|
||||||
<DashboardCardWrapper padding="0" minWidth="50px">
|
<DashboardCardWrapper padding="0" minWidth="50px">
|
||||||
<YStack height={'100%'}>
|
<YStack height={'100%'}>
|
||||||
{Array.from({ length: cards }).map((_, index) => (
|
{Array.from({ length: cardsAmount }).map((_, index) => (
|
||||||
<AddCard key={index} style={{ padding: '56px', height: getHeightPercentages(cards) }} />
|
<AddCard
|
||||||
|
key={index}
|
||||||
|
style={{ padding: '40px', height: getHeightPercentages(cardsAmount) }}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</YStack>
|
</YStack>
|
||||||
</DashboardCardWrapper>
|
</DashboardCardWrapper>
|
||||||
|
@ -31,13 +31,13 @@ const CreateAvatar = () => {
|
|||||||
}, [emojiRef])
|
}, [emojiRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack my={16}>
|
<YStack>
|
||||||
<XStack space>
|
<XStack>
|
||||||
<LabelInputField labelText="Device Name" placeholderText="Stake and chips" />
|
<LabelInputField labelText="Device Name" placeholderText="Stake and chips" />
|
||||||
</XStack>
|
</XStack>
|
||||||
<XStack my={10} justifyContent={'space-between'}>
|
<XStack space={'$3'} justifyContent={'space-between'}>
|
||||||
<YStack mr={60}>
|
<YStack>
|
||||||
<Text size={13} weight="regular" color={'#647084'}>
|
<Text size={13} weight="semibold" color={'#647084'}>
|
||||||
Device Avatar
|
Device Avatar
|
||||||
</Text>
|
</Text>
|
||||||
<XStack my={10} alignItems={'end'}>
|
<XStack my={10} alignItems={'end'}>
|
||||||
@ -64,8 +64,8 @@ const CreateAvatar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack flexWrap="wrap" width="80%">
|
<YStack flexWrap="wrap" width="73%">
|
||||||
<Text size={13} weight="regular" color={'#647084'}>
|
<Text size={13} weight="semibold" color={'#647084'}>
|
||||||
Highlight Color
|
Highlight Color
|
||||||
</Text>
|
</Text>
|
||||||
<ColorPicker setChosenColor={setChosenColor} />
|
<ColorPicker setChosenColor={setChosenColor} />
|
||||||
|
33
src/components/General/Header.stories.tsx
Normal file
33
src/components/General/Header.stories.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { XStack } from 'tamagui'
|
|
||||||
import NimbusLogo from '../Logos/NimbusLogo'
|
import NimbusLogo from '../Logos/NimbusLogo'
|
||||||
import TagContainer from './TagContainer'
|
import TagContainer from './TagContainer'
|
||||||
|
|
||||||
@ -8,10 +7,18 @@ type HeaderProps = {
|
|||||||
|
|
||||||
const Header = ({ selectedTag }: HeaderProps) => {
|
const Header = ({ selectedTag }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<XStack justifyContent="space-between" py={'25px'} mt={'4.4rem'}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingBottom: '25px',
|
||||||
|
marginTop: '4.4rem',
|
||||||
|
}}
|
||||||
|
className="header-container"
|
||||||
|
>
|
||||||
<NimbusLogo />
|
<NimbusLogo />
|
||||||
<TagContainer selectedTag={selectedTag} />
|
<TagContainer selectedTag={selectedTag} />
|
||||||
</XStack>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ type LabelInputProps = {
|
|||||||
const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => {
|
const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => {
|
||||||
return (
|
return (
|
||||||
<Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}>
|
<Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}>
|
||||||
<Text size={13} weight="regular" color={'#647084'}>
|
<Text size={13} weight="semibold" color={'#647084'}>
|
||||||
{labelText}
|
{labelText}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
|
@ -26,3 +26,15 @@
|
|||||||
color: #0d1625;
|
color: #0d1625;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.quick-start-bar {
|
||||||
|
top: 0;
|
||||||
|
width: 85%;
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-start-bar ul li {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import './QuickStartBar.css'
|
import styles from './QuickStartBar.module.css'
|
||||||
|
|
||||||
const QuickStartBar = () => {
|
const QuickStartBar = () => {
|
||||||
return (
|
return (
|
||||||
<nav className="quick-start-bar">
|
<nav className={styles['quick-start-bar']}>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -2,44 +2,23 @@ import { Tabs } from '@status-im/components'
|
|||||||
import { Stack } from 'tamagui'
|
import { Stack } from 'tamagui'
|
||||||
|
|
||||||
import ValidatorsList from './ValidatorsList'
|
import ValidatorsList from './ValidatorsList'
|
||||||
import { useMemo } from 'react'
|
import { VALIDATOR_TABS_RIGHT_SIDEBAR } from '../../../../constants'
|
||||||
|
|
||||||
const ValidatorsTabs = () => {
|
const ValidatorsTabs = () => {
|
||||||
const VALIDATOR_TABS = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
label: 'Active',
|
|
||||||
value: 'active',
|
|
||||||
children: <ValidatorsList />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Pending',
|
|
||||||
value: 'pending',
|
|
||||||
children: <ValidatorsList />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Inactive',
|
|
||||||
value: 'inactive',
|
|
||||||
children: <ValidatorsList />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="active">
|
<Tabs defaultValue={VALIDATOR_TABS_RIGHT_SIDEBAR[0]}>
|
||||||
<Stack style={{ cursor: 'pointer', width: 'fit-content' }}>
|
<Stack style={{ cursor: 'pointer', width: 'fit-content' }}>
|
||||||
<Tabs.List size={32}>
|
<Tabs.List size={32}>
|
||||||
{VALIDATOR_TABS.map(tab => (
|
{VALIDATOR_TABS_RIGHT_SIDEBAR.map(tab => (
|
||||||
<Tabs.Trigger key={tab.value} type="default" value={tab.value}>
|
<Tabs.Trigger key={tab} type="default" value={tab}>
|
||||||
{tab.label}
|
{tab}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Stack>
|
</Stack>
|
||||||
{VALIDATOR_TABS.map(tab => (
|
{VALIDATOR_TABS_RIGHT_SIDEBAR.map(tab => (
|
||||||
<Tabs.Content key={tab.value} value={tab.value} style={{ marginTop: '8px' }}>
|
<Tabs.Content key={tab} value={tab} style={{ marginTop: '8px' }}>
|
||||||
{tab.children}
|
<ValidatorsList />
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
|
import SyncStatusCard from './SyncStatusCard'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'ValidatorOnboarding/KeyGenerationSyncCard',
|
title: 'General/SyncStatusCard',
|
||||||
component: KeyGenerationSyncCard,
|
component: SyncStatusCard,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
} satisfies Meta<typeof KeyGenerationSyncCard>
|
} satisfies Meta<typeof SyncStatusCard>
|
||||||
|
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
@ -2,18 +2,18 @@ import { Stack, XStack, YStack } from 'tamagui'
|
|||||||
import { InfoBadgeIcon } from '@status-im/icons'
|
import { InfoBadgeIcon } from '@status-im/icons'
|
||||||
import { Text } from '@status-im/components'
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
import StandardGauge from '../../../../components/Charts/StandardGauge'
|
import StandardGauge from '../Charts/StandardGauge'
|
||||||
import BorderBox from '../../../../components/General/BorderBox'
|
import BorderBox from './BorderBox'
|
||||||
import { formatNumbersWithComa } from '../../../../utilities'
|
import { formatNumbersWithComa } from '../../utilities'
|
||||||
|
|
||||||
type KeyGenerationSyncCardProps = {
|
type SyncStatusCardProps = {
|
||||||
synced: number
|
synced: number
|
||||||
total: number
|
total: number
|
||||||
title: string
|
title: string
|
||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyncCardProps) => {
|
const SyncStatusCard = ({ synced, total, title, color }: SyncStatusCardProps) => {
|
||||||
return (
|
return (
|
||||||
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
|
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
|
||||||
<XStack space={'$2'} alignItems="center">
|
<XStack space={'$2'} alignItems="center">
|
||||||
@ -54,4 +54,4 @@ const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyn
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KeyGenerationSyncCard
|
export default SyncStatusCard
|
@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
|
|||||||
import TitleLogo from './TitleLogo'
|
import TitleLogo from './TitleLogo'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Dashboard/TitleLogo',
|
title: 'General/TitleLogo',
|
||||||
component: TitleLogo,
|
component: TitleLogo,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
@ -15,5 +15,11 @@ export default meta
|
|||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
subtitle: 'Node Management Dashboard',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutSubtitle: Story = {
|
||||||
args: {},
|
args: {},
|
||||||
}
|
}
|
@ -1,7 +1,11 @@
|
|||||||
import { Avatar, Text } from '@status-im/components'
|
import { Avatar, Text } from '@status-im/components'
|
||||||
import { Stack, XStack, YStack } from 'tamagui'
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
|
||||||
const TitleLogo = () => {
|
type TitleLogoProps = {
|
||||||
|
subtitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TitleLogo = ({ subtitle }: TitleLogoProps) => {
|
||||||
return (
|
return (
|
||||||
<XStack space={'$2'}>
|
<XStack space={'$2'}>
|
||||||
<Stack style={{ marginTop: '3px' }}>
|
<Stack style={{ marginTop: '3px' }}>
|
||||||
@ -18,7 +22,7 @@ const TitleLogo = () => {
|
|||||||
Nimbus
|
Nimbus
|
||||||
</Text>
|
</Text>
|
||||||
<Text size={19} color="#647084">
|
<Text size={19} color="#647084">
|
||||||
Node Management Dashboard
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
</XStack>
|
</XStack>
|
19
src/components/General/ValidatorProfile.stories.tsx
Normal file
19
src/components/General/ValidatorProfile.stories.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
}
|
33
src/components/General/ValidatorProfile.tsx
Normal file
33
src/components/General/ValidatorProfile.tsx
Normal 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
|
@ -1,8 +1,9 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import './layout.css'
|
|
||||||
import NimbusLogoMark from '../Logos/NimbusLogoMark'
|
|
||||||
import { useTheme } from 'tamagui'
|
import { useTheme } from 'tamagui'
|
||||||
|
|
||||||
|
import NimbusLogoMark from '../Logos/NimbusLogoMark'
|
||||||
|
import './layout.css'
|
||||||
|
|
||||||
type PageWrapperShadowProps = {
|
type PageWrapperShadowProps = {
|
||||||
breadcrumbBar?: ReactNode
|
breadcrumbBar?: ReactNode
|
||||||
rightImageSrc?: string
|
rightImageSrc?: string
|
||||||
@ -28,7 +29,6 @@ const PageWrapperShadow = ({
|
|||||||
<div className="container-inner">{children}</div>
|
<div className="container-inner">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="layout-right">
|
<section className="layout-right">
|
||||||
<div className="image-container">
|
<div className="image-container">
|
||||||
<img
|
<img
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* padding: 70px 0 0; */
|
|
||||||
}
|
}
|
||||||
.container-inner {
|
.container-inner {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
@ -83,3 +82,85 @@
|
|||||||
.image-container .nimbus-logomark svg {
|
.image-container .nimbus-logomark svg {
|
||||||
height: 73px;
|
height: 73px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.layout {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-left {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
justify-content: start;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-inner {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-right {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
order: 0;
|
||||||
|
margin-top: -10%;
|
||||||
|
margin-bottom: -72%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
margin: 0;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container .background-img {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-5%);
|
||||||
|
clip-path: inset(0 0 85% 0);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container .nimbus-logomark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content,
|
||||||
|
.breadcrumbBar,
|
||||||
|
.other-elements {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbBar,
|
||||||
|
.some-other-element {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container .background-img {
|
||||||
|
clip-path: polygon(0 0, 100% 0, 100% 40%, 0 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container::after {
|
||||||
|
width: 100%;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(to top, rgba(255, 255, 255, 1) 62%, rgba(255, 255, 255, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -45,7 +45,6 @@ export const DEPOSIT_SUBTITLE = 'Connect you Wallet to stake required ETH for ne
|
|||||||
export const CLIENT_SETUP_SUBTITLE = 'How many Validators would you like to run?'
|
export const CLIENT_SETUP_SUBTITLE = 'How many Validators would you like to run?'
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
|
|
||||||
export const years = [
|
export const years = [
|
||||||
'JAN',
|
'JAN',
|
||||||
'FEB',
|
'FEB',
|
||||||
@ -60,3 +59,57 @@ export const years = [
|
|||||||
'NOV',
|
'NOV',
|
||||||
'DEC',
|
'DEC',
|
||||||
]
|
]
|
||||||
|
export const VALIDATOR_TABS_RIGHT_SIDEBAR = ['Active', 'Pending', 'Inactive']
|
||||||
|
|
||||||
|
// Validator Management
|
||||||
|
export const VALIDATOR_TABS_MANAGEMENT = [
|
||||||
|
'Active',
|
||||||
|
'Pending',
|
||||||
|
'Inactive',
|
||||||
|
'Exited',
|
||||||
|
'Withdraw',
|
||||||
|
'All',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const VALIDATORS_DATA = [
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
address: 'zQ3asdf9d4Gs0',
|
||||||
|
balance: 32.0786,
|
||||||
|
income: 0.0786,
|
||||||
|
proposals: '1/102',
|
||||||
|
attestations: '1/102',
|
||||||
|
effectiveness: 98,
|
||||||
|
status: 'Active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
address: 'zQ3asdf9d4Gs0',
|
||||||
|
balance: 32.0786,
|
||||||
|
income: 0.0786,
|
||||||
|
proposals: '1/102',
|
||||||
|
attestations: '1/102',
|
||||||
|
effectiveness: 98,
|
||||||
|
status: 'Active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
address: 'zQ3asdf9d4Gs0',
|
||||||
|
balance: 32.0786,
|
||||||
|
income: 0.0786,
|
||||||
|
proposals: '1/102',
|
||||||
|
attestations: '1/102',
|
||||||
|
effectiveness: 98,
|
||||||
|
status: 'Active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
address: 'zQ3asdf9d4Gs0',
|
||||||
|
balance: 32.0786,
|
||||||
|
income: 0.0786,
|
||||||
|
proposals: '1/102',
|
||||||
|
attestations: '1/102',
|
||||||
|
effectiveness: 98,
|
||||||
|
status: 'Active',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
@ -62,7 +62,6 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
h1,
|
h1,
|
||||||
@ -103,11 +102,13 @@ ul li {
|
|||||||
}
|
}
|
||||||
.transparent-scrollbar::-webkit-scrollbar {
|
.transparent-scrollbar::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparent-scrollbar::-webkit-scrollbar-thumb {
|
.transparent-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparent-scrollbar::-webkit-scrollbar-thumb:hover {
|
.transparent-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
@ -130,3 +131,16 @@ ul li {
|
|||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 440px) {
|
||||||
|
.header-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.right-sidebar-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import BreadcrumbBar from '../../components/General/BreadcrumbBar/BreadcrumbBar'
|
import BreadcrumbBar from '../../components/General/BreadcrumbBar/BreadcrumbBar'
|
||||||
import { Button as StatusButton, Text, Avatar, Checkbox } from '@status-im/components'
|
import { Button as StatusButton, Text, Avatar, Checkbox } from '@status-im/components'
|
||||||
import { Label, Separator, XStack, YStack } from 'tamagui'
|
import { Article, Label, Separator, Stack, XStack, YStack } from 'tamagui'
|
||||||
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
||||||
import Titles from '../../components/General/Titles'
|
import Titles from '../../components/General/Titles'
|
||||||
import LabelInputField from '../../components/General/LabelInputField'
|
import LabelInputField from '../../components/General/LabelInputField'
|
||||||
@ -11,6 +11,30 @@ import { NodeIcon } from '@status-im/icons'
|
|||||||
const ConnectDevicePage = () => {
|
const ConnectDevicePage = () => {
|
||||||
const [autoConnectChecked, setAutoConnectChecked] = useState(false)
|
const [autoConnectChecked, setAutoConnectChecked] = useState(false)
|
||||||
const [portChecked, setPortChecked] = useState(false)
|
const [portChecked, setPortChecked] = useState(false)
|
||||||
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowWidth(window.innerWidth)
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const breakpoint = 768
|
||||||
|
|
||||||
|
const responsiveXStackStyle = {
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexDirection: windowWidth <= breakpoint ? 'column' : 'row',
|
||||||
|
flexWrap: windowWidth <= breakpoint ? 'wrap' : 'nowrap',
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsiveInputStyle = {
|
||||||
|
width: windowWidth <= breakpoint ? '100%' : '40%',
|
||||||
|
marginBottom: windowWidth <= breakpoint ? '1rem' : '0',
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapperShadow
|
<PageWrapperShadow
|
||||||
@ -21,31 +45,23 @@ const ConnectDevicePage = () => {
|
|||||||
<YStack space={'$3'}>
|
<YStack space={'$3'}>
|
||||||
<Header selectedTag="connect" />
|
<Header selectedTag="connect" />
|
||||||
|
|
||||||
<article className="content">
|
<Article className="content">
|
||||||
<Titles
|
<Titles
|
||||||
title="Connect Device"
|
title="Connect Device"
|
||||||
subtitle="Configure your device to connect to the Nimbus Node Manager"
|
subtitle="Configure your device to connect to the Nimbus Node Manager"
|
||||||
/>
|
/>
|
||||||
<YStack my={16}>
|
<YStack my={16}>
|
||||||
<XStack
|
<XStack style={responsiveXStackStyle}>
|
||||||
width={'100%'}
|
<Stack style={responsiveInputStyle}>
|
||||||
alignItems="center"
|
|
||||||
justifyContent="space-between"
|
|
||||||
// media query
|
|
||||||
$lg={{
|
|
||||||
flexDirection: 'column',
|
|
||||||
flexWrap: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XStack width={'40%'}>
|
|
||||||
<LabelInputField labelText="Beacon Address" placeholderText="something" />
|
<LabelInputField labelText="Beacon Address" placeholderText="something" />
|
||||||
</XStack>
|
</Stack>
|
||||||
<XStack width={'25%'}>
|
<Stack style={responsiveInputStyle}>
|
||||||
<LabelInputField labelText="Beacon Node Port" placeholderText="5052" />
|
<LabelInputField labelText="Beacon Node Port" placeholderText="5052" />
|
||||||
</XStack>
|
</Stack>
|
||||||
<XStack width={'25%'}>
|
<Stack style={responsiveInputStyle}>
|
||||||
<LabelInputField labelText="Client Validator Port" placeholderText="5052" />
|
<LabelInputField labelText="Client Validator Port" placeholderText="5052" />
|
||||||
</XStack>
|
</Stack>
|
||||||
|
|
||||||
<YStack width={20}>
|
<YStack width={20}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="port-checkbox"
|
id="port-checkbox"
|
||||||
@ -96,7 +112,7 @@ const ConnectDevicePage = () => {
|
|||||||
<Separator alignSelf="stretch" borderColor={'#F0F2F5'} />
|
<Separator alignSelf="stretch" borderColor={'#F0F2F5'} />
|
||||||
</YStack>
|
</YStack>
|
||||||
<StatusButton icon={<NodeIcon size={20} />}>Connect Device</StatusButton>
|
<StatusButton icon={<NodeIcon size={20} />}>Connect Device</StatusButton>
|
||||||
</article>
|
</Article>
|
||||||
</YStack>
|
</YStack>
|
||||||
</PageWrapperShadow>
|
</PageWrapperShadow>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Stack, YStack } from 'tamagui'
|
import { Stack, YStack, XStack } from 'tamagui'
|
||||||
|
|
||||||
import BasicInfoCards from './BasicInfoCards/BasicInfoCards'
|
import BasicInfoCards from './BasicInfoCards/BasicInfoCards'
|
||||||
import AddCardsContainer from '../../components/General/AddCards/AddCardsContainer'
|
import AddCardsContainer from '../../components/General/AddCards/AddCardsContainer'
|
||||||
import BalanceChartCard from './BalanceChartCard/BalanceChartCard'
|
import BalanceChartCard from './BalanceChartCard/BalanceChartCard'
|
||||||
@ -6,15 +7,16 @@ import CPUCard from './CPULoad/CPUCard'
|
|||||||
import ConsensusUptimeCard from './ConsensusUptime/ConsensusUptimeCard'
|
import ConsensusUptimeCard from './ConsensusUptime/ConsensusUptimeCard'
|
||||||
import ExecutionUptime from './ExecutionUptime/ExecutionUptime'
|
import ExecutionUptime from './ExecutionUptime/ExecutionUptime'
|
||||||
import DeviceUptime from './DeviceUptime/DeviceUptime'
|
import DeviceUptime from './DeviceUptime/DeviceUptime'
|
||||||
import TitleLogo from './TitleLogo'
|
import TitleLogo from '../../components/General/TitleLogo'
|
||||||
import StorageCard from './StorageCard/StorageCard'
|
import StorageCard from './StorageCard/StorageCard'
|
||||||
import NetworkCard from './NetworkCard/NetworkCard'
|
import NetworkCard from './NetworkCard/NetworkCard'
|
||||||
import SyncStatusCard from './SyncStatusCards/SyncStatusCards'
|
import SyncStatusCards from './SyncStatusCards/SyncStatusCards'
|
||||||
import MemoryCard from './MemoryCard/MemoryCard'
|
import MemoryCard from './MemoryCard/MemoryCard'
|
||||||
import { XStack } from 'tamagui'
|
|
||||||
type DashboardContentProps = {
|
type DashboardContentProps = {
|
||||||
windowWidth: number
|
windowWidth: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
|
const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
|
||||||
return (
|
return (
|
||||||
<YStack
|
<YStack
|
||||||
@ -30,7 +32,7 @@ const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
|
|||||||
}}
|
}}
|
||||||
className={'transparent-scrollbar'}
|
className={'transparent-scrollbar'}
|
||||||
>
|
>
|
||||||
<TitleLogo />
|
<TitleLogo subtitle="Node Management Dashboard" />
|
||||||
<Stack
|
<Stack
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -39,8 +41,8 @@ const DashboardContent = ({ windowWidth }: DashboardContentProps) => {
|
|||||||
gridAutoFlow: 'row',
|
gridAutoFlow: 'row',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SyncStatusCard />
|
<SyncStatusCards />
|
||||||
<AddCardsContainer />
|
<AddCardsContainer cardsAmount={2} />
|
||||||
{windowWidth < 1375 ? (
|
{windowWidth < 1375 ? (
|
||||||
<Stack style={{ gridColumn: '1 / span 2' }} width={'101%'}>
|
<Stack style={{ gridColumn: '1 / span 2' }} width={'101%'}>
|
||||||
<BalanceChartCard />
|
<BalanceChartCard />
|
||||||
|
@ -5,7 +5,7 @@ import DashboardCardWrapper from '../DashboardCardWrapper'
|
|||||||
import ExecutionClientCard from './ExecutionClientCard'
|
import ExecutionClientCard from './ExecutionClientCard'
|
||||||
import ConsensusCard from './ConsensusClientCard'
|
import ConsensusCard from './ConsensusClientCard'
|
||||||
|
|
||||||
const SyncStatusCard = () => {
|
const SyncStatusCards = () => {
|
||||||
return (
|
return (
|
||||||
<DashboardCardWrapper padding="0" minWidth="50px">
|
<DashboardCardWrapper padding="0" minWidth="50px">
|
||||||
<YStack space={'$2'}>
|
<YStack space={'$2'}>
|
||||||
@ -24,4 +24,4 @@ const SyncStatusCard = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SyncStatusCard
|
export default SyncStatusCards
|
||||||
|
@ -11,9 +11,28 @@ import DeviceNetworkHealth from '../../components/Charts/DeviceNetworkHealth'
|
|||||||
import { CloseCircleIcon } from '@status-im/icons'
|
import { CloseCircleIcon } from '@status-im/icons'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { RootState } from '../../redux/store'
|
import { RootState } from '../../redux/store'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const DeviceHealthCheck = () => {
|
const DeviceHealthCheck = () => {
|
||||||
const deviceHealthState = useSelector((state: RootState) => state.deviceHealth)
|
const deviceHealthState = useSelector((state: RootState) => state.deviceHealth)
|
||||||
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowWidth(window.innerWidth)
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
const breakpoint = 768
|
||||||
|
|
||||||
|
const responsiveStyle = {
|
||||||
|
flexWrap: windowWidth <= breakpoint ? 'wrap' : 'nowrap',
|
||||||
|
flexDirection: windowWidth <= breakpoint ? 'column' : 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: windowWidth <= breakpoint ? '200%' : '100%',
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapperShadow rightImageSrc="./background-images/eye-background.png" imgHeight="100%">
|
<PageWrapperShadow rightImageSrc="./background-images/eye-background.png" imgHeight="100%">
|
||||||
@ -33,14 +52,14 @@ const DeviceHealthCheck = () => {
|
|||||||
subtitle="Configure your device to start Staking on Nimbus"
|
subtitle="Configure your device to start Staking on Nimbus"
|
||||||
isAdvancedSettings={true}
|
isAdvancedSettings={true}
|
||||||
/>
|
/>
|
||||||
<XStack space={'$4'} width={'100%'}>
|
<XStack space={'$4'} style={responsiveStyle}>
|
||||||
<DeviceStorageHealth
|
<DeviceStorageHealth
|
||||||
storage={deviceHealthState.storage}
|
storage={deviceHealthState.storage}
|
||||||
maxStorage={deviceHealthState.maxMemory}
|
maxStorage={deviceHealthState.maxMemory}
|
||||||
/>
|
/>
|
||||||
<DeviceCPULoad load={deviceHealthState.cpuLoad} />
|
<DeviceCPULoad load={deviceHealthState.cpuLoad} />
|
||||||
</XStack>
|
</XStack>
|
||||||
<XStack space={'$4'} width={'100%'}>
|
<XStack space={'$4'} style={responsiveStyle}>
|
||||||
<DeviceMemory
|
<DeviceMemory
|
||||||
currentMemory={deviceHealthState.memory}
|
currentMemory={deviceHealthState.memory}
|
||||||
maxMemory={deviceHealthState.maxMemory}
|
maxMemory={deviceHealthState.maxMemory}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
.landing-page {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
13
src/pages/LandingPage/LandingPage.module.css
Normal file
13
src/pages/LandingPage/LandingPage.module.css
Normal file
@ -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 { XStack, YStack } from 'tamagui'
|
||||||
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
|
||||||
import NimbusLogo from '../../components/Logos/NimbusLogo'
|
|
||||||
import { NodeIcon } from '@status-im/icons'
|
import { NodeIcon } from '@status-im/icons'
|
||||||
import { Button as StatusButton, Text } from '@status-im/components'
|
import { Button as StatusButton, Text } from '@status-im/components'
|
||||||
|
|
||||||
|
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
||||||
import QuickStartBar from '../../components/General/QuickStartBar/QuickStartBar'
|
import QuickStartBar from '../../components/General/QuickStartBar/QuickStartBar'
|
||||||
import { useNavigate } from 'react-router'
|
import NimbusLogo from '../../components/Logos/NimbusLogo'
|
||||||
|
import styles from './LandingPage.module.css'
|
||||||
|
|
||||||
const LandingPage = () => {
|
const LandingPage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const getStartedHanlder = () => {
|
const onGetStartedHandler = () => {
|
||||||
navigate('/pair-device')
|
navigate('/pair-device')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageWrapperShadow rightImageSrc="./background-images/landing-page-bg.png" imgHeight="150%">
|
<PageWrapperShadow rightImageSrc="./background-images/landing-page-bg.png" imgHeight="150%">
|
||||||
<YStack className="landing-page">
|
<YStack className={styles['landing-page']}>
|
||||||
<XStack pt={'70px'}>
|
<XStack pt={'70px'}>
|
||||||
<NimbusLogo />
|
<NimbusLogo />
|
||||||
</XStack>
|
</XStack>
|
||||||
<YStack style={{ width: '100%', margin: '30vh 0 4vh' }} space={'16px'}>
|
<YStack className={styles['landing-texts']} space={'16px'}>
|
||||||
<Text size={27} weight={'semibold'}>
|
<Text size={27} weight={'semibold'}>
|
||||||
Light and performant clients, for all Ethereum validators.
|
Light and performant clients, for all Ethereum validators.
|
||||||
</Text>
|
</Text>
|
||||||
@ -30,9 +31,8 @@ const LandingPage = () => {
|
|||||||
you wish to run in a completely trustless and decentralized manner.
|
you wish to run in a completely trustless and decentralized manner.
|
||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<XStack>
|
<XStack>
|
||||||
<StatusButton icon={<NodeIcon size={20} />} onPress={getStartedHanlder}>
|
<StatusButton icon={<NodeIcon size={20} />} onPress={onGetStartedHandler}>
|
||||||
Get Started
|
Get Started
|
||||||
</StatusButton>
|
</StatusButton>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
import GenerateId from './GenerateId'
|
import GenerateId from './GenerateId'
|
||||||
import { withRouter } from 'storybook-addon-react-router-v6'
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Pair Device/GenerateId',
|
title: 'Pair Device/GenerateId',
|
||||||
|
@ -3,9 +3,11 @@ import { CompleteIdIcon, CopyIcon } from '@status-im/icons'
|
|||||||
import { Text } from '@tamagui/web'
|
import { Text } from '@tamagui/web'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Separator, XStack, YStack } from 'tamagui'
|
import { Separator, YStack } from 'tamagui'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import styles from './pairDevice.module.css'
|
||||||
|
|
||||||
type GenerateIdProps = {
|
type GenerateIdProps = {
|
||||||
isAwaitingPairing: boolean
|
isAwaitingPairing: boolean
|
||||||
}
|
}
|
||||||
@ -27,7 +29,10 @@ const GenerateId = ({ isAwaitingPairing }: GenerateIdProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack space={'$2'}>
|
<YStack space={'$2'}>
|
||||||
<XStack style={{ justifyContent: 'space-between' }}>
|
<div
|
||||||
|
style={{ display: 'flex', justifyContent: 'space-between' }}
|
||||||
|
className={styles['regenerate-id-container']}
|
||||||
|
>
|
||||||
<StatusText size={19} weight={'semibold'}>
|
<StatusText size={19} weight={'semibold'}>
|
||||||
Pair with Command line
|
Pair with Command line
|
||||||
</StatusText>
|
</StatusText>
|
||||||
@ -39,7 +44,7 @@ const GenerateId = ({ isAwaitingPairing }: GenerateIdProps) => {
|
|||||||
>
|
>
|
||||||
Regenerate ID
|
Regenerate ID
|
||||||
</Button>
|
</Button>
|
||||||
</XStack>
|
</div>
|
||||||
<YStack space={'$2'}>
|
<YStack space={'$2'}>
|
||||||
<StatusText size={15} color={'#647084'}>
|
<StatusText size={15} color={'#647084'}>
|
||||||
Generated Pairing ID Input
|
Generated Pairing ID Input
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { NodeIcon } from '@status-im/icons'
|
||||||
import { Separator, XStack, YStack } from 'tamagui'
|
import { Separator, XStack, YStack } from 'tamagui'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button, Text } from '@status-im/components'
|
import { Button, Text } from '@status-im/components'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
||||||
import SyncStatus from './SyncStatus'
|
import SyncStatus from './SyncStatus'
|
||||||
@ -8,53 +10,65 @@ import Titles from '../../components/General/Titles'
|
|||||||
import PairedSuccessfully from './PairedSuccessfully'
|
import PairedSuccessfully from './PairedSuccessfully'
|
||||||
import CreateAvatar from '../../components/General/CreateAvatar/CreateAvatar'
|
import CreateAvatar from '../../components/General/CreateAvatar/CreateAvatar'
|
||||||
import GenerateId from './GenerateId'
|
import GenerateId from './GenerateId'
|
||||||
import { NodeIcon } from '@status-im/icons'
|
|
||||||
import Header from '../../components/General/Header'
|
import Header from '../../components/General/Header'
|
||||||
import Icon from '../../components/General/Icon'
|
import Icon from '../../components/General/Icon'
|
||||||
|
|
||||||
const PairDevice = () => {
|
const PairDevice = () => {
|
||||||
const [isAwaitingPairing, setIsAwaitingPairing] = useState(false)
|
const [isAwaitingPairing, setIsAwaitingPairing] = useState(false)
|
||||||
|
const navigate = useNavigate()
|
||||||
const isPaired = false
|
const isPaired = false
|
||||||
const isPairing = true
|
const isPairing = false
|
||||||
|
|
||||||
const changeSetIsAwaitingPairing = (result: boolean) => {
|
const changeSetIsAwaitingPairing = (result: boolean) => {
|
||||||
setIsAwaitingPairing(result)
|
setIsAwaitingPairing(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectViaIpHandler = () => {
|
||||||
|
navigate('/connect-device')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapperShadow rightImageSrc="./background-images/day-night-bg.png" rightImageLogo={true}>
|
<PageWrapperShadow rightImageSrc="./background-images/day-night-bg.png" rightImageLogo={true}>
|
||||||
<YStack
|
<YStack space={'$3'}>
|
||||||
space={'$3'}
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Header selectedTag="pair" />
|
<Header selectedTag="pair" />
|
||||||
<Titles title="Pair Device" subtitle="Pair your device to the Nimbus Node Manager" />
|
<Titles
|
||||||
|
title="Connect to existing Nimbus Instance"
|
||||||
|
subtitle="Pair your existing device to the Nimbus Node Manager"
|
||||||
|
/>
|
||||||
{isPaired ? <PairedSuccessfully /> : <GenerateId isAwaitingPairing={isAwaitingPairing} />}
|
{isPaired ? <PairedSuccessfully /> : <GenerateId isAwaitingPairing={isAwaitingPairing} />}
|
||||||
{!isPaired && (
|
{isPaired === false && (
|
||||||
<SyncStatus
|
<SyncStatus
|
||||||
isPairing={isPairing}
|
isPairing={isPairing}
|
||||||
isAwaitingPairing={isAwaitingPairing}
|
isAwaitingPairing={isAwaitingPairing}
|
||||||
changeSetIsAwaitingPairing={changeSetIsAwaitingPairing}
|
changeSetIsAwaitingPairing={changeSetIsAwaitingPairing}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Separator borderColor={'#e3e3e3'} />
|
{isPaired === false && (
|
||||||
<Text size={19} weight={'semibold'} color="#09101C">
|
<YStack space={'$3'}>
|
||||||
Advanced Settings
|
<Separator borderColor={'#e3e3e3'} />
|
||||||
</Text>
|
<YStack space={'$1'}>
|
||||||
<XStack space={'$4'}>
|
<Text size={19} weight={'semibold'} color="#09101C">
|
||||||
<Button icon={<Icon src="/icons/connection-blue.svg" width={20} />} variant="outline">
|
Advanced Settings
|
||||||
Connect via IP
|
</Text>
|
||||||
</Button>
|
<XStack>
|
||||||
</XStack>
|
<Button
|
||||||
|
icon={<Icon src="/icons/connection-blue.svg" width={20} />}
|
||||||
|
variant="outline"
|
||||||
|
onPress={connectViaIpHandler}
|
||||||
|
>
|
||||||
|
Connect via IP
|
||||||
|
</Button>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
)}
|
||||||
{isPaired && <CreateAvatar />}
|
{isPaired && <CreateAvatar />}
|
||||||
<Separator borderColor={'#e3e3e3'} />
|
<Separator borderColor={'#e3e3e3'} />
|
||||||
<XStack>
|
<div>
|
||||||
<Button icon={<NodeIcon size={20} />} disabled={!isPaired}>
|
<Button icon={<NodeIcon size={20} />} disabled={!isPaired}>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</XStack>
|
</div>
|
||||||
</YStack>
|
</YStack>
|
||||||
</PageWrapperShadow>
|
</PageWrapperShadow>
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
import SyncStatus from './SyncStatus'
|
import SyncStatus from './SyncStatus'
|
||||||
import { withRouter } from 'storybook-addon-react-router-v6'
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Pair Device/SyncStatus',
|
title: 'Pair Device/SyncStatus',
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { XStack, YStack } from 'tamagui'
|
import { XStack, YStack } from 'tamagui'
|
||||||
import { Button, IconButton, InformationBox, Text } from '@status-im/components'
|
import { IconButton, InformationBox, Text } from '@status-im/components'
|
||||||
import { CloseCircleIcon } from '@status-im/icons'
|
import { CloseCircleIcon } from '@status-im/icons'
|
||||||
|
|
||||||
import Icon from '../../components/General/Icon'
|
|
||||||
import ConnectionIcon from '/icons/connection.svg'
|
|
||||||
import { convertSecondsToTimerFormat } from '../../utilities'
|
|
||||||
import { RefreshIcon } from '@status-im/icons'
|
import { RefreshIcon } from '@status-im/icons'
|
||||||
import { useNavigate } from 'react-router'
|
|
||||||
|
import { convertSecondsToTimerFormat } from '../../utilities'
|
||||||
|
|
||||||
type SyncStatusProps = {
|
type SyncStatusProps = {
|
||||||
isPairing: boolean
|
isPairing: boolean
|
||||||
@ -21,7 +18,6 @@ const SyncStatus = ({
|
|||||||
changeSetIsAwaitingPairing,
|
changeSetIsAwaitingPairing,
|
||||||
}: SyncStatusProps) => {
|
}: SyncStatusProps) => {
|
||||||
const [elapsedTime, setElapsedTime] = useState(0)
|
const [elapsedTime, setElapsedTime] = useState(0)
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const resetTimer = () => {
|
const resetTimer = () => {
|
||||||
setElapsedTime(0)
|
setElapsedTime(0)
|
||||||
@ -33,7 +29,7 @@ const SyncStatus = ({
|
|||||||
|
|
||||||
if (isPairing) {
|
if (isPairing) {
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
setElapsedTime(prevTime => prevTime + 65)
|
setElapsedTime(prevTime => prevTime + 1000)
|
||||||
if (elapsedTime >= 180) {
|
if (elapsedTime >= 180) {
|
||||||
changeSetIsAwaitingPairing(true)
|
changeSetIsAwaitingPairing(true)
|
||||||
}
|
}
|
||||||
@ -47,19 +43,15 @@ const SyncStatus = ({
|
|||||||
|
|
||||||
const timer = convertSecondsToTimerFormat(elapsedTime)
|
const timer = convertSecondsToTimerFormat(elapsedTime)
|
||||||
|
|
||||||
const connectViaIpHandler = () => {
|
|
||||||
navigate('/connect-device')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack space={'$2'}>
|
<YStack>
|
||||||
<XStack style={{ justifyContent: 'space-between' }}>
|
<XStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Text size={11} color="#647084" weight="medium">
|
<Text size={11} color="#647084" weight="medium">
|
||||||
Device Sync Status
|
Device Sync Status
|
||||||
</Text>
|
</Text>
|
||||||
{isPairing && (
|
{isPairing && (
|
||||||
<Text
|
<Text
|
||||||
size={isAwaitingPairing ? 15 : 11}
|
size={13}
|
||||||
color={isAwaitingPairing ? '#EB5757' : '#647084'}
|
color={isAwaitingPairing ? '#EB5757' : '#647084'}
|
||||||
weight={isAwaitingPairing && 'semibold'}
|
weight={isAwaitingPairing && 'semibold'}
|
||||||
>
|
>
|
||||||
@ -89,13 +81,6 @@ const SyncStatus = ({
|
|||||||
icon={<CloseCircleIcon size={20} />}
|
icon={<CloseCircleIcon size={20} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAwaitingPairing && (
|
|
||||||
<XStack>
|
|
||||||
<Button icon={<Icon src={ConnectionIcon} />} size={40} onPress={connectViaIpHandler}>
|
|
||||||
Connect via IP
|
|
||||||
</Button>
|
|
||||||
</XStack>
|
|
||||||
)}
|
|
||||||
</YStack>
|
</YStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
6
src/pages/PairDevice/pairDevice.module.css
Normal file
6
src/pages/PairDevice/pairDevice.module.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@media screen and (max-width: 440px) {
|
||||||
|
.regenerate-id-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
19
src/pages/ValidatorManagement/ManagementCard.stories.ts
Normal file
19
src/pages/ValidatorManagement/ManagementCard.stories.ts
Normal 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: {},
|
||||||
|
}
|
31
src/pages/ValidatorManagement/ManagementCard.tsx
Normal file
31
src/pages/ValidatorManagement/ManagementCard.tsx
Normal 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
|
19
src/pages/ValidatorManagement/ManagementHeader.stories.ts
Normal file
19
src/pages/ValidatorManagement/ManagementHeader.stories.ts
Normal 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: {},
|
||||||
|
}
|
39
src/pages/ValidatorManagement/ManagementHeader.tsx
Normal file
39
src/pages/ValidatorManagement/ManagementHeader.tsx
Normal 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
|
@ -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
|
19
src/pages/ValidatorManagement/ManagementTabs.stories.ts
Normal file
19
src/pages/ValidatorManagement/ManagementTabs.stories.ts
Normal 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: {},
|
||||||
|
}
|
43
src/pages/ValidatorManagement/ManagementTabs.tsx
Normal file
43
src/pages/ValidatorManagement/ManagementTabs.tsx
Normal 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
|
19
src/pages/ValidatorManagement/ValidatorManagement.stories.ts
Normal file
19
src/pages/ValidatorManagement/ValidatorManagement.stories.ts
Normal 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: {},
|
||||||
|
}
|
@ -1,7 +1,20 @@
|
|||||||
import { YStack } from 'tamagui'
|
import { XStack } from 'tamagui'
|
||||||
|
|
||||||
|
import ValidatorManagementContent from './ValidatorManagementContent'
|
||||||
|
import LeftSidebar from '../../components/General/LeftSidebar/LeftSidebar'
|
||||||
|
import RightSidebar from '../../components/General/RightSideBar/RightSidebar'
|
||||||
|
import './validatorManagement.css'
|
||||||
|
|
||||||
const ValidatorManagement = () => {
|
const ValidatorManagement = () => {
|
||||||
return <YStack></YStack>
|
return (
|
||||||
|
<XStack style={{ height: '100vh' }}>
|
||||||
|
<LeftSidebar />
|
||||||
|
<ValidatorManagementContent />
|
||||||
|
<div className="right-sidebar-wrapper">
|
||||||
|
<RightSidebar />
|
||||||
|
</div>
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValidatorManagement
|
export default ValidatorManagement
|
||||||
|
@ -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: {},
|
||||||
|
}
|
44
src/pages/ValidatorManagement/ValidatorManagementContent.tsx
Normal file
44
src/pages/ValidatorManagement/ValidatorManagementContent.tsx
Normal 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
|
94
src/pages/ValidatorManagement/validatorManagement.css
Normal file
94
src/pages/ValidatorManagement/validatorManagement.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { Avatar, DividerLine, Text } from '@status-im/components'
|
import { DividerLine, Text } from '@status-im/components'
|
||||||
import { XStack, YStack } from 'tamagui'
|
import { XStack, YStack } from 'tamagui'
|
||||||
|
|
||||||
import { getFormattedValidatorAddress } from '../../../../utilities'
|
|
||||||
import TransactionStatus from './TransactionStatus'
|
import TransactionStatus from './TransactionStatus'
|
||||||
|
import ValidatorProfile from '../../../../components/General/ValidatorProfile'
|
||||||
|
|
||||||
type ValidatorRequestProps = {
|
type ValidatorRequestProps = {
|
||||||
number: number
|
number: number
|
||||||
@ -17,23 +17,7 @@ const ValidatorRequest = ({ number, isTransactionConfirmation }: ValidatorReques
|
|||||||
<YStack space={'$3'} style={{ width: '100%' }}>
|
<YStack space={'$3'} style={{ width: '100%' }}>
|
||||||
<XStack style={{ justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
|
<XStack style={{ justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
|
||||||
<XStack style={{ justifyContent: 'space-between', width: '44%', alignItems: 'center' }}>
|
<XStack style={{ justifyContent: 'space-between', width: '44%', alignItems: 'center' }}>
|
||||||
<XStack space={'$2'}>
|
<ValidatorProfile number={number} address={'zQ3asdf9d4Gs0'} />
|
||||||
<Avatar
|
|
||||||
type="user"
|
|
||||||
size={32}
|
|
||||||
src="/icons/validator-request.svg"
|
|
||||||
name={number.toString()}
|
|
||||||
indicator="online"
|
|
||||||
/>
|
|
||||||
<YStack>
|
|
||||||
<Text size={13} weight={'semibold'}>
|
|
||||||
Validator {number}
|
|
||||||
</Text>
|
|
||||||
<Text size={13} color="#647084">
|
|
||||||
{getFormattedValidatorAddress('zQ3asdf9d4Gs0')}
|
|
||||||
</Text>
|
|
||||||
</YStack>
|
|
||||||
</XStack>
|
|
||||||
<Text size={13} color="#647084" weight={'semibold'}>
|
<Text size={13} color="#647084" weight={'semibold'}>
|
||||||
Keys Generated
|
Keys Generated
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { XStack } from 'tamagui'
|
import { XStack } from 'tamagui'
|
||||||
|
|
||||||
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
|
import SyncStatusCard from '../../../../components/General/SyncStatusCard'
|
||||||
import KeyGenerationTitle from '../KeyGenerationTitle'
|
import KeyGenerationTitle from '../KeyGenerationTitle'
|
||||||
|
|
||||||
const KeyGenerationHeader = () => {
|
const KeyGenerationHeader = () => {
|
||||||
@ -8,13 +8,13 @@ const KeyGenerationHeader = () => {
|
|||||||
<XStack style={{ width: '100%', justifyContent: 'space-between' }}>
|
<XStack style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<KeyGenerationTitle />
|
<KeyGenerationTitle />
|
||||||
<XStack space={'$2'}>
|
<XStack space={'$2'}>
|
||||||
<KeyGenerationSyncCard
|
<SyncStatusCard
|
||||||
synced={123.524}
|
synced={123.524}
|
||||||
total={172.503}
|
total={172.503}
|
||||||
title="Execution Sync Status"
|
title="Execution Sync Status"
|
||||||
color="#2a4af5"
|
color="#2a4af5"
|
||||||
/>
|
/>
|
||||||
<KeyGenerationSyncCard
|
<SyncStatusCard
|
||||||
synced={123.524}
|
synced={123.524}
|
||||||
total={172.503}
|
total={172.503}
|
||||||
title="Consensus Sync Status"
|
title="Consensus Sync Status"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user