Skip to content

Commit

Permalink
Add node status page
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Feb 22, 2024
1 parent ad3eab6 commit 6c8f163
Show file tree
Hide file tree
Showing 28 changed files with 1,612 additions and 934 deletions.
8 changes: 8 additions & 0 deletions apps/node-status/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ['custom'],
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
};
18 changes: 18 additions & 0 deletions apps/node-status/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Penumbra node status page

![Screenshot 2024-02-22 at 1 21 54 PM](https://github.com/penumbra-zone/web/assets/16624263/7422ff48-fe33-4f16-a13f-4e109998c7ec)

### Overview

This static site serves as a status page for the Penumbra node,
displaying output from [GetStatus](https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.util.tendermint_proxy.v1#penumbra.util.tendermint_proxy.v1.TendermintProxyService.GetStatus) rpc method
and linking to minifront. Designed to be hosted by PD.

### Run

```
pnpm install
pnpm dev # for local development
pnpm build # for getting build output for deployment on pd
```
13 changes: 13 additions & 0 deletions apps/node-status/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link href="./favicon.png" rel="icon" sizes="80x80" type="image/png" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Penumbra Node Status</title>
</head>
<body>
<div id="root"></div>
<script src="./src/main.tsx" type="module"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions apps/node-status/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "node-status",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx",
"preview": "vite preview"
},
"dependencies": {
"@penumbra-zone/crypto-web": "workspace:*",
"@penumbra-zone/transport": "workspace:*",
"@penumbra-zone/ui": "workspace:*",
"date-fns": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.22.1",
"tailwindcss": "^3.4.1"
},
"devDependencies": {
"@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.3.3",
"vite": "^5.1.4"
}
}
1 change: 1 addition & 0 deletions apps/node-status/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@penumbra-zone/ui/postcss.config.js';
Binary file added apps/node-status/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions apps/node-status/public/penumbra-rays.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/node-status/public/penumbra-text-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions apps/node-status/src/clients/grpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { createPromiseClient } from '@connectrpc/connect';
import { TendermintProxyService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/util/tendermint_proxy/v1/tendermint_proxy_connect';
import { devBaseUrl, prodBaseUrl } from '../constants.ts';

const transport = createGrpcWebTransport({
baseUrl: import.meta.env.MODE === 'production' ? prodBaseUrl : devBaseUrl,
});

export const tendermintClient = createPromiseClient(TendermintProxyService, transport);
13 changes: 13 additions & 0 deletions apps/node-status/src/components/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useRouteError } from 'react-router-dom';

export const ErrorBoundary = () => {
const error = useRouteError();

console.error(error);

return (
<div className='text-red'>
<h1 className='text-xl'>{String(error)}</h1>
</div>
);
};
14 changes: 14 additions & 0 deletions apps/node-status/src/components/frontend-referral.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Button } from '@penumbra-zone/ui';
import { devFrontend, prodFrontend } from '../constants.ts';

export const FrontendReferral = () => {
const onClickHandler = () => {
window.open(import.meta.env.MODE === 'production' ? prodFrontend : devFrontend);
};

return (
<Button variant='gradient' onClick={onClickHandler} className='w-full'>
Frontend app
</Button>
);
};
41 changes: 41 additions & 0 deletions apps/node-status/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Link } from 'react-router-dom';
import { LineWave } from 'react-loader-spinner';
import { cn } from '@penumbra-zone/ui/lib/utils.ts';
import { useDeceleratedFetchState } from '../fetching/refetch-hook.ts';

