Skip to content

Commit

Permalink
feat: view applications
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
PatrickDinh authored May 20, 2024
1 parent 6305ace commit 1da3e2a
Show file tree
Hide file tree
Showing 27 changed files with 894 additions and 153 deletions.
2 changes: 1 addition & 1 deletion src/App.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export const routes = evalTemplates([
},
{
template: Urls.Explore.Application.ById,
element: <ApplicationPage />,
errorElement: <ErrorPage title={applicationPageTitle} />,
element: <ApplicationPage />,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { cn } from '@/features/common/utils'
import { applicationBoxNameLabel, applicationBoxValueLabel } from './labels'
import { useMemo } from 'react'
import { DescriptionList } from '@/features/common/components/description-list'
import { ApplicationBox } from '../models'
import { ApplicationId } from '../data/types'
import { RenderLoadable } from '@/features/common/components/render-loadable'
import { useLoadableApplicationBox } from '../data/application-boxes'
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '@/features/common/components/dialog'

type Props = { applicationId: ApplicationId; boxName: string }

const dialogTitle = 'Application Box'
export function ApplicationBoxDetailsDialog({ applicationId, boxName }: Props) {
return (
<Dialog>
<DialogTrigger>
<label className={cn('text-primary underline')}>{boxName}</label>
</DialogTrigger>
<DialogContent className="w-[800px]">
<DialogHeader>
<h1 className={cn('text-2xl text-primary font-bold')}>{dialogTitle}</h1>
</DialogHeader>
<InternalDialogContent applicationId={applicationId} boxName={boxName} />
</DialogContent>
</Dialog>
)
}

function InternalDialogContent({ applicationId, boxName }: Props) {
const loadableApplicationBox = useLoadableApplicationBox(applicationId, boxName)

return (
<RenderLoadable loadable={loadableApplicationBox}>
{(applicationBox) => <ApplicationBoxDetails applicationBox={applicationBox} />}
</RenderLoadable>
)
}

function ApplicationBoxDetails({ applicationBox }: { applicationBox: ApplicationBox }) {
const items = useMemo(
() => [
{
dt: applicationBoxNameLabel,
dd: applicationBox.name,
},
{
dt: applicationBoxValueLabel,
dd: (
<div className="grid">
<div className="overflow-y-auto break-words"> {applicationBox.value}</div>
</div>
),
},
],
[applicationBox.name, applicationBox.value]
)

return <DescriptionList items={items} />
}
29 changes: 29 additions & 0 deletions src/features/applications/components/application-boxes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table'
import { useFetchNextApplicationBoxPage } from '../data/application-boxes'
import { ApplicationId } from '../data/types'
import { ColumnDef } from '@tanstack/react-table'
import { ApplicationBoxSummary } from '../models'
import { useMemo } from 'react'
import { ApplicationBoxDetailsDialog } from './application-box-details-dialog'

type Props = {
applicationId: ApplicationId
}

export function ApplicationBoxes({ applicationId }: Props) {
const fetchNextPage = useFetchNextApplicationBoxPage(applicationId)
const tableColumns = useMemo(() => createTableColumns(applicationId), [applicationId])

return <LazyLoadDataTable columns={tableColumns} fetchNextPage={fetchNextPage} />
}

const createTableColumns = (applicationId: ApplicationId): ColumnDef<ApplicationBoxSummary>[] => [
{
header: 'Name',
accessorKey: 'name',
cell: (context) => {
const boxName = context.getValue<string>()
return <ApplicationBoxDetailsDialog applicationId={applicationId} boxName={boxName} />
},
},
]
111 changes: 111 additions & 0 deletions src/features/applications/components/application-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Card, CardContent } from '@/features/common/components/card'
import { DescriptionList } from '@/features/common/components/description-list'
import { cn } from '@/features/common/utils'
import { Application } from '../models'
import { useMemo } from 'react'
import {
applicationAccountLabel,
applicationApprovalProgramLabel,
applicationApprovalProgramTabsListAriaLabel,
applicationBoxesLabel,
applicationClearStateProgramLabel,
applicationClearStateProgramTabsListAriaLabel,
applicationCreatorAccountLabel,
applicationDetailsLabel,
applicationGlobalStateByteLabel,
applicationGlobalStateLabel,
applicationGlobalStateUintLabel,
applicationIdLabel,
applicationLocalStateByteLabel,
applicationLocalStateUintLabel,
} from './labels'
import { isDefined } from '@/utils/is-defined'
import { ApplicationProgram } from './application-program'
import { ApplicationGlobalStateTable } from './application-global-state-table'
import { ApplicationBoxes } from './application-boxes'

type Props = {
application: Application
}

export function ApplicationDetails({ application }: Props) {
const applicationItems = useMemo(
() => [
{
dt: applicationIdLabel,
dd: application.id,
},
{
dt: applicationCreatorAccountLabel,
dd: application.creator,
},
{
dt: applicationAccountLabel,
dd: application.account,
},
application.globalStateSchema
? {
dt: applicationGlobalStateByteLabel,
dd: application.globalStateSchema.numByteSlice,
}
: undefined,
application.localStateSchema
? {
dt: applicationLocalStateByteLabel,
dd: application.localStateSchema.numByteSlice,
}
: undefined,
application.globalStateSchema
? {
dt: applicationGlobalStateUintLabel,
dd: application.globalStateSchema.numUint,
}
: undefined,
application.localStateSchema
? {
dt: applicationLocalStateUintLabel,
dd: application.localStateSchema.numUint,
}
: undefined,
],
[application.id, application.creator, application.account, application.globalStateSchema, application.localStateSchema]
).filter(isDefined)

return (
<div className={cn('space-y-6 pt-7')}>
<Card aria-label={applicationDetailsLabel} className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationDetailsLabel}</h1>
<DescriptionList items={applicationItems} />
</CardContent>
</Card>
<Card aria-label={applicationApprovalProgramLabel} className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationApprovalProgramLabel}</h1>
<ApplicationProgram tabsListAriaLabel={applicationApprovalProgramTabsListAriaLabel} base64Program={application.approvalProgram} />
</CardContent>
</Card>
<Card aria-label={applicationClearStateProgramLabel} className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationClearStateProgramLabel}</h1>
<ApplicationProgram
tabsListAriaLabel={applicationClearStateProgramTabsListAriaLabel}
base64Program={application.clearStateProgram}
/>
</CardContent>
</Card>
<Card aria-label={applicationGlobalStateLabel} className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationGlobalStateLabel}</h1>
<ApplicationGlobalStateTable application={application} />
</CardContent>
</Card>
<Card aria-label={applicationBoxesLabel} className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationBoxesLabel}</h1>
<ApplicationBoxes applicationId={application.id} />
</CardContent>
</Card>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ColumnDef } from '@tanstack/react-table'
import { Application, ApplicationGlobalStateValue } from '../models'
import { DataTable } from '@/features/common/components/data-table'
import { useMemo } from 'react'

