Skip to content

Commit

Permalink
feat: added signer id page
Browse files Browse the repository at this point in the history
  • Loading branch information
BLuEScioN committed Nov 8, 2024
1 parent 6ee9484 commit 09a6f64
Show file tree
Hide file tree
Showing 23 changed files with 1,666 additions and 257 deletions.
672 changes: 535 additions & 137 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/app/_components/time-filter/DatePickerInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FormLabel } from '@/ui/FormLabel';
import { UTCDate } from '@date-fns/utc';
import { Field, FieldProps, Form, Formik } from 'formik';
import DatePicker from 'react-datepicker';
Expand All @@ -7,6 +6,7 @@ import 'react-datepicker/dist/react-datepicker.css';
import { Box } from '../../../ui/Box';
import { Button } from '../../../ui/Button';
import { FormControl } from '../../../ui/FormControl';
import { FormLabel } from '../../../ui/FormLabel';
import { Stack } from '../../../ui/Stack';
import { DateInput } from './DateInput';

Expand Down
2 changes: 1 addition & 1 deletion src/app/_components/time-filter/TimeFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Stack } from '@/ui/Stack';
import { useColorModeValue } from '@chakra-ui/react';
import { CaretDown } from '@phosphor-icons/react';
import { ReactNode, useEffect, useMemo, useState } from 'react';
Expand All @@ -10,6 +9,7 @@ import { Icon } from '../../../ui/Icon';
import { Popover } from '../../../ui/Popover';
import { PopoverContent } from '../../../ui/PopoverContent';
import { PopoverTrigger } from '../../../ui/PopoverTrigger';
import { Stack } from '../../../ui/Stack';
import { Text } from '../../../ui/Text';
import { useDisclosure } from '../../../ui/hooks/useDisclosure';
import { DatePickerInput, DatePickerValues } from './DatePickerInput';
Expand Down
4 changes: 2 additions & 2 deletions src/app/_components/time-filter/TimeInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { FormLabel } from '@/ui/FormLabel';
import { Input } from '@/ui/Input';
import { Field, FieldProps, Form, Formik } from 'formik';

import { Box } from '../../../ui/Box';
import { Button } from '../../../ui/Button';
import { FormControl } from '../../../ui/FormControl';
import { FormLabel } from '../../../ui/FormLabel';
import { Input } from '../../../ui/Input';
import { Stack } from '../../../ui/Stack';

export type Time = string;
Expand Down
4 changes: 2 additions & 2 deletions src/app/_components/time-filter/TimeRangeInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { FormLabel } from '@/ui/FormLabel';
import { Input } from '@/ui/Input';
import { Field, FieldProps, Form, Formik } from 'formik';

import { Box } from '../../../ui/Box';
import { Button } from '../../../ui/Button';
import { FormControl } from '../../../ui/FormControl';
import { FormLabel } from '../../../ui/FormLabel';
import { Input } from '../../../ui/Input';
import { Stack } from '../../../ui/Stack';

type Time = number | string | undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';

import { logError } from '@/common/utils/error-utils';
import { useEffect } from 'react';

import { ErrorMessageLayout } from '../common/components/ErrorMessageLayout';
import { Section } from '../common/components/Section';
import { logError } from '../common/utils/error-utils';
import { Box } from '../ui/Box';
import { Grid } from '../ui/Grid';

Expand Down
2 changes: 1 addition & 1 deletion src/app/search/filters/Date.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Stack } from '@/ui/Stack';
import { useColorModeValue } from '@chakra-ui/react';
import { CaretDown } from '@phosphor-icons/react';
import { ReactNode, useEffect, useState } from 'react';
Expand All @@ -11,6 +10,7 @@ import { Icon } from '../../../ui/Icon';
import { Popover } from '../../../ui/Popover';
import { PopoverContent } from '../../../ui/PopoverContent';
import { PopoverTrigger } from '../../../ui/PopoverTrigger';
import { Stack } from '../../../ui/Stack';
import { Text } from '../../../ui/Text';
import { useDisclosure } from '../../../ui/hooks/useDisclosure';
import { AfterForm } from './After';
Expand Down
125 changes: 125 additions & 0 deletions src/app/signer/[signerKey]/AssociatedAddressesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ReactNode, Suspense, useMemo } from 'react';

