Skip to content

Commit 1da3e2a

Browse files
authored
feat: view applications
* wip - showing application * wip - show application * wip - show application global state * wip - test * fix test names * more tests * test global state * wip - boxes * wip - display boxes, the names aren't showing * Show boxes table * add tests for boxes * refactor boxes a bit * view application box page * small fix * PR feedback * update text * evict application result from cache * Switch to show dialogs for boxes * PR feedback * PR feedback
1 parent 6305ace commit 1da3e2a

27 files changed

+894
-153
lines changed

src/App.routes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export const routes = evalTemplates([
7979
},
8080
{
8181
template: Urls.Explore.Application.ById,
82-
element: <ApplicationPage />,
8382
errorElement: <ErrorPage title={applicationPageTitle} />,
83+
element: <ApplicationPage />,
8484
},
8585
],
8686
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { cn } from '@/features/common/utils'
2+
import { applicationBoxNameLabel, applicationBoxValueLabel } from './labels'
3+
import { useMemo } from 'react'
4+
import { DescriptionList } from '@/features/common/components/description-list'
5+
import { ApplicationBox } from '../models'
6+
import { ApplicationId } from '../data/types'
7+
import { RenderLoadable } from '@/features/common/components/render-loadable'
8+
import { useLoadableApplicationBox } from '../data/application-boxes'
9+
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '@/features/common/components/dialog'
10+
11+
type Props = { applicationId: ApplicationId; boxName: string }
12+
13+
const dialogTitle = 'Application Box'
14+
export function ApplicationBoxDetailsDialog({ applicationId, boxName }: Props) {
15+
return (
16+
<Dialog>
17+
<DialogTrigger>
18+
<label className={cn('text-primary underline')}>{boxName}</label>
19+
</DialogTrigger>
20+
<DialogContent className="w-[800px]">
21+
<DialogHeader>
22+
<h1 className={cn('text-2xl text-primary font-bold')}>{dialogTitle}</h1>
23+
</DialogHeader>
24+
<InternalDialogContent applicationId={applicationId} boxName={boxName} />
25+
</DialogContent>
26+
</Dialog>
27+
)
28+
}
29+
30+
function InternalDialogContent({ applicationId, boxName }: Props) {
31+
const loadableApplicationBox = useLoadableApplicationBox(applicationId, boxName)
32+
33+
return (
34+
<RenderLoadable loadable={loadableApplicationBox}>
35+
{(applicationBox) => <ApplicationBoxDetails applicationBox={applicationBox} />}
36+
</RenderLoadable>
37+
)
38+
}
39+
40+
function ApplicationBoxDetails({ applicationBox }: { applicationBox: ApplicationBox }) {
41+
const items = useMemo(
42+
() => [
43+
{
44+
dt: applicationBoxNameLabel,
45+
dd: applicationBox.name,
46+
},
47+
{
48+
dt: applicationBoxValueLabel,
49+
dd: (
50+
<div className="grid">
51+
<div className="overflow-y-auto break-words"> {applicationBox.value}</div>
52+
</div>
53+
),
54+
},
55+
],
56+
[applicationBox.name, applicationBox.value]
57+
)
58+
59+
return <DescriptionList items={items} />
60+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table'
2+
import { useFetchNextApplicationBoxPage } from '../data/application-boxes'
3+
import { ApplicationId } from '../data/types'
4+
import { ColumnDef } from '@tanstack/react-table'
5+
import { ApplicationBoxSummary } from '../models'
6+
import { useMemo } from 'react'
7+
import { ApplicationBoxDetailsDialog } from './application-box-details-dialog'
8+
9+
type Props = {
10+
applicationId: ApplicationId
11+
}
12+
13+
export function ApplicationBoxes({ applicationId }: Props) {
14+
const fetchNextPage = useFetchNextApplicationBoxPage(applicationId)
15+
const tableColumns = useMemo(() => createTableColumns(applicationId), [applicationId])
16+
17+
return <LazyLoadDataTable columns={tableColumns} fetchNextPage={fetchNextPage} />
18+
}
19+
20+
const createTableColumns = (applicationId: ApplicationId): ColumnDef<ApplicationBoxSummary>[] => [
21+
{
22+
header: 'Name',
23+
accessorKey: 'name',
24+
cell: (context) => {
25+
const boxName = context.getValue<string>()
26+
return <ApplicationBoxDetailsDialog applicationId={applicationId} boxName={boxName} />
27+
},
28+
},
29+
]
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Card, CardContent } from '@/features/common/components/card'
2+
import { DescriptionList } from '@/features/common/components/description-list'
3+
import { cn } from '@/features/common/utils'
4+
import { Application } from '../models'
5+
import { useMemo } from 'react'
6+
import {
7+
applicationAccountLabel,
8+
applicationApprovalProgramLabel,
9+
applicationApprovalProgramTabsListAriaLabel,
10+
applicationBoxesLabel,
11+
applicationClearStateProgramLabel,
12+
applicationClearStateProgramTabsListAriaLabel,
13+
applicationCreatorAccountLabel,
14+
applicationDetailsLabel,
15+
applicationGlobalStateByteLabel,
16+
applicationGlobalStateLabel,
17+
applicationGlobalStateUintLabel,
18+
applicationIdLabel,
19+
applicationLocalStateByteLabel,
20+
applicationLocalStateUintLabel,
21+
} from './labels'
22+
import { isDefined } from '@/utils/is-defined'
23+
import { ApplicationProgram } from './application-program'
24+
import { ApplicationGlobalStateTable } from './application-global-state-table'
25+
import { ApplicationBoxes } from './application-boxes'
26+
27+
type Props = {
28+
application: Application
29+
}
30+
31+
export function ApplicationDetails({ application }: Props) {
32+
const applicationItems = useMemo(
33+
() => [
34+
{
35+
dt: applicationIdLabel,
36+
dd: application.id,
37+
},
38+
{
39+
dt: applicationCreatorAccountLabel,
40+
dd: application.creator,
41+
},
42+
{
43+
dt: applicationAccountLabel,
44+
dd: application.account,
45+
},
46+
application.globalStateSchema
47+
? {
48+
dt: applicationGlobalStateByteLabel,
49+
dd: application.globalStateSchema.numByteSlice,
50+
}
51+
: undefined,
52+
application.localStateSchema
53+
? {
54+
dt: applicationLocalStateByteLabel,
55+
dd: application.localStateSchema.numByteSlice,
56+
}
57+
: undefined,
58+
application.globalStateSchema
59+
? {
60+
dt: applicationGlobalStateUintLabel,
61+
dd: application.globalStateSchema.numUint,
62+
}
63+
: undefined,
64+
application.localStateSchema
65+
? {
66+
dt: applicationLocalStateUintLabel,
67+
dd: application.localStateSchema.numUint,
68+
}
69+
: undefined,
70+
],
71+
[application.id, application.creator, application.account, application.globalStateSchema, application.localStateSchema]
72+
).filter(isDefined)
73+
74+
return (
75+
<div className={cn('space-y-6 pt-7')}>
76+
<Card aria-label={applicationDetailsLabel} className={cn('p-4')}>
77+
<CardContent className={cn('text-sm space-y-2')}>
78+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationDetailsLabel}</h1>
79+
<DescriptionList items={applicationItems} />
80+
</CardContent>
81+
</Card>
82+
<Card aria-label={applicationApprovalProgramLabel} className={cn('p-4')}>
83+
<CardContent className={cn('text-sm space-y-2')}>
84+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationApprovalProgramLabel}</h1>
85+
<ApplicationProgram tabsListAriaLabel={applicationApprovalProgramTabsListAriaLabel} base64Program={application.approvalProgram} />
86+
</CardContent>
87+
</Card>
88+
<Card aria-label={applicationClearStateProgramLabel} className={cn('p-4')}>
89+
<CardContent className={cn('text-sm space-y-2')}>
90+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationClearStateProgramLabel}</h1>
91+
<ApplicationProgram
92+
tabsListAriaLabel={applicationClearStateProgramTabsListAriaLabel}
93+
base64Program={application.clearStateProgram}
94+
/>
95+
</CardContent>
96+
</Card>
97+
<Card aria-label={applicationGlobalStateLabel} className={cn('p-4')}>
98+
<CardContent className={cn('text-sm space-y-2')}>
99+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationGlobalStateLabel}</h1>
100+
<ApplicationGlobalStateTable application={application} />
101+
</CardContent>
102+
</Card>
103+
<Card aria-label={applicationBoxesLabel} className={cn('p-4')}>
104+
<CardContent className={cn('text-sm space-y-2')}>
105+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationBoxesLabel}</h1>
106+
<ApplicationBoxes applicationId={application.id} />
107+
</CardContent>
108+
</Card>
109+
</div>
110+
)
111+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ColumnDef } from '@tanstack/react-table'
2+
import { Application, ApplicationGlobalStateValue } from '../models'
3+
import { DataTable } from '@/features/common/components/data-table'
4+
import { useMemo } from 'react'
5+
6+
type Props = {
7+
application: Application
8+
}
9+
10+
export function ApplicationGlobalStateTable({ application }: Props) {
11+
const entries = useMemo(() => Array.from(application.globalState.entries()), [application])
12+
return <DataTable columns={tableColumns} data={entries} />
13+
}
14+
15+
const tableColumns: ColumnDef<[string, ApplicationGlobalStateValue]>[] = [
16+
{
17+
header: 'Key',
18+
accessorFn: (item) => item,
19+
cell: (c) => c.getValue<[string, ApplicationGlobalStateValue]>()[0],
20+
},
21+
{
22+
header: 'Type',
23+
accessorFn: (item) => item,
24+
cell: (c) => c.getValue<[string, ApplicationGlobalStateValue]>()[1].type,
25+
},
26+
{
27+
header: 'Value',
28+
accessorFn: (item) => item,
29+
cell: (c) => c.getValue<[string, ApplicationGlobalStateValue]>()[1].value,
30+
},
31+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { cn } from '@/features/common/utils'
2+
import { TemplatedNavLink } from '@/features/routing/components/templated-nav-link/templated-nav-link'
3+
import { Urls } from '@/routes/urls'
4+
import { PropsWithChildren } from 'react'
5+
import { ApplicationId } from '../data/types'
6+
7+
type Props = PropsWithChildren<{
8+
applicationId: ApplicationId
9+
className?: string
10+
}>
11+
12+
export function ApplicationLink({ applicationId, className, children }: Props) {
13+
return (
14+
<TemplatedNavLink
15+
className={cn(!children && 'text-primary underline', className)}
16+
urlTemplate={Urls.Explore.Application.ById}
17+
urlParams={{ applicationId: applicationId.toString() }}
18+
>
19+
{children ? children : applicationId}
20+
</TemplatedNavLink>
21+
)
22+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { getByRole, render, waitFor } from '../../../tests/testing-library'
3+
import { ApplicationProgram, base64ProgramTabLabel, tealProgramTabLabel } from './application-program'
4+
import { algod } from '@/features/common/data'
5+
import { executeComponentTest } from '@/tests/test-component'
6+
7+
describe('application-program', () => {
8+
describe('when rendering an application program', () => {
9+
const tabListName = 'test'
10+
const program = 'CIEBQw=='
11+
const teal = '\n#pragma version 8\nint 1\nreturn\n'
12+
13+
it('should be rendered with the correct data', () => {
14+
vi.mocked(algod.disassemble('').do).mockImplementation(() => Promise.resolve({ result: teal }))
15+
16+
return executeComponentTest(
17+
() => {
18+
return render(<ApplicationProgram tabsListAriaLabel={tabListName} base64Program={program} />)
19+
},
20+
async (component, user) => {
21+
const tabList = component.getByRole('tablist', { name: tabListName })
22+
expect(tabList).toBeTruthy()
23+
expect(tabList.children.length).toBe(2)
24+
25+
const base64Tab = component.getByRole('tabpanel', { name: base64ProgramTabLabel })
26+
expect(base64Tab.getAttribute('data-state'), 'Base64 tab should be active').toBe('active')
27+
expect(base64Tab.textContent).toBe(program)
28+
29+
await user.click(getByRole(tabList, 'tab', { name: tealProgramTabLabel }))
30+
const tealTab = component.getByRole('tabpanel', { name: tealProgramTabLabel })
31+
await waitFor(() => expect(tealTab.getAttribute('data-state'), 'Teal tab should be active').toBe('active'))
32+
expect(tealTab.textContent).toBe(teal)
33+
}
34+
)
35+
})
36+
})
37+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { RenderLoadable } from '@/features/common/components/render-loadable'
2+
import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs'
3+
import { cn } from '@/features/common/utils'
4+
import { useProgramTeal } from '../data/program-teal'
5+
6+
const base64ProgramTabId = 'base64'
7+
const tealProgramTabId = 'teal'
8+
export const base64ProgramTabLabel = 'Base64'
9+
export const tealProgramTabLabel = 'Teal'
10+
11+
type Props = {
12+
tabsListAriaLabel: string
13+
base64Program: string
14+
}
15+
export function ApplicationProgram({ tabsListAriaLabel, base64Program }: Props) {
16+
const [tealLoadable, fetchTeal] = useProgramTeal(base64Program)
17+
18+
return (
19+
<Tabs
20+
defaultValue={base64ProgramTabId}
21+
onValueChange={(activeTab) => {
22+
if (activeTab === tealProgramTabId) {
23+
fetchTeal()
24+
}
25+
}}
26+
>
27+
<TabsList aria-label={tabsListAriaLabel}>
28+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={base64ProgramTabId}>
29+
{base64ProgramTabLabel}
30+
</TabsTrigger>
31+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={tealProgramTabId}>
32+
{tealProgramTabLabel}
33+
</TabsTrigger>
34+
</TabsList>
35+
<OverflowAutoTabsContent value={base64ProgramTabId}>
36+
<pre>{base64Program}</pre>
37+
</OverflowAutoTabsContent>
38+
<OverflowAutoTabsContent value={tealProgramTabId}>
39+
<div className="h-96">
40+
<RenderLoadable loadable={tealLoadable}>{(teal) => <pre>{teal}</pre>}</RenderLoadable>
41+
</div>
42+
</OverflowAutoTabsContent>
43+
</Tabs>
44+
)
45+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const applicationDetailsLabel = 'Application Details'
2+
export const applicationIdLabel = 'Application ID'
3+
export const applicationCreatorAccountLabel = 'Creator'
4+
export const applicationAccountLabel = 'Account'
5+
export const applicationGlobalStateByteLabel = 'Global State Byte'
6+
export const applicationGlobalStateUintLabel = 'Global State Uint'
7+
export const applicationLocalStateByteLabel = 'Local State Byte'
8+
export const applicationLocalStateUintLabel = 'Local State Uint'
9+
10+
export const applicationProgramsLabel = 'Application Programs'
11+
export const applicationApprovalProgramLabel = 'Approval Program'
12+
export const applicationClearStateProgramLabel = 'Clear State Program'
13+
export const applicationApprovalProgramTabsListAriaLabel = 'View Application Approval Program Tabs'
14+
export const applicationClearStateProgramTabsListAriaLabel = 'View Application Clear State Program Tabs'
15+
16+
export const applicationGlobalStateLabel = 'Global State'
17+
18+
export const applicationBoxesLabel = 'Boxes'
19+
20+
export const applicationBoxNameLabel = 'Box Name'
21+
export const applicationBoxValueLabel = 'Box Value'

0 commit comments

Comments
 (0)