Skip to content

Commit 6cb0b0e

Browse files
authored
Merge pull request #1895 from oasisprotocol/mz/roflInstance
Create ROFL app instance details page
2 parents 7ad0465 + eb1f988 commit 6cb0b0e

File tree

14 files changed

+386
-5
lines changed

14 files changed

+386
-5
lines changed

.changelog/1895.bugfix.md

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

src/app/components/ErrorDisplay/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const errorMap: Record<AppErrors, (t: TFunction, error: ErrorPayload) => Formatt
5757
title: t('errors.notFoundRoflApp'),
5858
message: t('errors.validateURL'),
5959
}),
60+
[AppErrors.NotFoundRoflAppInstance]: t => ({
61+
title: t('errors.notFoundRoflAppInstance'),
62+
message: t('errors.validateURL'),
63+
}),
6064
[AppErrors.NotFoundProposalId]: t => ({
6165
title: t('errors.notFoundProposal'),
6266
message: t('errors.validateURL'),
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { FC } from 'react'
2+
import { RouteUtils } from '../../utils/route-utils'
3+
import { Network } from '../../../types/network'
4+
import { Link } from '../Link'
5+
6+
type RoflAppInstanceLinkProps = {
7+
alwaysTrim?: boolean
8+
id: string
9+
network: Network
10+
rak: string
11+
}
12+
13+
export const RoflAppInstanceLink: FC<RoflAppInstanceLinkProps> = ({ alwaysTrim, id, network, rak }) => {
14+
const to = RouteUtils.getRoflAppInstanceRoute(network, id, rak)
15+
16+
return <Link address={rak} to={to} alwaysTrim={alwaysTrim} withSourceIndicator={false} />
17+
}

src/app/pages/RoflAppDetailsPage/InstancesCard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const InstancesView: FC<RoflAppDetailsContext> = ({ scope, id }) => {
4141
isTotalCountClipped: data?.data.is_total_count_clipped,
4242
rowsPerPage: limit,
4343
}}
44+
appId={id}
45+
scope={scope}
4446
/>
4547
)
4648
}

src/app/pages/RoflAppDetailsPage/InstancesList.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ import { FC } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import Typography from '@mui/material/Typography'
44
import { RoflInstance } from '../../../oasis-nexus/api'
5+
import { SearchScope } from '../../../types/searchScope'
56
import { COLORS } from '../../../styles/theme/colors'
7+
import { useScreenSize } from '../../hooks/useScreensize'
8+
import { trimLongString } from '../../utils/trimLongString'
69
import { Table, TableCellAlign, TableColProps } from '../../components/Table'
710
import { TablePaginationProps } from '../../components/Table/TablePagination'
8-
import { RoflAppInstanceStatusBadge } from 'app/components/Rofl/RoflAppInstanceStatusBadge'
11+
import { RoflAppInstanceStatusBadge } from '../../components/Rofl/RoflAppInstanceStatusBadge'
12+
import { RoflAppInstanceLink } from '../../components/Rofl/RoflAppInstanceLink'
913

1014
type InstancesListProps = {
1115
currentEpoch: number | undefined
1216
instances: RoflInstance[] | undefined
1317
isLoading: boolean
1418
limit: number
1519
pagination: false | TablePaginationProps
20+
appId: string
21+
scope: SearchScope
1622
}
1723

