From caed3ea1950896b6e451d51073d1e87a5a9ac585 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Tue, 30 Jan 2024 02:36:32 +0100 Subject: [PATCH 1/4] Move a ProposalStatusIcon to Proposals directory (We want to group related components together) --- src/app/components/NetworkProposalsList/index.tsx | 2 +- .../index.tsx => Proposals/ProposalStatusIcon.tsx} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/app/components/{ProposalStatusIcon/index.tsx => Proposals/ProposalStatusIcon.tsx} (98%) diff --git a/src/app/components/NetworkProposalsList/index.tsx b/src/app/components/NetworkProposalsList/index.tsx index 58bc97167..6ccd0b95d 100644 --- a/src/app/components/NetworkProposalsList/index.tsx +++ b/src/app/components/NetworkProposalsList/index.tsx @@ -4,7 +4,7 @@ import { Table, TableCellAlign, TableColProps } from '../../components/Table' import { Proposal } from '../../../oasis-nexus/api' import { TablePaginationProps } from '../Table/TablePagination' import { RoundedBalance } from '../RoundedBalance' -import { ProposalStatusIcon } from '../../components/ProposalStatusIcon' +import { ProposalStatusIcon } from '../../components/Proposals/ProposalStatusIcon' type NetworkProposalsListProps = { proposals?: Proposal[] diff --git a/src/app/components/ProposalStatusIcon/index.tsx b/src/app/components/Proposals/ProposalStatusIcon.tsx similarity index 98% rename from src/app/components/ProposalStatusIcon/index.tsx rename to src/app/components/Proposals/ProposalStatusIcon.tsx index ba9f03afa..bd069ae2d 100644 --- a/src/app/components/ProposalStatusIcon/index.tsx +++ b/src/app/components/Proposals/ProposalStatusIcon.tsx @@ -13,7 +13,7 @@ type ProposalStatus = { const StyledBox = styled(Box, { shouldForwardProp: prop => prop !== 'success', })(({ success }) => ({ - justifyContent: 'end', + // justifyContent: 'end', color: success ? COLORS.eucalyptus : COLORS.errorIndicatorBackground, textTransform: 'capitalize', display: 'flex', From 0cb9edc3d5fd5c68510234837f3759589d2d46af Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Tue, 30 Jan 2024 03:05:32 +0100 Subject: [PATCH 2/4] Add basic proposal details page --- .changelog/1202.feature.md | 1 + src/app/components/ErrorDisplay/index.tsx | 8 ++ .../Proposals/ProposalStatusIcon.tsx | 4 +- src/app/pages/ProposalDetailsPage/index.tsx | 99 +++++++++++++++++++ src/app/utils/helpers.ts | 2 + src/app/utils/route-utils.ts | 15 ++- src/locales/en/translation.json | 7 ++ src/oasis-nexus/api.ts | 30 ++++++ src/routes.tsx | 7 ++ src/types/errors.ts | 2 + 10 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 .changelog/1202.feature.md create mode 100644 src/app/pages/ProposalDetailsPage/index.tsx diff --git a/.changelog/1202.feature.md b/.changelog/1202.feature.md new file mode 100644 index 000000000..d03bf4843 --- /dev/null +++ b/.changelog/1202.feature.md @@ -0,0 +1 @@ +Add Network proposal details page diff --git a/src/app/components/ErrorDisplay/index.tsx b/src/app/components/ErrorDisplay/index.tsx index 56443f9e6..d2a859299 100644 --- a/src/app/components/ErrorDisplay/index.tsx +++ b/src/app/components/ErrorDisplay/index.tsx @@ -16,6 +16,10 @@ const errorMap: Record Formatt message: t('errors.validateURL'), }), [AppErrors.InvalidTxHash]: t => ({ title: t('errors.invalidTxHash'), message: t('errors.validateURL') }), + [AppErrors.InvalidProposalId]: t => ({ + title: t('errors.invalidProposalId'), + message: t('errors.validateURL'), + }), [AppErrors.InvalidPageNumber]: t => ({ title: t('errors.invalidPageNumber'), message: ( @@ -45,6 +49,10 @@ const errorMap: Record Formatt message: t('errors.validateURL'), }), [AppErrors.NotFoundTxHash]: t => ({ title: t('errors.notFoundTx'), message: t('errors.validateURL') }), + [AppErrors.NotFoundProposalId]: t => ({ + title: t('errors.notFoundProposal'), + message: t('errors.validateURL'), + }), [AppErrors.InvalidUrl]: t => ({ title: t('errors.invalidUrl'), message: t('errors.validateURL') }), [AppErrors.UnsupportedLayer]: t => ({ title: t('errors.error'), diff --git a/src/app/components/Proposals/ProposalStatusIcon.tsx b/src/app/components/Proposals/ProposalStatusIcon.tsx index bd069ae2d..5e08776b3 100644 --- a/src/app/components/Proposals/ProposalStatusIcon.tsx +++ b/src/app/components/Proposals/ProposalStatusIcon.tsx @@ -13,11 +13,9 @@ type ProposalStatus = { const StyledBox = styled(Box, { shouldForwardProp: prop => prop !== 'success', })(({ success }) => ({ - // justifyContent: 'end', color: success ? COLORS.eucalyptus : COLORS.errorIndicatorBackground, textTransform: 'capitalize', - display: 'flex', - flexDirection: 'row', + display: 'inline-flex', alignItems: 'center', gap: 3, flex: 1, diff --git a/src/app/pages/ProposalDetailsPage/index.tsx b/src/app/pages/ProposalDetailsPage/index.tsx new file mode 100644 index 000000000..dfd0ace7d --- /dev/null +++ b/src/app/pages/ProposalDetailsPage/index.tsx @@ -0,0 +1,99 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useRequiredScopeParam } from '../../hooks/useScopeParam' +import { Layer, Proposal, useGetConsensusProposalsProposalId } from '../../../oasis-nexus/api' +import { AppErrors } from '../../../types/errors' +import { useParams } from 'react-router-dom' +import { PageLayout } from '../../components/PageLayout' +import { SubPageCard } from '../../components/SubPageCard' +import { StyledDescriptionList } from 'app/components/StyledDescriptionList' +import { RoundedBalance } from '../../components/RoundedBalance' +import { DashboardLink } from '../ParatimeDashboardPage/DashboardLink' +import { useScreenSize } from '../../hooks/useScreensize' +import { ProposalStatusIcon } from '../../components/Proposals/ProposalStatusIcon' +import { TextSkeleton } from '../../components/Skeleton' +import { AccountLink } from '../../components/Account/AccountLink' + +export const ProposalDetailsPage: FC = () => { + const { t } = useTranslation() + const scope = useRequiredScopeParam() + if (scope.layer !== Layer.consensus) { + throw AppErrors.UnsupportedLayer + } + const proposalId = parseInt(useParams().proposalId!, 10) + + const { isLoading, data } = useGetConsensusProposalsProposalId(scope.network, proposalId) + if (!data?.data && !isLoading) { + throw AppErrors.NotFoundProposalId + } + const proposal = data?.data! + return ( + + + + + + ) +} + +export const ProposalDetailView: FC<{ + proposal: Proposal + isLoading?: boolean + showLayer?: boolean + standalone?: boolean +}> = ({ proposal, isLoading, showLayer = false, standalone = false }) => { + const { t } = useTranslation() + const { isMobile } = useScreenSize() + if (isLoading) return + console.log('loaded proposal', proposal) + return ( + + {showLayer && ( + <> +
{t('common.network')}
+
+ +
+ + )} + +
{t('networkProposal.id')}
+
{proposal.id}
+ +
{t('common.title')}
+
{proposal.handler}
+ + {/*Not enough data*/} + {/*
{t('common.type')}
*/} + {/*
???
*/} + +
{t('common.submitter')}
+
+ +
+ + {/*Not enough data*/} + {/*
{t('common.totalVotes')}
*/} + {/*
{proposal.invalid_votes}
*/} + +
{t('common.status')}
+
+ +
+ +
{t('networkProposal.deposit')}
+
+ +
+ +
{t('networkProposal.create')}
+
{proposal.created_at}
+ +
{t('networkProposal.close')}
+
{proposal.closes_at}
+
+ ) +} diff --git a/src/app/utils/helpers.ts b/src/app/utils/helpers.ts index 224798d16..635c89701 100644 --- a/src/app/utils/helpers.ts +++ b/src/app/utils/helpers.ts @@ -24,6 +24,8 @@ export const isValidEthAddress = (hexAddress: string): boolean => { return /^0x[0-9a-fA-F]{40}$/.test(hexAddress) } +export const isValidProposalId = (proposalId: string): boolean => /^[0-9]+$/.test(proposalId) + export async function getEvmBech32Address(evmAddress: string) { const ethAddrU8 = oasis.misc.fromHex(evmAddress.replace('0x', '')) const addr = await oasis.address.fromData( diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index 2aedb1221..68f75c703 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -1,5 +1,5 @@ import { LoaderFunctionArgs } from 'react-router-dom' -import { isValidTxHash } from './helpers' +import { isValidProposalId, isValidTxHash } from './helpers' import { isValidBlockHeight, isValidOasisAddress, isValidEthAddress } from './helpers' import { AppError, AppErrors } from '../../types/errors' import { EvmTokenType, Layer } from '../../oasis-nexus/api' @@ -160,6 +160,15 @@ const validateTxHashParam = (hash: string) => { return true } +const validateProposalIdParam = (proposalId: string) => { + const isValid = isValidProposalId(proposalId) + if (!isValid) { + throw new AppError(AppErrors.InvalidProposalId) + } + + return isValid +} + export const addressParamLoader = (queryParam: string = 'address') => ({ params }: LoaderFunctionArgs): string => { @@ -195,3 +204,7 @@ export const assertEnabledScope = ({ } return { network, layer } as SearchScope } + +export const proposalIdParamLoader = async ({ params }: LoaderFunctionArgs) => { + return validateProposalIdParam(params.proposalId!) +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 1958ff9a2..b175fc407 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -70,6 +70,7 @@ "lessThanAmount": "< {{value, number}} ", "missing": "n/a", "name": "Name", + "network": "Network", "nft": "NFT", "nfts": "NFTs", "not_defined": "Not defined", @@ -78,6 +79,7 @@ "paratime": "Paratime", "parentheses": "({{subject}})", "percentage": "Percentage", + "proposal": "Proposal", "rank": "Rank", "select": "Select", "size": "Size", @@ -85,14 +87,17 @@ "show": "Show", "smartContract": "Smart Contract", "smartContract_short": "Contract", + "submitter": "Submitter", "success": "Success", "status": "Status", "ticker": "Ticker", "timestamp": "Timestamp", + "title": "Title", "to": "To", "token": "Token", "tokenInstance": "TokenID [] ", "tokens": "Tokens", + "totalVotes": "Total votes", "transactions": "Transactions", "transactionAbbreviation": "Txs", "transactionFee": "Transaction Fee", @@ -179,9 +184,11 @@ "invalidAddress": "Invalid address", "invalidBlockHeight": "Invalid block height", "invalidPageNumber": "Invalid page number", + "invalidProposalId": "Invalid proposal ID", "invalidTxHash": "Invalid transaction hash", "notFoundBlockHeight": "Block not found", "notFoundTx": "Transaction not found", + "notFoundProposal": "Proposal not found", "pageDoesNotExist": "The page you are looking for does not exist.", "validateURL": "Please validate provided URL", "validateURLOrGoToFirstPage": "Please check the URL or .", diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index ef59642b6..bf147ee21 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -84,6 +84,10 @@ declare module './generated/api' { export interface Validator { ticker: NativeTicker } + + export interface Proposal { + network: Network + } } export const isAccountEmpty = (account: RuntimeAccount) => { @@ -811,6 +815,7 @@ export const useGetConsensusProposals: typeof generated.useGetConsensusProposals proposals: data.proposals.map(proposal => { return { ...proposal, + network, deposit: fromBaseUnits(proposal.deposit, consensusDecimals), } }), @@ -822,6 +827,31 @@ export const useGetConsensusProposals: typeof generated.useGetConsensusProposals }) } +export const useGetConsensusProposalsProposalId: typeof generated.useGetConsensusProposalsProposalId = ( + network, + proposalId, + options?, +) => { + return generated.useGetConsensusProposalsProposalId(network, proposalId, { + ...options, + request: { + ...options?.request, + transformResponse: [ + ...arrayify(axios.defaults.transformResponse), + (data: generated.Proposal, headers, status) => { + if (status !== 200) return data + return { + ...data, + network, + deposit: fromBaseUnits(data.deposit, consensusDecimals), + } + }, + ...arrayify(options?.request?.transformResponse), + ], + }, + }) +} + export const useGetConsensusValidators: typeof generated.useGetConsensusValidators = ( network, params?, diff --git a/src/routes.tsx b/src/routes.tsx index a445aaa11..c985082f0 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -14,6 +14,7 @@ import { blockHeightParamLoader, transactionParamLoader, assertEnabledScope, + proposalIdParamLoader, } from './app/utils/route-utils' import { searchParamLoader } from './app/components/Search/search-utils' import { RoutingErrorPage } from './app/pages/RoutingErrorPage' @@ -33,6 +34,7 @@ import { NFTTokenTransfersCard } from './app/pages/NFTInstanceDashboardPage/NFTT import { ConsensusDashboardPage } from 'app/pages/ConsensusDashboardPage' import { Layer } from './oasis-nexus/api' import { SearchScope } from './types/searchScope' +import { ProposalDetailsPage } from './app/pages/ProposalDetailsPage' const NetworkSpecificPart = () => ( @@ -72,6 +74,11 @@ export const routes: RouteObject[] = [ path: '', element: , }, + { + path: `proposal/:proposalId`, + element: , + loader: proposalIdParamLoader, + }, ], }, { diff --git a/src/types/errors.ts b/src/types/errors.ts index 00e7ffc21..fda487b1d 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -15,10 +15,12 @@ export enum AppErrors { InvalidAddress = 'invalid_address', InvalidBlockHeight = 'invalid_block_height', InvalidTxHash = 'invalid_tx_hash', + InvalidProposalId = 'invalid_proposal_id', InvalidPageNumber = 'invalid_page_number', PageDoesNotExist = 'page_does_not_exist', NotFoundBlockHeight = 'not_found_block_height', NotFoundTxHash = 'not_found_tx_hash', + NotFoundProposalId = 'not_found_proposal_id', InvalidUrl = 'invalid_url', Storage = 'storage', } From 6ee06e94846fae2489e045b0f045eb76625696cc Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Tue, 30 Jan 2024 03:08:46 +0100 Subject: [PATCH 3/4] Link to proposal details from proposal list --- .../components/NetworkProposalsList/index.tsx | 8 +++++--- src/app/components/Proposals/ProposalLink.tsx | 18 ++++++++++++++++++ src/app/utils/route-utils.ts | 4 ++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/app/components/Proposals/ProposalLink.tsx diff --git a/src/app/components/NetworkProposalsList/index.tsx b/src/app/components/NetworkProposalsList/index.tsx index 6ccd0b95d..39968fb73 100644 --- a/src/app/components/NetworkProposalsList/index.tsx +++ b/src/app/components/NetworkProposalsList/index.tsx @@ -5,6 +5,7 @@ import { Proposal } from '../../../oasis-nexus/api' import { TablePaginationProps } from '../Table/TablePagination' import { RoundedBalance } from '../RoundedBalance' import { ProposalStatusIcon } from '../../components/Proposals/ProposalStatusIcon' +import { ProposalLink } from '../Proposals/ProposalLink' type NetworkProposalsListProps = { proposals?: Proposal[] @@ -33,13 +34,14 @@ export const NetworkProposalsList: FC = ({ data: [ { align: TableCellAlign.Center, - content: <>{proposal.id}, + content: , key: 'id', }, { align: TableCellAlign.Left, - // TODO: link to Proposals page once it is ready - content: <>{proposal.handler}, + content: ( + + ), key: 'handler', }, { diff --git a/src/app/components/Proposals/ProposalLink.tsx b/src/app/components/Proposals/ProposalLink.tsx new file mode 100644 index 000000000..dfab0826c --- /dev/null +++ b/src/app/components/Proposals/ProposalLink.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react' +import { Link as RouterLink } from 'react-router-dom' +import Link from '@mui/material/Link' +import { RouteUtils } from '../../utils/route-utils' +import { Network } from '../../../types/network' + +export const ProposalLink: FC<{ + network: Network + proposalId: string | number + label?: string +}> = ({ network, proposalId, label = proposalId }) => { + const to = RouteUtils.getProposalRoute(network, proposalId) + return ( + + {label} + + ) +} diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index 68f75c703..fe0051103 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -115,6 +115,10 @@ export abstract class RouteUtils { return Object.values(Layer).filter(layer => RouteUtils.ENABLED_LAYERS_FOR_NETWORK[network][layer]) } + static getProposalRoute = (network: Network, proposalId: string | number) => { + return `/${encodeURIComponent(network)}/consensus/proposal/${encodeURIComponent(proposalId)}` + } + static getEnabledScopes(): SearchScope[] { return RouteUtils.getEnabledNetworks().flatMap(network => RouteUtils.getEnabledLayersForNetwork(network).map(layer => ({ network, layer })), From 980f7211c94739099fc47748b416e3c1b043cf63 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Tue, 30 Jan 2024 04:47:10 +0100 Subject: [PATCH 4/4] Add consensus to layer to loaded network proposals right away --- src/app/pages/ProposalDetailsPage/index.tsx | 9 +++------ src/oasis-nexus/api.ts | 2 ++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/pages/ProposalDetailsPage/index.tsx b/src/app/pages/ProposalDetailsPage/index.tsx index dfd0ace7d..b70d46db3 100644 --- a/src/app/pages/ProposalDetailsPage/index.tsx +++ b/src/app/pages/ProposalDetailsPage/index.tsx @@ -45,14 +45,14 @@ export const ProposalDetailView: FC<{ const { t } = useTranslation() const { isMobile } = useScreenSize() if (isLoading) return - console.log('loaded proposal', proposal) + return ( {showLayer && ( <>
{t('common.network')}
- +
)} @@ -69,10 +69,7 @@ export const ProposalDetailView: FC<{
{t('common.submitter')}
- +
{/*Not enough data*/} diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index bf147ee21..af9ca703e 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -87,6 +87,7 @@ declare module './generated/api' { export interface Proposal { network: Network + layer: typeof Layer.consensus } } @@ -843,6 +844,7 @@ export const useGetConsensusProposalsProposalId: typeof generated.useGetConsensu return { ...data, network, + layer: Layer.consensus, deposit: fromBaseUnits(data.deposit, consensusDecimals), } },