import {
SignersStackersData,
useSuspenseSignerStackersInfinite,
} from '../../../app/signers/data/UseSignerAddresses';
import { CopyButton } from '../../../common/components/CopyButton';
import { ListFooter } from '../../../common/components/ListFooter';
import { Section } from '../../../common/components/Section';
import { Box } from '../../../ui/Box';
import { Flex } from '../../../ui/Flex';
import { Stack } from '../../../ui/Stack';
import { Text } from '../../../ui/Text';
import { ExplorerErrorBoundary } from '../../_components/ErrorBoundary';
import { useSuspenseCurrentStackingCycle } from '../../_components/Stats/CurrentStackingCycle/useCurrentStackingCycle';
import { AssociatedAddressesTableSkeleton } from './skeleton';

export const addressItemHeight = 69;

export const AssociatedAddressesTableLayout = ({ children }: { children: ReactNode }) => {
return <Section title="Associated addresses">{children}</Section>;
};

export const AssociatedAddressListItemLayout = ({
children,
isLast,
}: {
children: ReactNode;
isLast: boolean;
}) => {
return (
<Flex
alignItems={'center'}
gap={1}
py={6}
borderBottom={isLast ? 'none' : '1px solid var(--stacks-colors-borderSecondary)'}
>
{children}
</Flex>
);
};

export const AssociatedAddressListItem = ({
stacker,
isLast,
}: {
stacker: SignersStackersData;
isLast: boolean;
}) => {
return (
<AssociatedAddressListItemLayout isLast={isLast}>
<Text fontSize={'sm'}>{stacker.stacker_address}</Text>
<CopyButton
className={'fancy-copy'}
initialValue={stacker.pox_address}
aria-label={'copy row'}
size={4}
color="textSubdued"
/>
</AssociatedAddressListItemLayout>
);
};

export const AssociatedAddressesTableBase = ({ signerKey }: { signerKey: string }) => {
const { currentCycleId } = useSuspenseCurrentStackingCycle();
const {
data: signerStackers,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useSuspenseSignerStackersInfinite(currentCycleId, signerKey);

const stackers = useMemo(
() => signerStackers?.pages.flatMap(page => page.results) ?? [],
[signerStackers]
);

return (
<AssociatedAddressesTableLayout>
<Stack gap={0}>
<Box h={addressItemHeight * 10} overflow="auto">
{signerStackers?.pages
.flatMap(page => page.results)
.map((stacker, i) => (
<AssociatedAddressListItem
key={stacker.stacker_address}
stacker={stacker}
isLast={i === stackers.length - 1}
/>
))}
</Box>

<ListFooter
label="addresses"
isLoading={isFetchingNextPage}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
pb={6}
position={'sticky'}
bottom={0}
bg="surface"
/>
</Stack>
</AssociatedAddressesTableLayout>
);
};

export const AssociatedAddressesTable = ({ signerKey }: { signerKey: string }) => {
return (
<ExplorerErrorBoundary
Wrapper={Section}
wrapperProps={{
title: 'Associated Addresses',
gridColumnStart: ['1', '1', '2'],
gridColumnEnd: ['2', '2', '3'],
minWidth: 0,
}}
tryAgainButton
>
<Suspense fallback={<AssociatedAddressesTableSkeleton />}>
<AssociatedAddressesTableBase signerKey={signerKey} />
</Suspense>
</ExplorerErrorBoundary>
);
};
45 changes: 45 additions & 0 deletions src/app/signer/[signerKey]/CycleSortFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SortDescending } from '@phosphor-icons/react';
import { useMemo } from 'react';

import { FilterMenu } from '../../../common/components/FilterMenu';

export enum CycleSortOrder {
Asc = 'asc',
Desc = 'desc',
}

function getSortOptionLabel(order: CycleSortOrder) {
switch (order) {
case CycleSortOrder.Asc:
return 'Cycle Asc';
case CycleSortOrder.Desc:
return 'Cycle Desc';
}
}

export function CycleSortFilter({
cycleSortOrder,
setCycleSortOrder,
}: {
cycleSortOrder: CycleSortOrder;
setCycleSortOrder: (order: CycleSortOrder) => void;
}) {
const menuItems = useMemo(
() =>
Object.values(CycleSortOrder).map(order => ({
onClick: () => {
setCycleSortOrder(order);
},
label: getSortOptionLabel(order),
})),
[setCycleSortOrder]
);

return (
<FilterMenu
filterLabel={() => getSortOptionLabel(cycleSortOrder)}
menuItems={menuItems}
leftIcon={SortDescending}
/>
);
}
51 changes: 51 additions & 0 deletions src/app/signer/[signerKey]/PageClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import { useParams } from 'next/navigation';

