Skip to content

Commit

Permalink
feat: replace all useModules and useStream callers in ModulesPage wit…
Browse files Browse the repository at this point in the history
…h useStreamModules (#3057)

This leaves the legacy stuff alone and just refactors the newer pieces,
with a long list of minor bug fixes built in.



https://github.com/user-attachments/assets/07280879-ed11-40ec-ab0d-89a3edd7471b

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
deniseli and github-actions[bot] authored Oct 10, 2024
1 parent 008ee44 commit 54bb6e2
Show file tree
Hide file tree
Showing 25 changed files with 314 additions and 344 deletions.
23 changes: 22 additions & 1 deletion backend/controller/console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,11 +352,32 @@ func (c *ConsoleService) StreamModules(ctx context.Context, req *connect.Request
}
}

// filterDeployments removes any duplicate modules by selecting the deployment with the
// latest CreatedAt.
func (c *ConsoleService) filterDeployments(unfilteredDeployments []dalmodel.Deployment) []dalmodel.Deployment {
latest := make(map[string]dalmodel.Deployment)

for _, deployment := range unfilteredDeployments {
if existing, found := latest[deployment.Module]; !found || deployment.CreatedAt.After(existing.CreatedAt) {
latest[deployment.Module] = deployment

}
}

var result []dalmodel.Deployment
for _, value := range latest {
result = append(result, value)
}

return result
}

