From ed5bcfcd6bda2e28e849503fbd559760d49ae5eb Mon Sep 17 00:00:00 2001 From: Denise Li Date: Thu, 26 Sep 2024 15:22:51 -0400 Subject: [PATCH] temp css styling more css filter cleanup to listbox light mode hover colors refactor display names search params sorting improvements cleanup icon --- .../console/src/components/Multiselect.tsx | 62 +++++++++++++++ .../src/features/modules/ModulesTree.tsx | 79 ++++++++++++++++--- .../src/features/modules/module.utils.ts | 4 +- .../features/modules/schema/schema.utils.ts | 43 ++++++++++ .../timeline/filters/TimelineTimeControls.tsx | 4 +- 5 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 frontend/console/src/components/Multiselect.tsx diff --git a/frontend/console/src/components/Multiselect.tsx b/frontend/console/src/components/Multiselect.tsx new file mode 100644 index 0000000000..7b823fbdc7 --- /dev/null +++ b/frontend/console/src/components/Multiselect.tsx @@ -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 ( +
+ +
+ + {selectedOpts.map((o) => o.displayName).join(', ')} + + + + +
+ + {allOpts.map((o) => ( + +
+ +
+ {o.displayName} +
+ ))} +
+
onChange(allOpts)} + > + Select All +
+
onChange([])}> + Deselect All +
+
+
+
+
+ ) +} diff --git a/frontend/console/src/features/modules/ModulesTree.tsx b/frontend/console/src/features/modules/ModulesTree.tsx index 020712eee2..8dff7642bd 100644 --- a/frontend/console/src/features/modules/ModulesTree.tsx +++ b/frontend/console/src/features/modules/ModulesTree.tsx @@ -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 = () => ( @@ -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]) @@ -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 (
  • - {module.decls.length === 0 || ( -
    {isExpanded && (
      - {module.decls.map((d, i) => ( + {filteredDecls.map((d, i) => ( ))}
    @@ -107,9 +127,16 @@ 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) { @@ -117,19 +144,51 @@ export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => { } }, [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 (
    diff --git a/frontend/console/src/features/modules/module.utils.ts b/frontend/console/src/features/modules/module.utils.ts index 57088cf833..68a3e56361 100644 --- a/frontend/console/src/features/modules/module.utils.ts +++ b/frontend/console/src/features/modules/module.utils.ts @@ -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() @@ -116,6 +116,8 @@ export const addModuleToLocalStorageIfMissing = (moduleName?: string) => { } } +export const collapseAllModulesInLocalStorage = () => localStorage.setItem('tree_m', '') + type IconMap = Record & React.RefAttributes>> export const declIcons: IconMap = { config: Settings02Icon, diff --git a/frontend/console/src/features/modules/schema/schema.utils.ts b/frontend/console/src/features/modules/schema/schema.utils.ts index 0e7b6591d6..d818c513c9 100644 --- a/frontend/console/src/features/modules/schema/schema.utils.ts +++ b/frontend/console/src/features/modules/schema/schema.utils.ts @@ -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[] } = { diff --git a/frontend/console/src/features/timeline/filters/TimelineTimeControls.tsx b/frontend/console/src/features/timeline/filters/TimelineTimeControls.tsx index 82a3562b0e..9994c0ebc4 100644 --- a/frontend/console/src/features/timeline/filters/TimelineTimeControls.tsx +++ b/frontend/console/src/features/timeline/filters/TimelineTimeControls.tsx @@ -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' @@ -117,7 +117,7 @@ export const TimelineTimeControls = ({ > {selected.label} -