import { Grid, GridProps } from '../../../ui/Grid';
import { Stack } from '../../../ui/Stack';
import { PageTitle } from '../../_components/PageTitle';
import { AssociatedAddressesTable } from './AssociatedAddressesTable';
import { SignerStats } from './SignerStats';
import { SignerSummary } from './SignerSummary';
import { StackingHistoryTable } from './StackingHistoryTable';

export function SignerKeyPageLayout(props: GridProps) {
return (
<Grid
gridColumnGap={8}
gridTemplateColumns={['100%', '100%', 'repeat(1, calc(100% - 352px) 320px)']}
gridRowGap={[8, 8, 'unset']}
maxWidth="100%"
alignItems="flex-start"
{...props}
/>
);
}

export default function PageClient() {
const params = useParams<{ signerKey: string }>();

if (!params) {
console.error('params is undefined. This component should receive params from its parent.');
return null; // or some error UI
}

const { signerKey } = params;
return (
<>
<PageTitle>Signer key</PageTitle>

<SignerKeyPageLayout>
<Stack gap={8}>
<SignerSummary signerKey={signerKey} />
<AssociatedAddressesTable signerKey={signerKey} />
<StackingHistoryTable signerKey={signerKey} />
</Stack>
<Stack gap={8}>
<SignerStats signerKey={signerKey} />
</Stack>
</SignerKeyPageLayout>
</>
);
}
86 changes: 86 additions & 0 deletions src/app/signer/[signerKey]/SignerStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ReactNode, Suspense } from 'react';

import { ExplorerErrorBoundary } from '../../../app/_components/ErrorBoundary';
import { formatSignerLatency, formatSignerProposalMetric } from '../../../app/signers/SignersTable';
import { useSignerMetricsSignerForCycle } from '../../../app/signers/data/signer-metrics-hooks';
import { Section } from '../../../common/components/Section';
import { Box } from '../../../ui/Box';
import { Stack } from '../../../ui/Stack';
import { Caption } from '../../../ui/typography';
import { useSuspenseCurrentStackingCycle } from '../../_components/Stats/CurrentStackingCycle/useCurrentStackingCycle';
import { SignerKeyStatsSkeleton } from './skeleton';

interface SignerStatsProps {
signerKey: string;
}

export const SignerStatsLayout = ({ children }: { children: ReactNode }) => {
return <Section title={'Stats'}>{children}</Section>;
};

export const SignerKeyStat = ({
label,
value,
}: {
label: string | ReactNode;
value: string | ReactNode;
}) => {
return (
<Stack gap={2} py={4}>
<Caption>{label}</Caption>
<Box>{value}</Box>
</Stack>
);
};

function SignerStatsBase({ signerKey }: SignerStatsProps) {
const { currentCycleId } = useSuspenseCurrentStackingCycle();
const { data: signerMetrics } = useSignerMetricsSignerForCycle(currentCycleId, signerKey);
const totalProposals =
signerMetrics?.proposals_accepted_count +
signerMetrics?.proposals_rejected_count +
signerMetrics?.proposals_missed_count;

return (
<SignerStatsLayout>
<SignerKeyStat
label="Latency"
value={formatSignerLatency(
signerMetrics?.average_response_time_ms,
signerMetrics?.proposals_missed_count
)}
/>
<SignerKeyStat
label="Accepted proposals %"
value={formatSignerProposalMetric(signerMetrics?.proposals_accepted_count / totalProposals)}
/>
<SignerKeyStat
label="Rejected proposals %"
value={formatSignerProposalMetric(signerMetrics?.proposals_rejected_count / totalProposals)}
/>
<SignerKeyStat
label="Missed proposals %"
value={formatSignerProposalMetric(signerMetrics?.proposals_missed_count / totalProposals)}
/>
</SignerStatsLayout>
);
}

export function SignerStats(props: SignerStatsProps) {
return (
<ExplorerErrorBoundary
Wrapper={Section}
wrapperProps={{
title: 'Signer Stats',
gridColumnStart: ['1', '1', '2'],
gridColumnEnd: ['2', '2', '3'],
minWidth: 0,
}}
tryAgainButton
>
<Suspense fallback={<SignerKeyStatsSkeleton />}>
<SignerStatsBase {...props} />
</Suspense>
</ExplorerErrorBoundary>
);
}
Loading

0 comments on commit 09a6f64

Please sign in to comment.