type Props = {
application: Application
}

export function ApplicationGlobalStateTable({ application }: Props) {
const entries = useMemo(() => Array.from(application.globalState.entries()), [application])
return <DataTable columns={tableColumns} data={entries} />
}

const tableColumns: ColumnDef<[string, ApplicationGlobalStateValue]>[] = [
{
header: 'Key',
accessorFn: (item) => item,
cell: (c) => c.getValue<[string, ApplicationGlobalStateValue]>()[0],
},
{
header: 'Type',
accessorFn: (item) => item,
cell: (c) => c.getValue<[string, ApplicationGlobalStateValue]>()[1].type,
},
{
header: 'Value',
accessorFn: (item) => item,
cell: (c) => c.getValue<[string, ApplicationGlobalStateValue]>()[1].value,
},
]
22 changes: 22 additions & 0 deletions src/features/applications/components/application-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { cn } from '@/features/common/utils'
import { TemplatedNavLink } from '@/features/routing/components/templated-nav-link/templated-nav-link'
import { Urls } from '@/routes/urls'
import { PropsWithChildren } from 'react'
import { ApplicationId } from '../data/types'

type Props = PropsWithChildren<{
applicationId: ApplicationId
className?: string
}>

export function ApplicationLink({ applicationId, className, children }: Props) {
return (
<TemplatedNavLink
className={cn(!children && 'text-primary underline', className)}
urlTemplate={Urls.Explore.Application.ById}
urlParams={{ applicationId: applicationId.toString() }}
>
{children ? children : applicationId}
</TemplatedNavLink>
)
}
37 changes: 37 additions & 0 deletions src/features/applications/components/application-program.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from 'vitest'
import { getByRole, render, waitFor } from '../../../tests/testing-library'
import { ApplicationProgram, base64ProgramTabLabel, tealProgramTabLabel } from './application-program'
import { algod } from '@/features/common/data'
import { executeComponentTest } from '@/tests/test-component'