export const Header = () => {
const isLoading = useDeceleratedFetchState();

return (
<header className='z-10 flex w-full flex-col items-center justify-between px-6 md:h-[82px] md:flex-row md:gap-12 md:px-12'>
<div className='mb-[30px] md:mb-0'>
<img
src='./penumbra-rays.svg'
alt='Penumbra logo'
className='absolute inset-x-0 top-[-75px] mx-auto h-[141px] w-[136px] rotate-[320deg] md:left-[-100px] md:top-[-140px] md:mx-0 md:size-[234px]'
/>
<Link to='/'>
<img
src='./penumbra-text-logo.svg'
alt='Penumbra logo'
className='relative z-10 mt-[20px] h-4 w-[171px] md:mt-0'
/>
</Link>
</div>
<div className='ml-[78px] flex items-center text-xl font-semibold'>
<span>Node Status</span>
<LineWave
visible={true}
height='70'
width='70'
color='white'
wrapperClass={cn(
'mb-5 transition-all duration-300',
isLoading ? 'opacity-100' : 'opacity-0',
)}
/>
</div>
<div className='w-[171px]' />
</header>
);
};
26 changes: 26 additions & 0 deletions apps/node-status/src/components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Header } from './header.tsx';
import { FrontendReferral } from './frontend-referral.tsx';
import { NodeInfo } from './node-info.tsx';
import { SyncInfo } from './sync-info.tsx';
import { ValidatorInfo } from './validator-info.tsx';
import { useRefetchStatusOnInterval } from '../fetching/refetch-hook.ts';

export const Index = () => {
useRefetchStatusOnInterval();

return (
<>
<Header />
<div className='mx-auto max-w-[900px] px-6'>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6'>
<SyncInfo />
<NodeInfo />
<div className='flex flex-col gap-4'>
<ValidatorInfo />
<FrontendReferral />
</div>
</div>
</div>
</>
);
};
62 changes: 62 additions & 0 deletions apps/node-status/src/components/node-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useLoaderData } from 'react-router-dom';
import { uint8ArrayToBase64 } from '@penumbra-zone/types';
import { Card, Identicon } from '@penumbra-zone/ui';
import { IndexLoaderResponse } from '../fetching/loader.ts';

export const NodeInfo = () => {
const {
status: { nodeInfo },
} = useLoaderData() as IndexLoaderResponse;
if (!nodeInfo) return <></>;

return (
<Card gradient className='flex flex-col gap-1'>
<div className='mb-2 flex flex-col gap-1'>
<strong>Network</strong>
<div className='flex items-center gap-2'>
<Identicon
uniqueIdentifier={nodeInfo.network}
type='gradient'
className='rounded-full'
size={14}
/>
<span className='text-2xl font-bold'>{nodeInfo.network}</span>
</div>
<strong>Version</strong>
<span className='text-2xl font-bold'>{nodeInfo.version}</span>
</div>
<div className='flex flex-col'>
<strong>Default Node ID</strong>
<span className='ml-2'>{nodeInfo.defaultNodeId}</span>
</div>
{nodeInfo.protocolVersion && (
<div className='flex flex-col'>
<strong>Protocol Version</strong>
<span className='ml-2'>Block: {nodeInfo.protocolVersion.block.toString()}</span>
<span className='ml-2'>P2P: {nodeInfo.protocolVersion.p2p.toString()}</span>
<span className='ml-2'>App: {nodeInfo.protocolVersion.app.toString()}</span>
</div>
)}
<div className='flex flex-col'>
<strong>Listen Address</strong>
<span className='ml-2'>{nodeInfo.listenAddr}</span>
</div>
<div className='flex flex-col'>
<strong>Channels</strong>
<span className='ml-2'>{uint8ArrayToBase64(nodeInfo.channels)}</span>
</div>
<div className='flex flex-col'>
<strong>Moniker</strong>
<span className='ml-2'>{nodeInfo.moniker}</span>
</div>
{nodeInfo.other && (
<div className='flex flex-col'>
<strong>Transaction Index</strong>
<span className='ml-2'>{nodeInfo.other.txIndex}</span>
<strong>RPC Address</strong>
<span className='ml-2'>{nodeInfo.other.rpcAddress}</span>
</div>
)}
</Card>
);
};
13 changes: 13 additions & 0 deletions apps/node-status/src/components/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createHashRouter } from 'react-router-dom';
import { ErrorBoundary } from './error-boundary.tsx';
import { Index } from './index.tsx';
import { IndexLoader } from '../fetching/loader.ts';

export const router = createHashRouter([
{
path: '/',
loader: IndexLoader,
element: <Index />,
errorElement: <ErrorBoundary />,
},
]);
65 changes: 65 additions & 0 deletions apps/node-status/src/components/sync-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useLoaderData } from 'react-router-dom';
import { IndexLoaderResponse } from '../fetching/loader.ts';
import { Card } from '@penumbra-zone/ui';
import { format } from 'date-fns';
import { SyncInfo as SyncInfoProto } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/util/tendermint_proxy/v1/tendermint_proxy_pb';

