Skip to content

Commit ddb20a5

Browse files
authored
Merge pull request #1848 from oasisprotocol/mz/enableRoflAppDetailsPage
Enable ROFL app details page
2 parents 1de51aa + 4669111 commit ddb20a5

File tree

10 files changed

+128
-3
lines changed

10 files changed

+128
-3
lines changed

.changelog/1848.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Enable ROFL app details page

src/app/components/ErrorDisplay/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ type FormattedError = { title: string; message: ReactNode }
1111
const errorMap: Record<AppErrors, (t: TFunction, error: ErrorPayload) => FormattedError> = {
1212
[AppErrors.Unknown]: (t, error) => ({ title: t('errors.unknown'), message: error.message }),
1313
[AppErrors.InvalidAddress]: t => ({ title: t('errors.invalidAddress'), message: t('errors.validateURL') }),
14+
[AppErrors.InvalidRoflAppId]: t => ({
15+
title: t('errors.invalidRoflAppId'),
16+
message: t('errors.validateURL'),
17+
}),
1418
[AppErrors.InvalidBlockHeight]: t => ({
1519
title: t('errors.invalidBlockHeight'),
1620
message: t('errors.validateURL'),
@@ -49,6 +53,10 @@ const errorMap: Record<AppErrors, (t: TFunction, error: ErrorPayload) => Formatt
4953
message: t('errors.validateURL'),
5054
}),
5155
[AppErrors.NotFoundTxHash]: t => ({ title: t('errors.notFoundTx'), message: t('errors.validateURL') }),
56+
[AppErrors.NotFoundRoflApp]: t => ({
57+
title: t('errors.notFoundRoflApp'),
58+
message: t('errors.validateURL'),
59+
}),
5260
[AppErrors.NotFoundProposalId]: t => ({
5361
title: t('errors.notFoundProposal'),
5462
message: t('errors.validateURL'),

src/app/pages/RoflAppDetailsPage/PolicyCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import CardContent from '@mui/material/CardContent'
66
import Grid from '@mui/material/Grid'
77
import Typography from '@mui/material/Typography'
88
import { Layer, RoflAppPolicy, useGetRuntimeRoflAppsIdTransactions } from '../../../oasis-nexus/api'
9+
import { Network } from '../../../types/network'
910
import { TransactionLink } from '../../components/Transactions/TransactionLink'
1011
import { EmptyStateCard } from './EmptyStateCard'
1112
import { GridRow } from './GridRow'
1213

1314
type PolicyCardProps = {
1415
id: string
1516
isFetched: boolean
16-
network: any
17+
network: Network
1718
policy: RoflAppPolicy | undefined
1819
}
1920

src/app/pages/RoflAppDetailsPage/hooks.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { SearchScope } from '../../../types/searchScope'
33
export type RoflAppDetailsContext = {
44
scope: SearchScope
55
id: string
6+
method: string
7+
setMethod: (value: string) => void
68
}
79

810
// TOOD: Placeholder file for a hook used within a router. Add when details page is ready

src/app/pages/RoflAppDetailsPage/index.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { FC } from 'react'
2+
import { useHref, useParams } from 'react-router-dom'
23
import { useTranslation } from 'react-i18next'
34
import { formatDistanceStrict } from 'date-fns'
45
import Box from '@mui/material/Box'
56
import Grid from '@mui/material/Grid'
7+
import Skeleton from '@mui/material/Skeleton'
68
import Typography from '@mui/material/Typography'
79
import { styled } from '@mui/material/styles'
8-
import { RoflApp } from '../../../oasis-nexus/api'
10+
import { Layer, RoflApp, useGetRuntimeRoflAppsId } from '../../../oasis-nexus/api'
911
import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat'
12+
import { AppErrors } from '../../../types/errors'
13+
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
14+
import { useTypedSearchParam } from '../../hooks/useTypedSearchParam'
1015
import { useScreenSize } from '../../hooks/useScreensize'
16+
import { PageLayout } from '../../components/PageLayout'
17+
import { SubPageCard } from '../../components/SubPageCard'
1118
import { TextSkeleton } from '../../components/Skeleton'
1219
import { StyledDescriptionList } from '../../components/StyledDescriptionList'
1320
import { RoflAppStatusBadge } from '../../components/Rofl/RoflAppStatusBadge'
@@ -19,6 +26,67 @@ import { TeeType } from './TeeType'
1926
import { Endorsement } from './Endorsement'
2027
import { Enclaves } from './Enclaves'
2128
import { Secrets } from './Secrets'
29+
import { RouterTabs } from '../../components/RouterTabs'
30+
import { instancesContainerId } from '../../utils/tabAnchors'
31+
import { RoflAppDetailsContext } from '../RoflAppDetailsPage/hooks'
32+
import { MetaDataCard } from './MetaDataCard'
33+
import { PolicyCard } from './PolicyCard'
34+
35+
export const RoflAppDetailsPage: FC = () => {
36+
const { t } = useTranslation()
37+
const scope = useRequiredScopeParam()
38+
const id = useParams().id!
39+
const txLink = useHref('')
40+
const instancesLink = useHref(`instances#${instancesContainerId}`)
41+
const [method, setMethod] = useTypedSearchParam('method', 'any', {
42+
deleteParams: ['page'],
43+
})
44+
const context: RoflAppDetailsContext = { scope, id, method, setMethod }
45+
const { isFetched, isLoading, data } = useGetRuntimeRoflAppsId(scope.network, Layer.sapphire, id)
46+
const roflApp = data?.data
47+
48+
if (!roflApp && isFetched) {
49+
throw AppErrors.NotFoundRoflApp
50+
}
51+
52+
return (
53+
<PageLayout>
54+
<SubPageCard
55+
featured
56+
title={
57+
isLoading ? <Skeleton variant="text" /> : roflApp?.metadata['net.oasis.rofl.name'] || roflApp?.id
58+
}
59+
>
60+
<RoflAppDetailsView detailsPage isLoading={isLoading} app={roflApp} />
61+
</SubPageCard>
62+
<Grid container spacing={4}>
63+
<StyledGrid item xs={12} md={6}>
64+
<MetaDataCard isFetched={isFetched} metadata={roflApp?.metadata} />
65+
</StyledGrid>
66+
<StyledGrid item xs={12} md={6}>
67+
{roflApp && (
68+
<PolicyCard
69+
id={roflApp?.id}
70+
network={roflApp?.network}
71+
isFetched={isFetched}
72+
policy={roflApp?.policy}
73+
/>
74+
)}
75+
</StyledGrid>
76+
</Grid>
77+
<RouterTabs
78+
tabs={[
79+
{ label: t('common.transactions'), to: txLink },
80+
{
81+
label: t('rofl.instances'),
82+
to: instancesLink,
83+
},
84+
]}
85+
context={context}
86+
/>
87+
</PageLayout>
88+
)
89+
}
2290

2391
export const StyledGrid = styled(Grid)(({ theme }) => ({
2492
[theme.breakpoints.up('sm')]: {

src/app/utils/helpers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ export const isValidEthAddress = (hexAddress: string): boolean => {
2424
return /^0x[0-9a-fA-F]{40}$/.test(hexAddress)
2525
}
2626

27+
export const isValidRoflAppId = (id: string): boolean => {
28+
try {
29+
oasis.address.fromBech32('rofl', id)
30+
return true
31+
} catch (e) {
32+
return false
33+
}
34+
}
35+
2736
export const isValidProposalId = (proposalId: string): boolean => /^[0-9]+$/.test(proposalId)
2837

2938
/** oasis.address.fromData(...) but without being needlessly asynchronous */

src/app/utils/route-utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { LoaderFunctionArgs } from 'react-router-dom'
2-
import { isValidProposalId, isValidTxHash, isValidTxOasisHash } from './helpers'
2+
import { isValidProposalId, isValidRoflAppId, isValidTxHash, isValidTxOasisHash } from './helpers'
33
import { isValidBlockHeight, isValidOasisAddress, isValidEthAddress } from './helpers'
44
import { AppError, AppErrors } from '../../types/errors'
55
import { EvmTokenType, HasScope, Layer } from '../../oasis-nexus/api'
@@ -295,6 +295,21 @@ const validateConsensusAddressParam = (address: string) => {
295295
return isValid
296296
}
297297

298+
const validateRoflAppIdParam = (id: string) => {
299+
const isValid = isValidRoflAppId(id)
300+
301+
if (!isValid) {
302+
throw new AppError(AppErrors.InvalidRoflAppId)
303+
}
304+
305+
return isValid
306+
}
307+
308+
export type RoflAppLoaderData = {
309+
id: string
310+
searchTerm: string
311+
}
312+
298313
const validateRuntimeAddressParam = (address: string) => {
299314
const isValid = isValidOasisAddress(address) || isValidEthAddress(address)
300315
if (!isValid) {
@@ -364,6 +379,16 @@ export const runtimeAddressParamLoader =
364379
}
365380
}
366381

382+
export const roflAppParamLoader =
383+
(queryParam: string = 'id') =>
384+
({ params, request }: LoaderFunctionArgs): RoflAppLoaderData => {
385+
validateRoflAppIdParam(params[queryParam]!)
386+
return {
387+
id: params[queryParam]!,
388+
searchTerm: getSearchTermFromRequest(request),
389+
}
390+
}
391+
367392
export const blockHeightParamLoader = async ({ params }: LoaderFunctionArgs) => {
368393
return validateBlockHeightParam(params.blockHeight!)
369394
}

src/locales/en/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,14 @@
254254
"unsupportedLayer": "Unsupported layer",
255255
"unsupportedNetwork": "Unsupported network",
256256
"invalidAddress": "Invalid address",
257+
"invalidRoflAppId": "Invalid ROFL app ID",
257258
"invalidBlockHeight": "Invalid block height",
258259
"invalidPageNumber": "Invalid page number",
259260
"invalidProposalId": "Invalid proposal ID",
260261
"invalidTxHash": "Invalid transaction hash",
261262
"notFoundBlockHeight": "Block not found",
262263
"notFoundTx": "Transaction not found",
264+
"notFoundRoflApp": "ROFL app not found",
263265
"notFoundProposal": "Proposal not found",
264266
"pageDoesNotExist": "The page you are looking for does not exist.",
265267
"validateURL": "Please validate provided URL",

src/routes.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
fixedLayer,
2727
RouteUtils,
2828
skipGraph,
29+
roflAppParamLoader,
2930
} from './app/utils/route-utils'
3031
import { RoutingErrorPage } from './app/pages/RoutingErrorPage'
3132
import { ThemeByScope, withDefaultTheme } from './app/components/ThemeByScope'
@@ -64,6 +65,7 @@ import { ConsensusAccountEventsCard } from './app/pages/ConsensusAccountDetailsP
6465
import { useConsensusAccountDetailsProps } from './app/pages/ConsensusAccountDetailsPage/hooks'
6566
import { ConsensusAccountTransactionsCard } from './app/pages/ConsensusAccountDetailsPage/ConsensusAccountTransactionsCard'
6667
import { RoflAppsPage } from './app/pages/RoflAppsPage'
68+
import { RoflAppDetailsPage } from 'app/pages/RoflAppDetailsPage'
6769
import { FC, useEffect } from 'react'
6870
import { AnalyticsConsentProvider } from './app/components/AnalyticsConsent'
6971
import { HighlightingContextProvider } from './app/components/HighlightingContext'
@@ -300,6 +302,11 @@ export const routes: RouteObject[] = [
300302
path: `rofl/app`,
301303
element: <RoflAppsPage />,
302304
},
305+
{
306+
path: `rofl/app/:id`,
307+
element: <RoflAppDetailsPage />,
308+
loader: roflAppParamLoader(),
309+
},
303310
],
304311
},
305312
],

src/types/errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ export enum AppErrors {
1313
UnsupportedNetwork = 'unsupported_network',
1414
UnsupportedLayer = 'unsupported_layer',
1515
InvalidAddress = 'invalid_address',
16+
InvalidRoflAppId = 'invalid_rofl_app_id',
1617
InvalidBlockHeight = 'invalid_block_height',
1718
InvalidTxHash = 'invalid_tx_hash',
1819
InvalidProposalId = 'invalid_proposal_id',
1920
InvalidPageNumber = 'invalid_page_number',
2021
PageDoesNotExist = 'page_does_not_exist',
2122
NotFoundBlockHeight = 'not_found_block_height',
2223
NotFoundTxHash = 'not_found_tx_hash',
24+
NotFoundRoflApp = 'not_found_rofl_app',
2325
NotFoundProposalId = 'not_found_proposal_id',
2426
InvalidUrl = 'invalid_url',
2527
InvalidVote = 'invalid_vote',

0 commit comments

Comments
 (0)