Skip to content

Commit

Permalink
tree
Browse files Browse the repository at this point in the history
lint fix and nav fixes

rm border, darker bg, tree_w to localStorage

flicker

cleanup
  • Loading branch information
deniseli committed Aug 28, 2024
1 parent 7cd61b6 commit 3957fe7
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 104 deletions.
56 changes: 56 additions & 0 deletions frontend/src/components/ResizableHorizontalPanels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type React from 'react'
import { useRef, useState } from 'react'

interface ResizableHorizontalPanelsProps {
leftPanelContent: React.ReactNode
rightPanelContent: React.ReactNode
minLeftPanelWidth?: number
minRightPanelWidth?: number
leftPanelWidth: number
setLeftPanelWidth: (n: number) => void
}

export const ResizableHorizontalPanels: React.FC<ResizableHorizontalPanelsProps> = ({
leftPanelContent,
rightPanelContent,
minLeftPanelWidth = 100,
minRightPanelWidth = 100,
leftPanelWidth,
setLeftPanelWidth,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [isDragging, setIsDragging] = useState(false)

const startDragging = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault()
setIsDragging(true)
}

const stopDragging = () => setIsDragging(false)

const onDrag = (e: React.MouseEvent<HTMLDivElement>) => {
if (!isDragging || !containerRef.current) {
return
}
const containerDims = containerRef.current.getBoundingClientRect()
const newWidth = e.clientX - containerDims.x
const maxWidth = containerDims.width - minRightPanelWidth
if (newWidth >= minLeftPanelWidth && newWidth <= maxWidth) {
setLeftPanelWidth(newWidth)
}
}

return (
<div ref={containerRef} className='flex flex-row h-full w-full' onMouseMove={onDrag} onMouseUp={stopDragging} onMouseLeave={stopDragging}>
<div style={{ width: `${leftPanelWidth}px` }} className='overflow-auto'>
{leftPanelContent}
</div>
<div
className='cursor-col-resize bg-gray-100 dark:bg-gray-900 hover:bg-indigo-600'
onMouseDown={startDragging}
style={{ width: '3px', cursor: 'col-resize' }}
/>
<div className='flex-1 overflow-auto'>{rightPanelContent}</div>
</div>
)
}
12 changes: 12 additions & 0 deletions frontend/src/features/modules/ModulePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useParams } from 'react-router-dom'

export const ModulePanel = () => {
const { moduleName } = useParams()

const body = (
<div className='flex-1 py-2 px-4'>
<p>Module: {moduleName}</p>
</div>
)
return body
}
35 changes: 24 additions & 11 deletions frontend/src/features/modules/ModulesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import { useMemo } from 'react'
import type React from 'react'
import { useMemo, useState } from 'react'
import { useSchema } from '../../api/schema/use-schema'
import { ResizableHorizontalPanels } from '../../components/ResizableHorizontalPanels'
import { ModulesTree } from './ModulesTree'
import { moduleTreeFromSchema } from './module.utils'

export const ModulesPage = () => {
export const ModulesPanel = () => {
return (
<div className='flex-1 py-2 px-4'>
<p>Content</p>
</div>
)
}

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('tree_w')) || 300)

return (
<div className='flex h-full'>
<div className='w-64 h-full'>
<ModulesTree modules={tree} />
</div>
function setTreeWidthWithLS(newWidth: number) {
localStorage.setItem('tree_w', `${newWidth}`)
setTreeWidth(newWidth)
}

<div className='flex-1 py-2 px-4'>
<p>Content</p>
</div>
</div>
return (
<ResizableHorizontalPanels
leftPanelContent={<ModulesTree modules={tree} />}
rightPanelContent={body}
leftPanelWidth={treeWidth}
setLeftPanelWidth={setTreeWidthWithLS}
/>
)
}
180 changes: 132 additions & 48 deletions frontend/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,144 @@
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { ChevronRightIcon } from '@heroicons/react/24/outline'
import { classNames } from '../../utils'
import {
ArrowRightCircleIcon,
BellAlertIcon,
BellIcon,
BoltIcon,
BookOpenIcon,
ChevronRightIcon,
CircleStackIcon,
CodeBracketSquareIcon,
Cog6ToothIcon,
DocumentDuplicateIcon,
LockClosedIcon,
NumberedListIcon,
SquaresPlusIcon,
} from '@heroicons/react/24/outline'
import { TableCellsIcon } from '@heroicons/react/24/solid'
import type { ForwardRefExoticComponent, SVGProps } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import type { Decl } from '../../protos/xyz/block/ftl/v1/schema/schema_pb'
import type { ModuleTreeItem } from './module.utils'

