Skip to content

Commit

Permalink
temp
Browse files Browse the repository at this point in the history
css

styling

more css

filter

cleanup

to listbox

light mode

hover colors

refactor

display names

search params

sorting improvements

cleanup

icon
  • Loading branch information
deniseli committed Sep 27, 2024
1 parent fb5c766 commit ed5bcfc
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 13 deletions.
62 changes: 62 additions & 0 deletions frontend/console/src/components/Multiselect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Listbox } from '@headlessui/react'
import { ArrowDown01Icon, Tick01Icon } from 'hugeicons-react'

export interface MultiselectOpt {
key: string
displayName: string
}

export function sortMultiselectOpts(o: MultiselectOpt[]) {
return o.sort((a: MultiselectOpt, b: MultiselectOpt) => (a.key < b.key ? -1 : 1))
}

export const Multiselect = ({
allOpts,
selectedOpts,
onChange,
}: { allOpts: MultiselectOpt[]; selectedOpts: MultiselectOpt[]; onChange: (types: MultiselectOpt[]) => void }) => {
sortMultiselectOpts(selectedOpts)
return (
<div className='w-full'>
<Listbox multiple value={selectedOpts} onChange={onChange}>
<div className='relative w-[calc(100%-0.75rem)] items-center'>
<Listbox.Button className='w-full m-1.5 py-1 px-2 text-sm border-gray-200 dark:border-white/5 rounded-md text-sm text-gray-500 hover:text-gray-900 hover:dark:text-gray-200 bg-gray-300 dark:bg-gray-800'>
<span className='block truncate w-[calc(100%-30px)] h-5'>{selectedOpts.map((o) => o.displayName).join(', ')}</span>
<span className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-1'>
<ArrowDown01Icon className='w-5' />
</span>
</Listbox.Button>
</div>
<Listbox.Options
anchor='bottom'
transition
className='w-[var(--button-width)] min-w-48 rounded-xl mt-1 border dark:border-white/5 bg-white/90 dark:bg-gray-700/90 transition duration-100 ease-in truncate'
>
{allOpts.map((o) => (
<Listbox.Option
className='cursor-pointer py-1.5 px-2 group flex items-center gap-2 select-none text-sm dark:text-white hover:bg-gray-200 hover:dark:bg-gray-800'
key={o.key}
value={o}
>
<div className='w-4'>
<Tick01Icon className='size-4 invisible group-data-[selected]:visible' />
</div>
{o.displayName}
</Listbox.Option>
))}
<div className='w-full mt-2 text-center text-sm dark:text-white border-t dark:border-white/5 bg-white dark:bg-gray-700'>
<div
className='w-1/2 p-2 inline-block hover:bg-gray-200 hover:dark:bg-white/10 cursor-pointer text-clip border-r dark:border-white/5'
onClick={() => onChange(allOpts)}
>
Select All
</div>
<div className='w-1/2 p-2 inline-block hover:bg-gray-200 hover:dark:bg-white/10 cursor-pointer text-clip' onClick={() => onChange([])}>
Deselect All
</div>
</div>
</Listbox.Options>
</Listbox>
</div>
)
}
79 changes: 69 additions & 10 deletions frontend/console/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { ArrowRight01Icon, CircleArrowRight02Icon, CodeIcon, FileExportIcon, PackageIcon } from 'hugeicons-react'
import { ArrowRight01Icon, ArrowShrinkIcon, CircleArrowRight02Icon, CodeIcon, FileExportIcon, PackageIcon } from 'hugeicons-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
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 { addModuleToLocalStorageIfMissing, declIcons, declUrl, listExpandedModulesFromLocalStorage, toggleModuleExpansionInLocalStorage } from './module.utils'
import {
addModuleToLocalStorageIfMissing,
collapseAllModulesInLocalStorage,
declIcons,
declUrl,
listExpandedModulesFromLocalStorage,
toggleModuleExpansionInLocalStorage,
} from './module.utils'
import { declTypeMultiselectOpts } from './schema/schema.utils'

