Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add infrastructure tabs and content #2588

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

playwright-report/
test-results/

Comment on lines +10 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for catching this!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You bet! I think there are going to be other similar issues after the relocating of all these files.

package-lock.json
node_modules
dist
Expand Down
2 changes: 1 addition & 1 deletion frontend/console/e2e/infrastructure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, ftlTest } from './ftl-test'
ftlTest('shows infrastructure', async ({ page }) => {
const infrastructureNavItem = page.getByRole('link', { name: 'Infrastructure' })
await infrastructureNavItem.click()
await expect(page).toHaveURL(/\/infrastructure$/)
await expect(page).toHaveURL(/\/infrastructure\/controllers$/)

const controllersTab = await page.getByRole('button', { name: 'Controllers' })
await expect(controllersTab).toBeVisible()
Expand Down
24 changes: 24 additions & 0 deletions frontend/console/src/components/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { classNames } from '../utils'

type ListProps<T> = {
items: T[]
renderItem: (item: T) => React.ReactNode
onClick?: (item: T) => void
className?: string
}

export const List = <T,>({ items, renderItem, onClick, className }: ListProps<T>) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to remove this , or is that doing some fancy JS magic I haven't learned yet? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the , is required for the component to parse correctly. Removing it causes all kinds of errors 😂 . I think it's just a react generic component feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wow that's good to know!

return (
<ul className={classNames('divide-y divide-gray-100 dark:divide-gray-700 overflow-hidden', className)}>
{items.map((item, index) => (
<li
key={index}
className={`relative flex justify-between gap-x-6 p-4 ${onClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700' : ''}`}
onClick={onClick ? () => onClick(item) : undefined}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, if you use this component without an onClick, does passing undefined into this prop definitely not throw a runtime error? I wasn't sure if it was safe to pass undefined to a fn prop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah should be ok. All the current stuff is using it without an onClick.

>
{renderItem(item)}
</li>
))}
</ul>
)
}
24 changes: 24 additions & 0 deletions frontend/console/src/components/StatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type React from 'react'
import { classNames } from '../utils'

type StatusIndicatorProps = {
state: 'success' | 'error' | 'idle'
text?: string
}

export const StatusIndicator: React.FC<StatusIndicatorProps> = ({ state, text }) => {
const backgrounds = {
idle: 'text-gray-500 bg-gray-500/20 dark:bg-gray-100/10',
success: 'text-green-500 bg-emerald-500/20 dark:text-green-400 dark:bg-green-400/10 ',
error: 'text-rose-500 bg-rose-500/20 dark:text-rose-400 dark:bg-rose-400/10',
}

return (
<div className='flex items-center gap-x-1.5'>
<div className={classNames(backgrounds[state], 'flex-none rounded-full p-1')}>
<div className='h-1.5 w-1.5 rounded-full bg-current' />
</div>
{text && <p className='text-xs leading-5 text-gray-500'>{text}</p>}
</div>
)
}
30 changes: 28 additions & 2 deletions frontend/console/src/features/infrastructure/ControllersList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
export const ControllersList = () => {
return <>Controllers Content</>
import { Badge } from '../../components/Badge'
import { List } from '../../components/List'
import type { StatusResponse_Controller } from '../../protos/xyz/block/ftl/v1/ftl_pb'

export const ControllersList = ({ controllers }: { controllers: StatusResponse_Controller[] }) => {
return (
<List
items={controllers}
renderItem={(controller) => (
<>
<div className='flex min-w-0 gap-x-4'>
<div className='min-w-0 flex-auto'>
<p className='text-sm font-semibold leading-6'>
<span className='absolute inset-x-0 -top-px bottom-0' />
{controller.key}
</p>
<p className='mt-1 flex text-xs leading-5 text-gray-500 dark:text-gray-400 font-roboto-mono'>{controller.endpoint}</p>
</div>
</div>
<div className='flex shrink-0 items-center gap-x-4'>
<div className='hidden sm:flex sm:flex-col sm:items-end'>
<Badge name={controller.version} />
</div>
</div>
</>
)}
/>
)
}
39 changes: 37 additions & 2 deletions frontend/console/src/features/infrastructure/DeploymentsList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
export const DeploymentsList = () => {
return <>Deployments Content</>
import { AttributeBadge } from '../../components'
import { Badge } from '../../components/Badge'
import { List } from '../../components/List'
import type { StatusResponse_Deployment } from '../../protos/xyz/block/ftl/v1/ftl_pb'
import { classNames } from '../../utils'
import { deploymentTextColor } from '../deployments/deployment.utils'
import { renderValue } from './infrastructure.utils'

export const DeploymentsList = ({ deployments }: { deployments: StatusResponse_Deployment[] }) => {
return (
<List
items={deployments}
renderItem={(deployment) => (
<div className='flex w-full'>
<div className='flex gap-x-4 items-center w-1/2'>
<div className='whitespace-nowrap'>
<div className='flex gap-x-2 items-center'>
<p>{deployment.name}</p>
<Badge name={deployment.language} />
</div>

<p className={classNames(deploymentTextColor(deployment.key), 'text-sm font-semibold leading-6')}>{deployment.key}</p>
</div>
</div>
<div className='flex gap-x-4 items-center w-1/2 justify-end'>
<div className='flex flex-wrap gap-1'>
<AttributeBadge key='replicas' name='replicas' value={deployment.replicas.toString()} />
<AttributeBadge key='min_replicas' name='min_replicas' value={deployment.minReplicas.toString()} />
{Object.entries(deployment.labels?.fields || {}).map(([key, value]) => (
<AttributeBadge key={key} name={key} value={renderValue(value)} />
))}
</div>
</div>
</div>
)}
/>
)
}
24 changes: 13 additions & 11 deletions frontend/console/src/features/infrastructure/InfrastructurePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import { useStatus } from '../../api/status/use-status'
import { Tabs } from '../../components/Tabs'
import { ControllersList } from './ControllersList'
Expand All @@ -9,7 +9,8 @@ import { RunnersList } from './RunnersList'

export const InfrastructurePage = () => {
const status = useStatus()
const [searchParams, setSearchParams] = useSearchParams()
const navigate = useNavigate()
const location = useLocation()

const [tabs, setTabs] = useState([
{ name: 'Controllers', id: 'controllers' },
Expand Down Expand Up @@ -41,30 +42,31 @@ export const InfrastructurePage = () => {
)
}, [status.data])

const handleTabClick = (tabId: string) => {
setSearchParams({ tab: tabId })
}
const currentTab = location.pathname.split('/').pop()

const currentTab = searchParams.get('tab') || tabs[0].id
const renderTabContent = () => {
switch (currentTab) {
case 'controllers':
return <ControllersList />
return <ControllersList controllers={status.data?.controllers || []} />
case 'runners':
return <RunnersList />
return <RunnersList runners={status.data?.runners || []} />
case 'deployments':
return <DeploymentsList />
return <DeploymentsList deployments={status.data?.deployments || []} />
case 'routes':
return <RoutesList />
return <RoutesList routes={status.data?.routes || []} />
default:
return <></>
}
}

const handleTabClick = (tabId: string) => {
navigate(`/infrastructure/${tabId}`)
}

return (
<div className='px-6'>
<Tabs tabs={tabs} initialTabId={currentTab} onTabClick={handleTabClick} />
<div className='mt-4'>{renderTabContent()}</div>
<div className='mt-2'>{renderTabContent()}</div>
</div>
)
}
28 changes: 26 additions & 2 deletions frontend/console/src/features/infrastructure/RoutesList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
export const RoutesList = () => {
return <>Routes Content</>
import { AttributeBadge } from '../../components'
import { List } from '../../components/List'
import type { StatusResponse_Route } from '../../protos/xyz/block/ftl/v1/ftl_pb'

export const RoutesList = ({ routes }: { routes: StatusResponse_Route[] }) => {
return (
<List
items={routes}
renderItem={(route) => (
<div className='flex w-full'>
<div className='flex gap-x-4 items-center w-1/2'>
<div className='whitespace-nowrap'>
<div className='flex gap-x-2 items-center'>{route.module}</div>
<p className='mt-1 flex text-xs leading-5 text-gray-500 dark:text-gray-400 font-roboto-mono'>{route.endpoint}</p>
</div>
</div>
<div className='flex gap-x-4 items-center w-1/2 justify-end'>
<div className='flex flex-wrap gap-1 justify-end'>
<AttributeBadge name='deployment' value={route.deployment} />
<AttributeBadge name='runner' value={route.runner} />
</div>
</div>
</div>
)}
/>
)
}
50 changes: 48 additions & 2 deletions frontend/console/src/features/infrastructure/RunnersList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
export const RunnersList = () => {
return <>Runners Content</>
import { AttributeBadge } from '../../components'
import { List } from '../../components/List'
import { StatusIndicator } from '../../components/StatusIndicator'
import { RunnerState, type StatusResponse_Runner } from '../../protos/xyz/block/ftl/v1/ftl_pb'
import { classNames } from '../../utils'
import { deploymentTextColor } from '../deployments/deployment.utils'
import { renderValue } from './infrastructure.utils'

export const RunnersList = ({ runners }: { runners: StatusResponse_Runner[] }) => {
return (
<List
items={runners}
renderItem={(runner) => (
<>
<div className='flex gap-x-4 items-center'>
<div className='whitespace-nowrap'>
<p className='text-sm font-semibold leading-6'>{runner.key}</p>
<p className='mt-1 flex text-xs leading-5 text-gray-500 dark:text-gray-400 font-roboto-mono'>{runner.endpoint}</p>
<div className='mt-1 flex gap-x-2 items-center'>
{status(runner.state)}
{runner.deployment && <p className={classNames(deploymentTextColor(runner.deployment), 'text-xs')}>{runner.deployment}</p>}
</div>
</div>
</div>
<div className='flex gap-x-4 items-center w-1/2'>
<div className='flex flex-wrap gap-1 justify-end'>
{Object.entries(runner.labels?.fields || {}).map(([key, value]) => (
<AttributeBadge key={key} name={key} value={renderValue(value)} />
))}
</div>
</div>
</>
)}
/>
)
}

const status = (state: RunnerState) => {
switch (state) {
case RunnerState.RUNNER_ASSIGNED:
return <StatusIndicator state='success' text='Assigned' />
case RunnerState.RUNNER_RESERVED:
return <StatusIndicator state='success' text='Reserved' />
case RunnerState.RUNNER_DEAD:
return <StatusIndicator state='error' text='Dead' />
case RunnerState.RUNNER_IDLE:
return <StatusIndicator state='idle' text='Idle' />
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Value } from '@bufbuild/protobuf'

export const renderValue = (value: Value): string => {
switch (value.kind?.case) {
case 'numberValue':
return value.kind.value.toString()
case 'stringValue':
return value.kind.value
case 'boolValue':
return value.kind.value ? 'true' : 'false'
case 'structValue':
return value.kind.value.toJsonString()
case 'listValue':
return value.kind.value.values.map(renderValue).join(', ')
default:
return ''
}
}
3 changes: 2 additions & 1 deletion frontend/console/src/providers/routing-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const router = createBrowserRouter(
<Route path='deployments/:deploymentKey' element={<DeploymentPage />} />
<Route path='deployments/:deploymentKey/verbs/:verbName' element={<VerbPage />} />
<Route path='graph' element={<ConsolePage />} />
<Route path='infrastructure' element={<InfrastructurePage />} />
<Route path='infrastructure' element={<Navigate to='infrastructure/controllers' replace />} />
<Route path='infrastructure/*' element={<InfrastructurePage />} />
</Route>

<Route path='*' element={<NotFoundPage />} />
Expand Down
Loading