// This could alternatively be an icon, but we'd need to pick a good one.
const ExportBadge = () => <span className='text-xs py-0.5 px-1.5 bg-gray-200 dark:bg-gray-800 dark:text-gray-300 rounded-md'>Exported</span>

type IconMap = Record<string, ForwardRefExoticComponent<SVGProps<SVGSVGElement> & { title?: string; titleId?: string }>>
const icons: IconMap = {
config: Cog6ToothIcon,
data: TableCellsIcon,
database: CircleStackIcon,
enum: NumberedListIcon,
fsm: SquaresPlusIcon,
topic: BellIcon,
typeAlias: DocumentDuplicateIcon,
secret: LockClosedIcon,
subscription: BellAlertIcon,
verb: BoltIcon,
}

type WithExport = { export?: boolean }

const DeclNode = ({ decl, href }: { decl: Decl; href: string }) => {
const navigate = useNavigate()
if (!decl.value || !decl.value.case || !decl.value.value) {
return []
}
const Icon = icons[decl.value.case] || CodeBracketSquareIcon
return (
<li className='my-1'>
<DisclosureButton
className={'hover:bg-gray-100 hover:dark:bg-gray-700 group flex items-center gap-x-2 rounded-md pl-4 pr-2 text-sm font-light leading-6 w-full'}
onClick={(e) => {
e.preventDefault()
navigate(href)
}}
>
<Icon aria-hidden='true' className='size-4 shrink-0' />
{decl.value.value.name}
{(decl.value.value as WithExport).export === true ? <ExportBadge /> : []}
</DisclosureButton>
</li>
)
}

const ModuleSection = ({
module,
isExpanded,
path,
params,
toggleExpansion,
}: { module: ModuleTreeItem; isExpanded: boolean; path: string; params: string; toggleExpansion: (m: string) => void }) => {
const navigate = useNavigate()
return (
<li key={module.name} id={`module-tree-module-${module.name}`} className='my-2'>
<Disclosure as='div' defaultOpen={isExpanded}>
<DisclosureButton
className='hover:bg-gray-100 hover:dark:bg-gray-700 group flex w-full modules-center gap-x-2 space-y-1 rounded-md px-2 text-left text-sm font-medium leading-6'
onClick={() => toggleExpansion(module.name)}
>
<BookOpenIcon aria-hidden='true' className='size-4 my-1 shrink-0 ' />
{module.name}
<ArrowRightCircleIcon
aria-hidden='true'
className='size-4 shrink-0 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600'
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
navigate(`${path}?${params}`)
}}
/>
{module.decls.length === 0 || (
<ChevronRightIcon aria-hidden='true' className='ml-auto h-4 w-4 shrink-0 group-data-[open]:rotate-90 group-data-[open]:text-gray-500' />
)}
</DisclosureButton>
<DisclosurePanel as='ul' className='px-2'>
{module.decls.map((d, i) => (
<DeclNode key={i} decl={d} href={`${path}/${d.value.case}/${d.value.value?.name}?${params}`} />
))}
</DisclosurePanel>
</Disclosure>
</li>
)
}

export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
const [searchParams, setSearchParams] = useSearchParams()
modules.sort((m1, m2) => Number(m1.isBuiltin) - Number(m2.isBuiltin))

const expandedModules = (searchParams.get('tree_m') || '').split(',')

function toggleModuleExpansion(moduleName: string) {
const expanded = (searchParams.get('tree_m') || '').split(',')
const i = expanded.indexOf(moduleName)
if (i === -1) {
searchParams.set('tree_m', [...expanded, moduleName].join(','))
} else {
expanded.splice(i, 1)
if (expanded.length === 1) {
searchParams.delete('tree_m')
} else {
searchParams.set('tree_m', expanded.join(','))
}
}
setSearchParams(searchParams)
}

