Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node status page #592

Merged
merged 2 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use process.env.NODE_ENV? Not available in Vite?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this code will be executed in a browser environment, getting env variables that way will throw. Vite provides this as a kind analogous way to access env variables with a client side app.

};

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 { useDelayedIsLoading } from '../fetching/refetch-hook.ts';

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

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 { uint8ArrayToString } 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'>{uint8ArrayToString(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
Loading