const getFormattedTime = (syncInfo: SyncInfoProto): { date?: string; time?: string } => {
const dateObj = syncInfo.latestBlockTime?.toDate();
if (!dateObj) return {};

const date = format(dateObj, 'EEE MMM dd yyyy');
const time = format(dateObj, "HH:mm:ss 'GMT'x");

return { date, time };
};

export const SyncInfo = () => {
const {
status: { syncInfo },
latestBlockHash,
latestAppHash,
} = useLoaderData() as IndexLoaderResponse;
if (!syncInfo) return <></>;

const { date, time } = getFormattedTime(syncInfo);

return (
<Card gradient className='flex flex-col gap-2 md:col-span-2'>
<div className='flex justify-between gap-2'>
<div className='flex flex-col gap-2'>
<strong>Latest Block Height</strong>{' '}
<span className='text-4xl font-bold'>{syncInfo.latestBlockHeight.toString()}</span>
</div>
<div className='flex flex-col items-center gap-2'>
<strong>Caught Up</strong>{' '}
{syncInfo.catchingUp ? (
<div className='flex w-12 items-center justify-center rounded bg-red-700 p-1'>
False
</div>
) : (
<div className='flex w-12 items-center justify-center rounded bg-green-700 p-1'>
True
</div>
)}
</div>
<div className='flex flex-col'>
<strong>Latest Block Time</strong>
<span className='text-xl font-bold'>{date}</span>
<span className='text-xl font-bold'>{time}</span>
</div>
</div>

<div>
<div>
<strong>Latest Block Hash: </strong>
<span className='break-all'>{latestBlockHash}</span>
</div>
<div>
<strong>Latest App Hash: </strong>
<span className='break-all'>{latestAppHash}</span>
</div>
</div>
</Card>
);
};
48 changes: 48 additions & 0 deletions apps/node-status/src/components/validator-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useLoaderData } from 'react-router-dom';
import { uint8ArrayToHex, uint8ArrayToString } from '@penumbra-zone/types';
import { Card } from '@penumbra-zone/ui';
import { IndexLoaderResponse } from '../fetching/loader';
import { PublicKey } from '@buf/tendermint_tendermint.bufbuild_es/tendermint/crypto/keys_pb';

const PublicKeyComponent = ({ publicKey }: { publicKey: PublicKey | undefined }) => {
if (!publicKey) return null;

const publicKeyType = publicKey.sum.case;
const value = publicKey.sum.value ? uint8ArrayToHex(publicKey.sum.value) : undefined;

return (
<div className='flex flex-col'>
<strong>Public Key</strong>
<span className='ml-2'>Type: {publicKeyType}</span>
<span className='ml-2 break-all'>Value: {value}</span>
</div>
);
};

export const ValidatorInfo = () => {
const {
status: { validatorInfo },
} = useLoaderData() as IndexLoaderResponse;
if (!validatorInfo) return <></>;

return (
// Outer div used to shrink to size instead of expand to sibling's size
<div className='flex flex-col justify-start'>
<Card gradient>
<div className='flex flex-col'>
<strong>Voting Power</strong>
<span className='ml-2'>{validatorInfo.votingPower.toString()}</span>
</div>
<div className='flex flex-col'>
<strong>Proposer Priority</strong>
<span className='ml-2'>{validatorInfo.proposerPriority.toString()}</span>
</div>
<div className='flex flex-col'>
<strong>Address</strong>
<span className='ml-2 break-all'>{uint8ArrayToString(validatorInfo.address)}</span>
</div>
<PublicKeyComponent publicKey={validatorInfo.pubKey} />
</Card>
</div>
);
};
5 changes: 5 additions & 0 deletions apps/node-status/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const prodBaseUrl = '/';
export const devBaseUrl = 'https://grpc.testnet.penumbra.zone';

export const prodFrontend = '/app/';
export const devFrontend = 'https://app.testnet.penumbra.zone';
Loading

0 comments on commit 6c8f163

Please sign in to comment.