return (
<div className='flex grow flex-col h-full gap-y-5 overflow-y-auto border-r border-gray-200 dark:border-gray-600 py-2 px-6'>
<div className='flex grow flex-col h-full gap-y-5 overflow-y-auto bg-gray-100 dark:bg-gray-900 px-6'>
<nav className='flex flex-1 flex-col'>
<ul className='flex flex-1 flex-col gap-y-7'>
<li>
<ul className='-mx-2'>
{modules.map((item) => (
<li key={item.name} id={`module-tree-item-${item.name}`}>
{!item.children ? (
<a
href={item.href}
className={classNames(
item.current ? '' : 'hover:bg-gray-50 hover:dark:bg-gray-700',
'group flex gap-x-3 rounded-md px-2 text-sm font-semibold leading-6',
)}
>
<item.icon aria-hidden='true' className='size-3 shrink-0' />
{item.name}
</a>
) : (
<Disclosure as='div' defaultOpen={item.expanded}>
<DisclosureButton
className={classNames(
item.current ? '' : 'hover:bg-gray-50 hover:dark:bg-gray-700',
'group flex w-full items-center gap-x-2 rounded-md px-2 text-left text-sm font-semibold leading-6',
)}
>
<item.icon aria-hidden='true' className='size-4 shrink-0 ' />
{item.name}
<ChevronRightIcon aria-hidden='true' className='ml-auto h-5 w-5 shrink-0 group-data-[open]:rotate-90 group-data-[open]:text-gray-500' />
</DisclosureButton>
<DisclosurePanel as='ul' className='px-2'>
{item.children.map((subItem) => (
<li key={subItem.name}>
<DisclosureButton
as='a'
href={subItem.href}
className={classNames(
subItem.current ? '' : 'hover:bg-gray-50 hover:dark:bg-gray-700',
'group flex items-center gap-x-2 rounded-md pl-4 pr-2 text-sm leading-6',
)}
>
<subItem.icon aria-hidden='true' className='size-4 shrink-0' />
{subItem.name}
</DisclosureButton>
</li>
))}
</DisclosurePanel>
</Disclosure>
)}
</li>
{modules.map((m) => (
<ModuleSection
key={m.name}
module={m}
isExpanded={expandedModules.includes(m.name)}
path={`/modules/${m.name}`}
params={searchParams.toString()}
toggleExpansion={toggleModuleExpansion}
/>
))}
</ul>
</li>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/features/modules/decls/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Data } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { ExportBadge } from './ExportBadge'

export const DataPanel = ({ value, moduleName, declName }: { value: Data; moduleName: string; declName: string }) => {
return (
<div className='flex-1 py-2 px-4'>
{value.export ? <ExportBadge /> : []}
<div className='inline-block mr-3 align-middle'>
<p>
data: {moduleName}.{declName}
</p>
{value.comments.length > 0 ? <p className='text-xs my-1'>{value.comments}</p> : []}
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions frontend/src/features/modules/decls/DeclPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { useSchema } from '../../../api/schema/use-schema'
import { declFromSchema } from '../module.utils'
import { DataPanel } from './DataPanel'
import { VerbPanel } from './VerbPanel'

export const DeclPanel = () => {
const { moduleName, declCase, declName } = useParams()
if (!moduleName || !declName) {
// Should be impossible, but validate anyway for type safety
return []
}

const schema = useSchema()
const decl = useMemo(() => declFromSchema(moduleName, declName, schema?.data || []), [schema?.data, moduleName, declCase, declName])
if (!decl) {
return []
}

const nameProps = { moduleName, declName }
switch (decl.value.case) {
case 'data':
return <DataPanel value={decl.value.value} {...nameProps} />
case 'verb':
return <VerbPanel value={decl.value.value} {...nameProps} />
}
return (
<div className='flex-1 py-2 px-4'>
<p>
{declCase} declaration: {moduleName}.{declName}
</p>
</div>
)
}
5 changes: 5 additions & 0 deletions frontend/src/features/modules/decls/ExportBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const ExportBadge = () => (
<div>
<span className='text-xs py-0.5 px-1.5 bg-gray-200 dark:bg-gray-900 dark:text-gray-300 rounded-md'>Exported</span>
</div>
)
Loading

0 comments on commit 3957fe7

Please sign in to comment.