describe('application-program', () => {
describe('when rendering an application program', () => {
const tabListName = 'test'
const program = 'CIEBQw=='
const teal = '\n#pragma version 8\nint 1\nreturn\n'

it('should be rendered with the correct data', () => {
vi.mocked(algod.disassemble('').do).mockImplementation(() => Promise.resolve({ result: teal }))

return executeComponentTest(
() => {
return render(<ApplicationProgram tabsListAriaLabel={tabListName} base64Program={program} />)
},
async (component, user) => {
const tabList = component.getByRole('tablist', { name: tabListName })
expect(tabList).toBeTruthy()
expect(tabList.children.length).toBe(2)

const base64Tab = component.getByRole('tabpanel', { name: base64ProgramTabLabel })
expect(base64Tab.getAttribute('data-state'), 'Base64 tab should be active').toBe('active')
expect(base64Tab.textContent).toBe(program)

await user.click(getByRole(tabList, 'tab', { name: tealProgramTabLabel }))
const tealTab = component.getByRole('tabpanel', { name: tealProgramTabLabel })
await waitFor(() => expect(tealTab.getAttribute('data-state'), 'Teal tab should be active').toBe('active'))
expect(tealTab.textContent).toBe(teal)
}
)
})
})
})
45 changes: 45 additions & 0 deletions src/features/applications/components/application-program.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { RenderLoadable } from '@/features/common/components/render-loadable'
import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs'
import { cn } from '@/features/common/utils'
import { useProgramTeal } from '../data/program-teal'

const base64ProgramTabId = 'base64'
const tealProgramTabId = 'teal'
export const base64ProgramTabLabel = 'Base64'
export const tealProgramTabLabel = 'Teal'

type Props = {
tabsListAriaLabel: string
base64Program: string
}
export function ApplicationProgram({ tabsListAriaLabel, base64Program }: Props) {
const [tealLoadable, fetchTeal] = useProgramTeal(base64Program)

return (
<Tabs
defaultValue={base64ProgramTabId}
onValueChange={(activeTab) => {
if (activeTab === tealProgramTabId) {
fetchTeal()
}
}}
>
<TabsList aria-label={tabsListAriaLabel}>
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={base64ProgramTabId}>
{base64ProgramTabLabel}
</TabsTrigger>
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={tealProgramTabId}>
{tealProgramTabLabel}
</TabsTrigger>
</TabsList>
<OverflowAutoTabsContent value={base64ProgramTabId}>
<pre>{base64Program}</pre>
</OverflowAutoTabsContent>
<OverflowAutoTabsContent value={tealProgramTabId}>
<div className="h-96">
<RenderLoadable loadable={tealLoadable}>{(teal) => <pre>{teal}</pre>}</RenderLoadable>
</div>
</OverflowAutoTabsContent>
</Tabs>
)
}
21 changes: 21 additions & 0 deletions src/features/applications/components/labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const applicationDetailsLabel = 'Application Details'
export const applicationIdLabel = 'Application ID'
export const applicationCreatorAccountLabel = 'Creator'
export const applicationAccountLabel = 'Account'
export const applicationGlobalStateByteLabel = 'Global State Byte'
export const applicationGlobalStateUintLabel = 'Global State Uint'
export const applicationLocalStateByteLabel = 'Local State Byte'
export const applicationLocalStateUintLabel = 'Local State Uint'

export const applicationProgramsLabel = 'Application Programs'
export const applicationApprovalProgramLabel = 'Approval Program'
export const applicationClearStateProgramLabel = 'Clear State Program'
export const applicationApprovalProgramTabsListAriaLabel = 'View Application Approval Program Tabs'
export const applicationClearStateProgramTabsListAriaLabel = 'View Application Clear State Program Tabs'

export const applicationGlobalStateLabel = 'Global State'

export const applicationBoxesLabel = 'Boxes'

export const applicationBoxNameLabel = 'Box Name'
export const applicationBoxValueLabel = 'Box Value'
Loading

0 comments on commit 1da3e2a

Please sign in to comment.