func (c *ConsoleService) sendStreamModulesResp(ctx context.Context, stream *connect.ServerStream[pbconsole.StreamModulesResponse]) error {
deployments, err := c.dal.GetDeploymentsWithMinReplicas(ctx)
unfilteredDeployments, err := c.dal.GetDeploymentsWithMinReplicas(ctx)
if err != nil {
return fmt.Errorf("failed to get deployments: %w", err)
}
deployments := c.filterDeployments(unfilteredDeployments)
sch := &schema.Schema{
Modules: slices.Map(deployments, func(d dalmodel.Deployment) *schema.Module {
return d.Schema
Expand Down
6 changes: 3 additions & 3 deletions frontend/console/src/api/modules/use-stream-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export const useStreamModules = () => {
hasModules = true
const newModuleNames = response.modules.map((m) => m.name)
queryClient.setQueryData<Module[]>(queryKey, (prev = []) => {
return [...response.modules, ...prev.filter((m) => !newModuleNames.includes(m.name))]
return [...response.modules, ...prev.filter((m) => !newModuleNames.includes(m.name))].sort((a, b) => a.name.localeCompare(b.name))
})
}
}
return hasModules ? queryClient.getQueryData(queryKey) : []
return hasModules ? (queryClient.getQueryData(queryKey) as Module[]) : []
} catch (error) {
if (error instanceof ConnectError) {
if (error.code !== Code.Canceled) {
Expand All @@ -41,7 +41,7 @@ export const useStreamModules = () => {
}
}

return useQuery({
return useQuery<Module[]>({
queryKey: queryKey,
queryFn: async ({ signal }) => streamModules({ signal }),
enabled: isVisible,
Expand Down
17 changes: 11 additions & 6 deletions frontend/console/src/features/modules/ModulePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { useMemo } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { useParams } from 'react-router-dom'
import { useModules } from '../../api/modules/use-modules'
import { useStreamModules } from '../../api/modules/use-stream-modules'
import { Schema } from './schema/Schema'

export const ModulePanel = () => {
const { moduleName } = useParams()
const modules = useModules()
const modules = useStreamModules()
const ref = useRef<HTMLDivElement>(null)

const module = useMemo(() => {
if (!modules.isSuccess || modules.data.modules.length === 0) {
if (!modules?.data) {
return
}
return modules.data.modules.find((module) => module.name === moduleName)
return modules.data.find((module) => module.name === moduleName)
}, [modules?.data, moduleName])

useEffect(() => {
ref?.current?.parentElement?.scrollTo({ top: 0, behavior: 'smooth' })
}, [moduleName])

if (!module) return

return (
<div className='mt-4 mx-4'>
<div ref={ref} className='mt-4 mx-4'>
<Schema schema={module.schema} />
</div>
)
Expand Down
14 changes: 6 additions & 8 deletions frontend/console/src/features/modules/ModulesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import type React from 'react'
import { useMemo, useState } from 'react'
import { useSchema } from '../../api/schema/use-schema'
import { useStreamModules } from '../../api/modules/use-stream-modules'
import { ResizableHorizontalPanels } from '../../components/ResizableHorizontalPanels'
import { ModulesTree } from './ModulesTree'
import { moduleTreeFromSchema } from './module.utils'

const treeWidthStorageKey = 'tree_w'
import { getTreeWidthFromLS, moduleTreeFromStream, setTreeWidthInLS } from './module.utils'

export const ModulesPage = ({ body }: { body: React.ReactNode }) => {
const schema = useSchema()
const tree = useMemo(() => moduleTreeFromSchema(schema?.data || []), [schema?.data])
const [treeWidth, setTreeWidth] = useState(Number(localStorage.getItem(treeWidthStorageKey)) || 300)
const modules = useStreamModules()
const tree = useMemo(() => moduleTreeFromStream(modules?.data || []), [modules?.data])
const [treeWidth, setTreeWidth] = useState(getTreeWidthFromLS())

function setTreeWidthWithLS(newWidth: number) {
localStorage.setItem(treeWidthStorageKey, `${newWidth}`)
setTreeWidthInLS(newWidth)
setTreeWidth(newWidth)
}

Expand Down
27 changes: 10 additions & 17 deletions frontend/console/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { Multiselect, sortMultiselectOpts } from '../../components/Multiselect'
import type { MultiselectOpt } from '../../components/Multiselect'
import type { Decl } from '../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { classNames } from '../../utils'
import type { ModuleTreeItem } from './module.utils'
import {
type DeclInfo,
addModuleToLocalStorageIfMissing,
collapseAllModulesInLocalStorage,
declIcons,
declUrl,
declSumTypeIsExported,
declUrlFromInfo,
listExpandedModulesFromLocalStorage,
toggleModuleExpansionInLocalStorage,
} from './module.utils'
Expand All @@ -22,12 +23,7 @@ const ExportedIcon = () => (
</span>
)

type WithExport = { export?: boolean }

const DeclNode = ({ decl, href, isSelected }: { decl: Decl; href: string; isSelected: boolean }) => {
if (!decl.value || !decl.value.case || !decl.value.value) {
return []
}
const DeclNode = ({ decl, href, isSelected }: { decl: DeclInfo; href: string; isSelected: boolean }) => {
const navigate = useNavigate()
const declRef = useRef<HTMLDivElement>(null)

Expand All @@ -42,12 +38,12 @@ const DeclNode = ({ decl, href, isSelected }: { decl: Decl; href: string; isSele
}
}, [isSelected])

const Icon = useMemo(() => declIcons[decl.value.case || ''] || CodeIcon, [decl.value.case])
const Icon = useMemo(() => declIcons[decl.declType || ''] || CodeIcon, [decl.declType])
return (
<li className='my-1'>
<div
ref={declRef}
id={`decl-${decl.value.value.name}`}
id={`decl-${decl.value.name}`}
className={classNames(
isSelected ? 'bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-gray-600' : 'hover:bg-gray-200 hover:dark:bg-gray-700',
'group flex items-center gap-x-2 pl-4 pr-2 text-sm font-light leading-6 w-full cursor-pointer scroll-mt-10',
Expand All @@ -58,8 +54,8 @@ const DeclNode = ({ decl, href, isSelected }: { decl: Decl; href: string; isSele
}}
>
<Icon aria-hidden='true' className='size-4 shrink-0 ml-3' />
{decl.value.value.name}
{(decl.value.value as WithExport).export === true ? <ExportedIcon /> : []}
{decl.value.name}
{declSumTypeIsExported(decl.value) ? <ExportedIcon /> : []}
</div>
</li>
)
Expand Down Expand Up @@ -87,10 +83,7 @@ const ModuleSection = ({
}
}, [moduleName]) // moduleName is the selected module; module.name is the one being rendered

const filteredDecls = useMemo(
() => module.decls.filter((d) => d.value?.case && !!selectedDeclTypes.find((o) => o.key === d.value.case)),
[module.decls, selectedDeclTypes],
)
const filteredDecls = useMemo(() => module.decls.filter((d) => !!selectedDeclTypes.find((o) => o.key === d.declType)), [module.decls, selectedDeclTypes])

return (
<li key={module.name} id={`module-tree-module-${module.name}`} className='mb-2'>
Expand Down Expand Up @@ -121,7 +114,7 @@ const ModuleSection = ({
{isExpanded && (
<ul>
{filteredDecls.map((d, i) => (
<DeclNode key={i} decl={d} href={declUrl(module.name, d)} isSelected={isSelected && declName === d.value.value?.name} />
<DeclNode key={i} decl={d} href={declUrlFromInfo(module.name, d)} isSelected={isSelected && declName === d.value.name} />
))}
</ul>
)}
Expand Down
11 changes: 7 additions & 4 deletions frontend/console/src/features/modules/decls/ConfigPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { Config } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { Schema } from '../schema/Schema'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'

export const ConfigPanel = ({ value, moduleName, declName }: { value: Config; moduleName: string; declName: string }) => {
export const ConfigPanel = ({ value, schema, moduleName, declName }: { value: Config; schema: string; moduleName: string; declName: string }) => {
if (!value || !schema) {
return
}
return (
<div className='py-2 px-4'>
<PanelHeader exported={false} comments={value.comments}>
Config: {moduleName}.{declName}
</PanelHeader>
<div className='text-sm my-4'>
Type: <TypeEl t={value.type} />
<div className='-mx-3.5'>
<Schema schema={schema} />
</div>
</div>
)
Expand Down
11 changes: 8 additions & 3 deletions frontend/console/src/features/modules/decls/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { Data } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { DataSnippet } from './DataSnippet'
import { Schema } from '../schema/Schema'
import { PanelHeader } from './PanelHeader'

export const DataPanel = ({ value, moduleName, declName }: { value: Data; moduleName: string; declName: string }) => {
export const DataPanel = ({ value, schema, moduleName, declName }: { value: Data; schema: string; moduleName: string; declName: string }) => {
if (!value || !schema) {
return
}
const maybeTypeParams = value.typeParameters.length === 0 ? '' : `<${value.typeParameters.map((p) => p.name).join(', ')}>`
return (
<div className='py-2 px-4'>
<PanelHeader exported={value.export} comments={value.comments}>
data: {moduleName}.{declName}
{maybeTypeParams}
</PanelHeader>
<DataSnippet value={value} />
<div className='-mx-3.5'>
<Schema schema={schema} />
</div>
</div>
)
}
22 changes: 0 additions & 22 deletions frontend/console/src/features/modules/decls/DataSnippet.tsx

This file was deleted.

7 changes: 5 additions & 2 deletions frontend/console/src/features/modules/decls/DatabasePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { Database } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { Schema } from '../schema/Schema'
import { PanelHeader } from './PanelHeader'

export const DatabasePanel = ({ value, moduleName, declName }: { value: Database; moduleName: string; declName: string }) => {
export const DatabasePanel = ({ value, schema, moduleName, declName }: { value: Database; schema: string; moduleName: string; declName: string }) => {
return (
<div className='py-2 px-4'>
<PanelHeader exported={false} comments={value.comments}>
Database: {moduleName}.{declName}
</PanelHeader>
<div className='text-sm my-4'>Type: {value.type}</div>
<div className='-mx-3.5'>
<Schema schema={schema} />
</div>
</div>
)
}
59 changes: 45 additions & 14 deletions frontend/console/src/features/modules/decls/DeclLink.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import { useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useModules } from '../../../api/modules/use-modules'
import { useStreamModules } from '../../../api/modules/use-stream-modules'
import { classNames } from '../../../utils'
import { getTreeWidthFromLS } from '../module.utils'
import { Schema } from '../schema/Schema'
import { type DeclSchema, declFromModules } from '../schema/schema.utils'
import { type DeclSchema, declSchemaFromModules } from '../schema/schema.utils'

const topbarHeight = 64

const SnippetContainer = ({
decl,
moduleName,
visible,
linkRect,
containerRect,
}: { decl: DeclSchema; moduleName: string; visible: boolean; linkRect?: DOMRect; containerRect?: DOMRect }) => {
const [containerX, setContainerX] = useState(containerRect?.x || getTreeWidthFromLS())
useEffect(() => {
if (containerRect !== undefined) {
setContainerX(containerRect.x)
}
}, [containerRect])

const SnippetContainer = ({ decl, linkRect, containerRect }: { decl: DeclSchema; linkRect?: DOMRect; containerRect?: DOMRect }) => {
const ref = useRef<HTMLDivElement>(null)
const snipRect = ref?.current?.getBoundingClientRect()

const hasRects = !!snipRect && !!linkRect
const toTop = hasRects && window.innerHeight - linkRect.top - linkRect.height < snipRect.height + linkRect.height
const fitsAbove = hasRects && linkRect.top - topbarHeight > snipRect.height
const fitsToRight = hasRects && window.innerWidth - linkRect.left >= snipRect.width
const fitsToLeft = hasRects && !!containerRect && linkRect.left - containerRect.x + linkRect.width >= snipRect.width
const fitsToLeft = hasRects && linkRect.left - containerX + linkRect.width >= snipRect.width
const horizontalAlignmentClassNames = fitsToRight ? '-ml-1' : fitsToLeft ? '-translate-x-full left-full ml-0' : ''
const style = {
transform: !fitsToRight && !fitsToLeft ? `translateX(-${(linkRect?.left || 0) - (containerRect?.left || 0)}px)` : undefined,
transform: !fitsToRight && !fitsToLeft ? `translateX(-${(linkRect?.left || 0) - containerX}px)` : undefined,
}
return (
<div
ref={ref}
style={style}
className={classNames(
toTop ? 'bottom-full' : '',
fitsAbove ? 'bottom-full' : '',
visible ? '' : 'invisible',
horizontalAlignmentClassNames,
'absolute p-4 rounded-md border-solid border border border-gray-400 bg-gray-200 dark:border-gray-800 dark:bg-gray-700 text-gray-700 dark:text-white text-xs font-normal z-10 drop-shadow-xl cursor-default',
'absolute p-4 pl-0.5 rounded-md border-solid border border border-gray-400 bg-gray-200 dark:border-gray-800 dark:bg-gray-700 text-gray-700 dark:text-white text-xs font-normal z-10 drop-shadow-xl cursor-default',
)}
>
<Schema schema={decl.schema} containerRect={containerRect} />
<Schema schema={decl.schema} moduleName={moduleName} containerRect={containerRect} />
</div>
)
}
Expand All @@ -40,18 +57,24 @@ export const DeclLink = ({
textColors = 'text-indigo-600 dark:text-indigo-400',
containerRect,
}: { moduleName?: string; declName: string; slim?: boolean; textColors?: string; containerRect?: DOMRect }) => {
if (!moduleName || !declName) {
return
}
const navigate = useNavigate()
const modules = useModules()
const decl = useMemo(() => (moduleName ? declFromModules(moduleName, declName, modules) : undefined), [moduleName, declName, modules?.data])
const modules = useStreamModules()
const decl = useMemo(
() => (moduleName && !!modules?.data ? declSchemaFromModules(moduleName, declName, modules?.data) : undefined),
[moduleName, declName, modules?.data],
)
const [isHovering, setIsHovering] = useState(false)
const linkRef = useRef<HTMLSpanElement>(null)

const str = moduleName && slim !== true ? `${moduleName}.${declName}` : declName

if (!decl) {
return str
}

const linkRef = useRef<HTMLSpanElement>(null)
return (
<span
className='inline-block rounded-md cursor-pointer hover:bg-gray-400/30 hover:dark:bg-gray-900/30 p-1 -m-1 relative'
Expand All @@ -61,7 +84,15 @@ export const DeclLink = ({
<span ref={linkRef} className={textColors} onClick={() => navigate(`/modules/${moduleName}/${decl.declType}/${declName}`)}>
{str}
</span>
{!slim && isHovering && <SnippetContainer decl={decl} linkRect={linkRef?.current?.getBoundingClientRect()} containerRect={containerRect} />}
{!slim && (
<SnippetContainer
decl={decl}
moduleName={moduleName}
visible={isHovering}
linkRect={linkRef?.current?.getBoundingClientRect()}
containerRect={containerRect}
/>
)}
</span>
)
}
Loading

0 comments on commit 54bb6e2

Please sign in to comment.