diff --git a/console/client/index.html b/console/client/index.html
index 2406df2c88..a6e97b543d 100644
--- a/console/client/index.html
+++ b/console/client/index.html
@@ -4,7 +4,7 @@
FTL
-
+
diff --git a/console/client/src/App.tsx b/console/client/src/App.tsx
index 078ad058c9..0b9e60ecf9 100644
--- a/console/client/src/App.tsx
+++ b/console/client/src/App.tsx
@@ -1,4 +1,6 @@
import { Navigate, Route, Routes } from 'react-router-dom'
+import { DeploymentPage } from './features/deployments/DeploymentPage.tsx'
+import { DeploymentsPage } from './features/deployments/DeploymentsPage.tsx'
import { GraphPage } from './features/graph/GraphPage.tsx'
import { ModulesPage } from './features/modules/ModulesPage.tsx'
import { TimelinePage } from './features/timeline/TimelinePage.tsx'
@@ -13,6 +15,8 @@ export const App = () => {
} />
} />
} />
+ } />
+ } />
} />
diff --git a/console/client/src/components/ButtonSmall.tsx b/console/client/src/components/ButtonSmall.tsx
new file mode 100644
index 0000000000..22bd3abdf1
--- /dev/null
+++ b/console/client/src/components/ButtonSmall.tsx
@@ -0,0 +1,17 @@
+interface Props {
+ children: React.ReactNode
+ onClick?: () => void
+ className?: string
+}
+
+export const ButtonSmall = ({ children, onClick }: Props) => {
+ return (
+
+ )
+}
diff --git a/console/client/src/components/Card.tsx b/console/client/src/components/Card.tsx
index e153026120..5b315697d9 100644
--- a/console/client/src/components/Card.tsx
+++ b/console/client/src/components/Card.tsx
@@ -1,6 +1,19 @@
interface Props {
+ topBarColor?: string
+ onClick?: () => void
children: React.ReactNode
}
-export const Card = ({ children }: Props) => {
- return {children}
+export const Card = ({ topBarColor, onClick, children }: Props) => {
+ return (
+
+ {topBarColor && (
+
+ )}
+
+
{children}
+
+ )
}
diff --git a/console/client/src/features/deployments/DeploymentPage.tsx b/console/client/src/features/deployments/DeploymentPage.tsx
new file mode 100644
index 0000000000..15fdc01e05
--- /dev/null
+++ b/console/client/src/features/deployments/DeploymentPage.tsx
@@ -0,0 +1,74 @@
+import { RocketLaunchIcon } from '@heroicons/react/24/outline'
+import React from 'react'
+import { useParams } from 'react-router-dom'
+import { ButtonSmall } from '../../components/ButtonSmall'
+import { Card } from '../../components/Card'
+import { PageHeader } from '../../components/PageHeader'
+import { Module } from '../../protos/xyz/block/ftl/v1/console/console_pb'
+import { MetadataCalls, VerbRef } from '../../protos/xyz/block/ftl/v1/schema/schema_pb'
+import { modulesContext } from '../../providers/modules-provider'
+import { verbRefString } from '../verbs/verb.utils'
+
+export const DeploymentPage = () => {
+ const { deploymentName } = useParams()
+ const modules = React.useContext(modulesContext)
+ const [module, setModule] = React.useState()
+ const [calls, setCalls] = React.useState([])
+
+ React.useEffect(() => {
+ if (modules) {
+ const module = modules.modules.find((module) => module.deploymentName === deploymentName)
+ setModule(module)
+ }
+ }, [modules, deploymentName])
+
+ React.useEffect(() => {
+ if (!module) return
+
+ const verbCalls: VerbRef[] = []
+
+ const metadata = module.verbs
+ .map((v) => v.verb)
+ .map((v) => v?.metadata)
+ .flat()
+
+ const metadataCalls = metadata
+ .filter((metadata) => metadata?.value.case === 'calls')
+ .map((metadata) => metadata?.value.value as MetadataCalls)
+
+ const calls = metadataCalls.map((metadata) => metadata?.calls).flat()
+
+ calls.forEach((call) => {
+ if (!verbCalls.find((v) => v.name === call.name && v.module === call.module)) {
+ verbCalls.push({ name: call.name, module: call.module } as VerbRef)
+ }
+ })
+
+ setCalls(Array.from(verbCalls))
+ }, [modules])
+
+ return (
+ <>
+ } title={`Deployments - ${deploymentName}`} />
+
+
+
+ {module?.verbs.map((verb) => (
+
console.log('click')}>
+ {verb.verb?.name}
+ {verb.verb?.name}
+
+ ))}
+
+
Calls
+
+ {calls?.map((verb) => (
+ -
+ {verbRefString(verb)}
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/console/client/src/features/deployments/DeploymentsPage.tsx b/console/client/src/features/deployments/DeploymentsPage.tsx
new file mode 100644
index 0000000000..cb5b71b16b
--- /dev/null
+++ b/console/client/src/features/deployments/DeploymentsPage.tsx
@@ -0,0 +1,29 @@
+import { RocketLaunchIcon } from '@heroicons/react/24/outline'
+import React from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Card } from '../../components/Card'
+import { PageHeader } from '../../components/PageHeader'
+import { modulesContext } from '../../providers/modules-provider'
+
+export const DeploymentsPage = () => {
+ const modules = React.useContext(modulesContext)
+ const navigate = useNavigate()
+
+ return (
+ <>
+ } title='Deployments' />
+
+ {modules.modules.map((module) => (
+
navigate(`/deployments/${module.deploymentName}`)}
+ >
+ {module.name}
+ {module.deploymentName}
+
+ ))}
+
+ >
+ )
+}
diff --git a/console/client/src/layout/Navigation.tsx b/console/client/src/layout/Navigation.tsx
index 8559db24a5..6ad88b2f63 100644
--- a/console/client/src/layout/Navigation.tsx
+++ b/console/client/src/layout/Navigation.tsx
@@ -1,4 +1,4 @@
-import { CubeTransparentIcon, ListBulletIcon, Square3Stack3DIcon } from '@heroicons/react/24/outline'
+import { CubeTransparentIcon, ListBulletIcon, RocketLaunchIcon, Square3Stack3DIcon } from '@heroicons/react/24/outline'
import { useContext } from 'react'
import { Link, NavLink } from 'react-router-dom'
import { DarkModeSwitch } from '../components/DarkModeSwitch'
@@ -8,6 +8,7 @@ import { classNames } from '../utils'
const navigation = [
{ name: 'Events', href: '/events', icon: ListBulletIcon },
{ name: 'Modules', href: '/modules', icon: Square3Stack3DIcon },
+ { name: 'Deployments', href: '/deployments', icon: RocketLaunchIcon },
{ name: 'Graph', href: '/graph', icon: CubeTransparentIcon },
]
@@ -54,7 +55,7 @@ export const Navigation = () => {
aria-hidden='true'
/>
{item.name}
- {item.href === '/modules' && (
+ {['/modules', '/deployments'].includes(item.href) && (