vega-frontend-monorepo/apps/governance/src/routes/home/index.tsx

291 lines
9.1 KiB
TypeScript

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
AsyncRenderer,
Button,
ExternalLink,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { useProtocolUpgradesQuery } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import {
getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals,
} from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const nodesToShow = 6;
const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: ProposalFieldsFragment[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
return (
<section className="mb-16" data-testid="home-proposals">
<Heading title={t('Proposals')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="flex items-center mb-8 gap-8">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
{t(`readMoreGovernance`)}
</ExternalLink>
</div>
<ul
data-testid="home-proposal-list"
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
>
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
</ul>
</section>
);
};
interface HomeNodesProps {
activeNodes: NodesFragmentFragment[];
consensusNodes: NodesFragmentFragment[];
trimmedActiveNodes: NodesFragmentFragment[];
}
const HomeNodes = ({
activeNodes,
consensusNodes,
trimmedActiveNodes,
}: HomeNodesProps) => {
const { t } = useTranslation();
const highlightedNodeData = [
{ title: t('active nodes'), length: activeNodes.length },
{ title: t('consensus nodes'), length: consensusNodes.length },
];
return (
<section className="mb-12" data-testid="home-validators">
<Heading title={t('Validators')} />
<h3 className="mb-6">{t('homeValidatorsIntro')}</h3>
<div className="flex items-center mb-8 gap-8">
<Link to={Routes.VALIDATORS}>
<Button size="md">{t('homeValidatorsButtonText')}</Button>
</Link>
<ExternalLink href={ExternalLinks.VALIDATOR_FORUM}>
{t(`readMoreValidators`)}
</ExternalLink>
</div>
<div className="grid grid-cols-[repeat(6,_1fr)] items-center gap-x-6 gap-y-2">
{highlightedNodeData.map(({ title, length }, index) => (
<div key={index} className="col-span-3">
<Link to={Routes.VALIDATORS}>
<RoundedWrapper paddingBottom={true}>
<div
data-testid="node-information"
className="flex flex-col items-center m-[-1rem] px-4 py-6 hover:bg-neutral-800"
>
<span className="text-5xl">{length}</span>
<span className="text-sm uppercase text-neutral-400">
{title}
</span>
</div>
</RoundedWrapper>
</Link>
</div>
))}
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
<div key={id} data-testid="validators" className="col-span-2">
<ValidatorDetailsLink id={id} avatarUrl={avatarUrl} name={name} />
</div>
))}
</div>
{activeNodes.length > nodesToShow && (
<Link to={Routes.VALIDATORS}>
<span className="underline">
And {activeNodes.length - nodesToShow} more...
</span>
</Link>
)}
</section>
);
};
interface ValidatorDetailsLinkProps {
id: string;
avatarUrl: string | null | undefined;
name: string;
}
export const ValidatorDetailsLink = ({
id,
avatarUrl,
name,
}: ValidatorDetailsLinkProps) => {
return (
<Link to={`${Routes.VALIDATORS}/${id}`}>
<RoundedWrapper paddingBottom={true} border={false}>
<div className="flex items-center justify-center m-[-1rem] p-3 bg-neutral-900 hover:bg-neutral-800">
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
/>
)}
<span className="text-sm">{name}</span>
</div>
</RoundedWrapper>
</Link>
);
};
const GovernanceHome = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const {
data: proposalsData,
loading: proposalsLoading,
error: proposalsError,
} = useProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const {
data: validatorsData,
error: validatorsError,
loading: validatorsLoading,
refetch,
} = useNodesQuery();
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
const proposals = useMemo(
() =>
proposalsData
? getNotRejectedProposals<ProposalFieldsFragment>(
proposalsData.proposalsConnection
)
: [],
[proposalsData]
);
const protocolUpgradeProposals = useMemo(
() =>
protocolUpgradesData
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
protocolUpgradesData.protocolUpgradeProposals
).filter(
(p) =>
Number(p.upgradeBlockHeight) >
Number(protocolUpgradesData.lastBlockHeight)
)
: [],
[protocolUpgradesData]
);
const totalProposalsDesired = 4;
const protocolUpgradeProposalsToShow = protocolUpgradeProposals.slice(
0,
totalProposalsDesired
);
const proposalsToShow =
protocolUpgradeProposalsToShow.length === totalProposalsDesired
? []
: proposals.slice(
0,
totalProposalsDesired - protocolUpgradeProposalsToShow.length
);
const activeNodes = removePaginationWrapper(
validatorsData?.nodesConnection.edges
);
const trimmedActiveNodes = activeNodes?.slice(0, nodesToShow);
const consensusNodes = activeNodes.filter(
(node) =>
node.rankingScore.status ===
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT
);
return (
<AsyncRenderer
loading={proposalsLoading || protocolUpgradesLoading || validatorsLoading}
error={proposalsError || protocolUpgradesError || validatorsError}
data={proposalsData && protocolUpgradesData && validatorsData}
>
<HomeProposals
proposals={proposalsToShow}
protocolUpgradeProposals={protocolUpgradeProposalsToShow}
/>
<HomeNodes
activeNodes={activeNodes}
consensusNodes={consensusNodes}
trimmedActiveNodes={trimmedActiveNodes}
/>
<section className="grid grid-cols-2 gap-12 mb-16">
<div data-testid="home-rewards">
<Heading title={t('Rewards')} marginTop={false} />
<h3 className="mb-6">{t('homeRewardsIntro')}</h3>
<div className="flex items-center mb-8 gap-4">
<Link to={Routes.REWARDS}>
<Button size="md">{t('homeRewardsButtonText')}</Button>
</Link>
</div>
</div>
<div data-testid="home-vega-token">
<Heading title={t('VEGA Token')} marginTop={false} />
<h3 className="mb-6">{t('homeVegaTokenIntro')}</h3>
<div className="flex items-center mb-8 gap-4">
<Link to={Routes.WITHDRAWALS}>
<Button size="md">{t('homeVegaTokenButtonText')}</Button>
</Link>
</div>
</div>
</section>
</AsyncRenderer>
);
};
export default GovernanceHome;