1824
export const InstancesList: FC<InstancesListProps> = ({
@@ -21,15 +27,18 @@ export const InstancesList: FC<InstancesListProps> = ({
2127
pagination,
2228
instances,
2329
currentEpoch,
30+
appId,
31+
scope,
2432
}) => {
2533
const { t } = useTranslation()
34+
const { isTablet } = useScreenSize()
35+
2636
const tableColumns: TableColProps[] = [
2737
{ key: 'rak', content: t('rofl.rakAbbreviation') },
2838
{ key: 'node', content: t('rofl.nodeId') },
2939
{ key: 'expirationEpoch', content: t('rofl.expirationEpoch'), align: TableCellAlign.Right },
3040
{ key: 'expirationStatus', content: t('common.status'), align: TableCellAlign.Right },
3141
]
32-
3342
const tableRows =
3443
currentEpoch !== undefined && instances
3544
? instances?.map(instance => {
@@ -41,11 +50,15 @@ export const InstancesList: FC<InstancesListProps> = ({
4150
data: [
4251
{
4352
key: 'rak',
44-
content: <Typography variant="mono">{instance.rak}</Typography>,
53+
content: <RoflAppInstanceLink id={appId} network={scope.network} rak={instance.rak} />,
4554
},
4655
{
4756
key: 'node',
48-
content: <Typography variant="mono">{instance.endorsing_node_id}</Typography>,
57+
content: (
58+
<Typography variant="mono">
59+
{isTablet ? trimLongString(instance.endorsing_node_id) : instance.endorsing_node_id}
60+
</Typography>
61+
),
4962
},
5063
{
5164
key: 'expirationEpoch',
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { FC } from 'react'
2+
import Box from '@mui/material/Box'
3+
import { useScreenSize } from '../../hooks/useScreensize'
4+
import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE } from '../../config'
5+
import { transactionsContainerId } from '../../utils/tabAnchors'
6+
import { LinkableCardLayout } from '../../components/LinkableCardLayout'
7+
import { RuntimeTransactions } from '../../components/Transactions'
8+
import { RuntimeTransactionTypeFilter } from '../../components/Transactions/RuntimeTransactionTypeFilter'
9+
import { RoflAppInstanceDetailsContext, useRoflAppInstanceRakTransactions } from './hooks'
10+
11+
export const RoflAppInstanceRakTransactionsCard: FC<RoflAppInstanceDetailsContext> = context => {
12+
const { method, setMethod, scope } = context
13+
const { isMobile } = useScreenSize()
14+
15+
return (
16+
<LinkableCardLayout
17+
containerId={transactionsContainerId}
18+
title={
19+
<Box
20+
sx={{
21+
display: 'flex',
22+
justifyContent: 'end',
23+
}}
24+
>
25+
{!isMobile && (
26+
<RuntimeTransactionTypeFilter layer={scope.layer} value={method} setValue={setMethod} />
27+
)}
28+
</Box>
29+
}
30+
>
31+
{isMobile && (
32+
<RuntimeTransactionTypeFilter layer={scope.layer} value={method} setValue={setMethod} expand />
33+
)}
34+
<RoflAppInstanceRakTransactions {...context} />
35+
</LinkableCardLayout>
36+
)
37+
}
38+
39+
const RoflAppInstanceRakTransactions: FC<RoflAppInstanceDetailsContext> = ({ scope, id, rak, method }) => {
40+
const { isLoading, transactions, pagination, totalCount, isTotalCountClipped } =
41+
useRoflAppInstanceRakTransactions(scope, id, rak, method)
42+
43+
return (
44+
<RuntimeTransactions
45+
transactions={transactions}
46+
isLoading={isLoading}
47+
limit={NUMBER_OF_ITEMS_ON_SEPARATE_PAGE}
48+
pagination={{
49+
selectedPage: pagination.selectedPage,
50+
linkToPage: pagination.linkToPage,
51+
totalCount,
52+
isTotalCountClipped,
53+
rowsPerPage: NUMBER_OF_ITEMS_ON_SEPARATE_PAGE,
54+
}}
55+
filtered={method !== 'any'}
56+
/>
57+
)
58+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useOutletContext } from 'react-router-dom'
2+
import { Layer, useGetRuntimeRoflAppsIdInstancesRakTransactions } from '../../../oasis-nexus/api'
3+
import { AppErrors } from '../../../types/errors'
4+
import { SearchScope } from '../../../types/searchScope'
5+
import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE as limit } from '../../config'
6+
import { getRuntimeTransactionMethodFilteringParam } from '../../components/RuntimeTransactionMethod'
7+
import { useSearchParamsPagination } from '../..//components/Table/useSearchParamsPagination'
8+
9+
export type RoflAppInstanceDetailsContext = {
10+
scope: SearchScope
11+
id: string
12+
rak: string
13+
method: string
14+
setMethod: (value: string) => void
15+
}
16+
17+
export const useRoflAppInstanceDetailsProps = () => useOutletContext<RoflAppInstanceDetailsContext>()
18+
19+
export const useRoflAppInstanceRakTransactions = (
20+
scope: SearchScope,
21+
id: string,
22+
rak: string,
23+
method: string,
24+
) => {
25+
const { network, layer } = scope
26+
const pagination = useSearchParamsPagination('page')
27+
const offset = (pagination.selectedPage - 1) * limit
28+
if (layer !== Layer.sapphire) {
29+
throw AppErrors.UnsupportedLayer
30+
}
31+
32+
const query = useGetRuntimeRoflAppsIdInstancesRakTransactions(network, Layer.sapphire, id, rak, {
33+
limit,
34+
offset: offset,
35+
...getRuntimeTransactionMethodFilteringParam(method),
36+
})
37+
const { isFetched, isLoading, data } = query
38+
const transactions = data?.data.transactions
39+
40+
if (isFetched && pagination.selectedPage > 1 && !transactions?.length) {
41+
throw AppErrors.PageDoesNotExist
42+
}
43+
44+
const totalCount = data?.data.total_count
45+
const isTotalCountClipped = data?.data.is_total_count_clipped
46+
47+
return {
48+
isLoading,
49+
isFetched,
50+
transactions,
51+
pagination,
52+
totalCount,
53+
isTotalCountClipped,
54+
}
55+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { FC } from 'react'
2+
import { useHref, useParams } from 'react-router-dom'
3+
import { useTranslation } from 'react-i18next'
4+
import Typography from '@mui/material/Typography'
5+
import { Layer, RoflInstance, useGetRuntimeRoflAppsIdInstancesRak } from '../../../oasis-nexus/api'
6+
import { SearchScope } from '../../../types/searchScope'
7+
import { AppErrors } from '../../../types/errors'
8+
import { RoflAppInstanceDetailsContext } from './hooks'
9+
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
10+
import { useScreenSize } from '../../hooks/useScreensize'
11+
import { PageLayout } from '../../components/PageLayout'
12+
import { SubPageCard } from '../../components/SubPageCard'
13+
import { TextSkeleton } from '../../components/Skeleton'
14+
import { StyledDescriptionList } from '../../components/StyledDescriptionList'
15+
import { RouterTabs } from '../../components/RouterTabs'
16+
import { RoflAppLink } from '../../components/Rofl/RoflAppLink'
17+
import { CopyToClipboard } from '../../components/CopyToClipboard'
18+
import { useTypedSearchParam } from '../../hooks/useTypedSearchParam'
19+
20+
export const RoflAppInstanceDetailsPage: FC = () => {
21+
const { t } = useTranslation()
22+
const scope = useRequiredScopeParam()
23+
const id = useParams().id!
24+
const rak = useParams().rak!
25+
const txLink = useHref('')
26+
const [method, setMethod] = useTypedSearchParam('method', 'any', {
27+
deleteParams: ['page'],
28+
})
29+
const context: RoflAppInstanceDetailsContext = { scope, id, rak, method, setMethod }
30+
const instancesQuery = useGetRuntimeRoflAppsIdInstancesRak(scope.network, Layer.sapphire, id, rak)
31+
const { isLoading, isFetched, data } = instancesQuery
32+
const instance = data?.data
33+
34+
if (!instance && isFetched) {
35+
throw AppErrors.NotFoundRoflAppInstance
36+
}
37+
38+
return (
39+
<PageLayout>
40+
<SubPageCard featured title={t('rofl.instanceDetails')}>
41+
<RoflAppInstanceDetailsView isLoading={isLoading} appId={id} instance={instance} scope={scope} />
42+
</SubPageCard>
43+
<RouterTabs tabs={[{ label: t('common.transactions'), to: txLink }]} context={context} />
44+
</PageLayout>
45+
)
46+
}
47+
48+
export const RoflAppInstanceDetailsView: FC<{
49+
isLoading?: boolean
50+
appId: string
51+
instance: RoflInstance | undefined
52+
scope: SearchScope
53+
}> = ({ appId, instance, isLoading, scope }) => {
54+
const { t } = useTranslation()
55+
const { isMobile } = useScreenSize()
56+
57+
if (isLoading) return <TextSkeleton numberOfRows={6} />
58+
if (!instance) return <></>
59+
60+
return (
61+
<StyledDescriptionList titleWidth={isMobile ? '100px' : '200px'}>
62+
<dt>{t('rofl.rakAbbreviation')}</dt>
63+
<dd>
64+
<Typography variant="mono">
65+
{instance.rak} <CopyToClipboard value={instance.rak} />
66+
</Typography>
67+
</dd>
68+
<dt>{t('rofl.rekAbbreviation')}</dt>
69+
<dd>
70+
<Typography variant="mono">
71+
{instance.rek} <CopyToClipboard value={instance.rek} />
72+
</Typography>
73+
</dd>
74+
<dt>{t('rofl.expirationEpoch')}</dt>
75+
<dd>{instance.expiration_epoch.toLocaleString()}</dd>
76+
<dt>{t('rofl.roflAppId')}</dt>
77+
<dd>
78+
<RoflAppLink id={appId} network={scope.network} withSourceIndicator={false} />
79+
<CopyToClipboard value={appId} />
80+
</dd>
81+
<dt>{t('rofl.endorsingNodeId')}</dt>
82+
<dd>
83+
<Typography variant="mono">
84+
{instance.endorsing_node_id} <CopyToClipboard value={instance.endorsing_node_id} />
85+
</Typography>
86+
</dd>
87+
<dt>{t('rofl.extraKeys')}</dt>
88+
<dd>
89+
{instance.extra_keys.length ? (
90+
<table>
91+
<tbody>
92+
{instance.extra_keys.map((key, index) => {
93+
const entries = Object.entries(JSON.parse(key))
94+
const [keyType, keyValue] = entries[0]
95+
96+
return (
97+
<tr key={index}>
98+
<td>
99+
<Typography variant="mono">{keyType}:</Typography>
100+
</td>
101+
<td>
102+
<Typography variant="mono">{String(keyValue)}</Typography>
103+
</td>
104+
</tr>
105+
)
106+
})}
107+
</tbody>
108+
</table>
109+
) : (
110+
t('common.missing')
111+
)}
112+
</dd>
113+
</StyledDescriptionList>
114+
)
115+
}

src/app/utils/route-utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ export abstract class RouteUtils {
178178
return `/${encodeURIComponent(network)}/sapphire/rofl/app/${encodeURIComponent(id)}`
179179
}
180180

181+
static getRoflAppInstanceRoute = (network: Network, id: string, rak: string) => {
182+
return `/${encodeURIComponent(network)}/sapphire/rofl/app/${encodeURIComponent(id)}/instance/${encodeURIComponent(rak)}`
183+
}
184+
181185
static getAccountTokensRoute = (
182186
scope: SearchScope,
183187
account: string,

src/locales/en/translation.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@
263263
"notFoundBlockHeight": "Block not found",
264264
"notFoundTx": "Transaction not found",
265265
"notFoundRoflApp": "ROFL app not found",
266+
"notFoundRoflAppInstance": "ROFL app instance not found",
266267
"notFoundProposal": "Proposal not found",
267268
"pageDoesNotExist": "The page you are looking for does not exist.",
268269
"validateURL": "Please validate provided URL",
@@ -717,7 +718,12 @@
717718
"secrets": "Secrets",
718719
"updates": "Updates",
719720
"secretLength": "[{{value}} bytes]",
720-
"expired": "Expired"
721+
"expired": "Expired",
722+
"instanceDetails": "Instance details",
723+
"rekAbbreviation": "REK",
724+
"roflAppId": "ROFL App ID",
725+
"endorsingNodeId": "Endorsing node ID",
726+
"extraKeys": "Extra keys"
721727
},
722728
"search": {
723729
"placeholder": "Address, Block, Contract, Transaction hash, Token name, etc.",

0 commit comments

Comments
 (0)