const ExportedIcon = () => (
<span className='w-4' title='Exported'>
Expand Down Expand Up @@ -55,7 +65,12 @@ const DeclNode = ({ decl, href, isSelected }: { decl: Decl; href: string; isSele
)
}

const ModuleSection = ({ module, isExpanded, toggleExpansion }: { module: ModuleTreeItem; isExpanded: boolean; toggleExpansion: (m: string) => void }) => {
const ModuleSection = ({
module,
isExpanded,
toggleExpansion,
selectedDeclTypes,
}: { module: ModuleTreeItem; isExpanded: boolean; toggleExpansion: (m: string) => void; selectedDeclTypes: MultiselectOpt[] }) => {
const { moduleName, declName } = useParams()
const navigate = useNavigate()
const isSelected = useMemo(() => moduleName === module.name, [moduleName, module.name])
Expand All @@ -72,6 +87,11 @@ const ModuleSection = ({ module, isExpanded, toggleExpansion }: { module: Module
}
}, [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],
)

return (
<li key={module.name} id={`module-tree-module-${module.name}`} className='my-2'>
<div
Expand All @@ -92,13 +112,13 @@ const ModuleSection = ({ module, isExpanded, toggleExpansion }: { module: Module
navigate(`/modules/${module.name}`)
}}
/>
{module.decls.length === 0 || (
<ArrowRight01Icon aria-hidden='true' className='ml-auto mr-2 h-4 w-4 shrink-0 group-data-[open]:rotate-90 group-data-[open]:text-gray-500' />
{filteredDecls.length === 0 || (
<ArrowRight01Icon aria-hidden='true' className={`ml-auto mr-2 h-4 w-4 shrink-0 ${isExpanded ? 'rotate-90 text-gray-500' : ''}`} />
)}
</div>
{isExpanded && (
<ul>
{module.decls.map((d, i) => (
{filteredDecls.map((d, i) => (
<DeclNode key={i} decl={d} href={declUrl(module.name, d)} isSelected={isSelected && declName === d.value.value?.name} />
))}
</ul>
Expand All @@ -107,29 +127,68 @@ const ModuleSection = ({ module, isExpanded, toggleExpansion }: { module: Module
)
}

const declTypesSearchParamKey = 'dt'

export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
const { moduleName, declName } = useParams()

const [searchParams, setSearchParams] = useSearchParams()
const declTypeKeysFromUrl = searchParams.getAll(declTypesSearchParamKey)
const declTypesFromUrl = declTypeMultiselectOpts.filter((o) => declTypeKeysFromUrl.includes(o.key))
const [selectedDeclTypes, setSelectedDeclTypes] = useState(declTypesFromUrl.length === 0 ? declTypeMultiselectOpts : declTypesFromUrl)

const [expandedModules, setExpandedModules] = useState(listExpandedModulesFromLocalStorage())
useEffect(() => {
if (declName) {
addModuleToLocalStorageIfMissing(moduleName)
}
}, [moduleName, declName])

function toggleFn(moduleName: string) {
function msOnChange(opts: MultiselectOpt[]) {
const params = new URLSearchParams()
if (opts.length !== declTypeMultiselectOpts.length) {
for (const o of sortMultiselectOpts(opts)) {
params.append(declTypesSearchParamKey, o.key)
}
}
setSearchParams(params)
setSelectedDeclTypes(opts)
}

function toggle(moduleName: string) {
toggleModuleExpansionInLocalStorage(moduleName)
setExpandedModules(listExpandedModulesFromLocalStorage())
return
}

function collapseAll() {
collapseAllModulesInLocalStorage()
setExpandedModules([])
}

modules.sort((m1, m2) => Number(m1.isBuiltin) - Number(m2.isBuiltin))
return (
<div className={'flex grow flex-col h-full gap-y-5 overflow-y-auto bg-gray-100 dark:bg-gray-900'}>
<nav>
<div className='sticky top-0 border-b border-gray-300 bg-gray-100 dark:border-gray-800 dark:bg-gray-900'>
<span className='block w-[calc(100%-30px)]'>
<Multiselect allOpts={declTypeMultiselectOpts} selectedOpts={selectedDeclTypes} onChange={msOnChange} />
</span>
<span
className='absolute inset-y-0 right-0 flex items-center px-1 mx-1 my-1.5 rounded-md cursor-pointer bg-gray-300 hover:bg-gray-400 dark:bg-gray-800 hover:dark:bg-gray-700'
onClick={collapseAll}
>
<ArrowShrinkIcon className='w-5 dark:text-gray-300' />
</span>
</div>
<ul>
{modules.map((m) => (
<ModuleSection key={m.name} module={m} isExpanded={expandedModules.includes(m.name)} toggleExpansion={toggleFn} />
<ModuleSection
key={m.name}
module={m}
isExpanded={expandedModules.includes(m.name)}
toggleExpansion={toggle}
selectedDeclTypes={selectedDeclTypes}
/>
))}
</ul>
</nav>
Expand Down
4 changes: 3 additions & 1 deletion frontend/console/src/features/modules/module.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const declFromSchema = (moduleName: string, declName: string, schema: Pul
return module.schema.decls.find((d) => d.value.value?.name === declName)
}

export const listExpandedModulesFromLocalStorage = () => (localStorage.getItem('tree_m') || '').split(',')
export const listExpandedModulesFromLocalStorage = () => (localStorage.getItem('tree_m') || '').split(',').filter((s) => s !== '')

export const toggleModuleExpansionInLocalStorage = (moduleName: string) => {
const expanded = listExpandedModulesFromLocalStorage()
Expand All @@ -116,6 +116,8 @@ export const addModuleToLocalStorageIfMissing = (moduleName?: string) => {
}
}

export const collapseAllModulesInLocalStorage = () => localStorage.setItem('tree_m', '')

type IconMap = Record<string, React.FC<Omit<HugeiconsProps, 'ref'> & React.RefAttributes<SVGSVGElement>>>
export const declIcons: IconMap = {
config: Settings02Icon,
Expand Down
43 changes: 43 additions & 0 deletions frontend/console/src/features/modules/schema/schema.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,49 @@ export const staticKeywords = ['module', 'export']

export const declTypes = ['config', 'data', 'database', 'enum', 'fsm', 'topic', 'typealias', 'secret', 'subscription', 'verb']

export const declTypeMultiselectOpts = [
{
key: 'config',
displayName: 'Config',
},
{
key: 'data',
displayName: 'Data',
},
{
key: 'database',
displayName: 'Database',
},
{
key: 'enum',
displayName: 'Enum',
},
{
key: 'fsm',
displayName: 'FSM',
},
{
key: 'topic',
displayName: 'Topic',
},
{
key: 'typealias',
displayName: 'Type Alias',
},
{
key: 'secret',
displayName: 'FSM',
},
{
key: 'subscription',
displayName: 'Subscription',
},
{
key: 'verb',
displayName: 'Verb',
},
]

// Keep these in sync with backend/schema/module.go#L86-L95
const skipNewLineDeclTypes = ['config', 'secret', 'database', 'topic', 'subscription']
const skipGapAfterTypes: { [key: string]: string[] } = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Timestamp } from '@bufbuild/protobuf'
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { Backward02Icon, Forward02Icon, PauseIcon, PlayIcon, Tick02Icon, UnfoldLessIcon } from 'hugeicons-react'
import { ArrowDown01Icon, Backward02Icon, Forward02Icon, PauseIcon, PlayIcon, Tick02Icon } from 'hugeicons-react'
import { useEffect, useState } from 'react'
import { bgColor, borderColor, classNames, formatTimestampShort, formatTimestampTime, panelColor, textColor } from '../../../utils'

Expand Down Expand Up @@ -117,7 +117,7 @@ export const TimelineTimeControls = ({
>
<span className='block truncate'>{selected.label}</span>
<span className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-1'>
<UnfoldLessIcon className='h-5 w-5 text-gray-400' aria-hidden='true' />
<ArrowDown01Icon className='h-5 w-5 text-gray-400' aria-hidden='true' />
</span>
</ListboxButton>

Expand Down

0 comments on commit ed5bcfc

Please sign in to comment.