From 1cb8105b5e6e5527aa016b9ab036535d33a6e5ac Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 25 Jun 2024 15:28:08 +0200 Subject: [PATCH] Add search capabilities to Peripheral Inspector view - Provide custom SearchOverlay component - Add search overlay to filter tree and tree table - Move custom data into the 'data' for filtering in tree table Closes #23 --- .../tree/components/search-overlay.tsx | 86 +++++++++++++++++++ src/components/tree/components/search.css | 81 +++++++++++++++++ src/components/tree/components/tree.tsx | 44 +++++++--- src/components/tree/components/treetable.tsx | 30 +++++-- src/components/tree/components/utils.tsx | 2 +- src/components/tree/types.ts | 25 ++++-- src/plugin/peripheral/nodes/messagenode.ts | 1 - .../peripheral/nodes/peripheralfieldnode.ts | 1 - src/plugin/peripheral/nodes/peripheralnode.ts | 1 - .../nodes/peripheralregisternode.ts | 1 - .../tree/peripheral-session-tree.ts | 1 - .../peripheral-cdt-tree-data-provider.ts | 2 +- 12 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 src/components/tree/components/search-overlay.tsx create mode 100644 src/components/tree/components/search.css diff --git a/src/components/tree/components/search-overlay.tsx b/src/components/tree/components/search-overlay.tsx new file mode 100644 index 0000000..0c37750 --- /dev/null +++ b/src/components/tree/components/search-overlay.tsx @@ -0,0 +1,86 @@ +/********************************************************************* + * Copyright (c) 2024 Arm Limited and others + * + * This program and the accompanying materials are made available under the + * terms of the MIT License as outlined in the LICENSE File + ********************************************************************************/ + +import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; +import React from 'react'; +import './search.css'; + +export interface SearchOverlayProps { + onChange?: (text: string) => void; + onShow?: () => void; + onHide?: () => void; +} + +export interface SearchOverlay { + focus: () => void; + value(): string; + setValue: (value: string) => void; + show: () => void; + hide: () => void; +} + +export const SearchOverlay = React.forwardRef((props, ref) => { + const [showSearch, setShowSearch] = React.useState(false); + const searchTextRef = React.useRef(null); + const previousFocusedElementRef = React.useRef(null); + + const show = () => { + previousFocusedElementRef.current = document.activeElement as HTMLElement; + setShowSearch(true); + setTimeout(() => searchTextRef.current?.select(), 100); + props.onShow?.(); + }; + + const hide = () => { + setShowSearch(false); + props.onHide?.(); + if (previousFocusedElementRef.current) { + previousFocusedElementRef.current.focus(); + } + }; + + const onTextChange = (e: React.ChangeEvent) => { + const value = e.target.value; + props.onChange?.(value); + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.ctrlKey && e.key === 'f') { + e.preventDefault(); + e.stopPropagation(); + show(); + } else if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + hide(); + } + }; + + const onFocus = (e: React.FocusEvent) => { + if (e.relatedTarget) { + previousFocusedElementRef.current = e.relatedTarget as HTMLElement; + } + }; + + React.useImperativeHandle(ref, () => ({ + focus: () => searchTextRef.current?.focus(), + value: () => searchTextRef.current?.value ?? '', + setValue: (newValue: string) => { + if (searchTextRef.current) { + searchTextRef.current.value = newValue; + } + }, + show: () => show(), + hide: () => hide() + })); + + return (
+ + hide()} /> +
+ ); +}); diff --git a/src/components/tree/components/search.css b/src/components/tree/components/search.css new file mode 100644 index 0000000..800003c --- /dev/null +++ b/src/components/tree/components/search.css @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the MIT License as outlined in the LICENSE File + ********************************************************************************/ + + .search-overlay { + position: fixed; + top: -33px; + opacity: 0; + right: 5px; + background-color: var(--vscode-editorWidget-background); + box-shadow: 0 0 4px 1px var(--vscode-widget-shadow); + color: var(--vscode-editorWidget-foreground); + border-bottom: 1px solid var(--vscode-widget-border); + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-left: 1px solid var(--vscode-widget-border); + border-right: 1px solid var(--vscode-widget-border); + box-sizing: border-box; + height: 33px; + line-height: 19px; + overflow: hidden; + padding: 4px; + z-index: 35; + display: flex; + flex-direction: row; + gap: 5px; + + -webkit-transition: all 0.2s ease; + -moz-transition: all 0.2s ease; + -ms-transition: all 0.2s ease; + -o-transition: all 0.2s ease; + transition: all 0.2s ease; + } + + .search-overlay.visible { + top: 5px; + opacity: 1; + } + + .search-overlay .search-input { + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); + outline: none; + scrollbar-width: none; + border: none; + box-sizing: border-box; + display: inline-block; + font-family: inherit; + font-size: inherit; + height: 100%; + line-height: inherit; + resize: none; + width: 100%; + padding: 4px 6px; + margin: 0; + } + + .search-overlay input.search-input:focus { + outline: 1px solid var(--vscode-focusBorder) + } + + + .search-input::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .search-input::-moz-placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .search-input:-ms-input-placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .search-input:-webkit-input-placeholder { + color: var(--vscode-input-placeholderForeground); + } + \ No newline at end of file diff --git a/src/components/tree/components/tree.tsx b/src/components/tree/components/tree.tsx index 0d97eed..2b52e43 100644 --- a/src/components/tree/components/tree.tsx +++ b/src/components/tree/components/tree.tsx @@ -8,15 +8,15 @@ * SPDX-License-Identifier: EPL-2.0 *********************************************************************/ -import './common.css'; -import './tree.css'; - import { Tree, TreeEventNodeEvent, TreeNodeClickEvent } from 'primereact/tree'; import { TreeNode } from 'primereact/treenode'; import { classNames } from 'primereact/utils'; import React from 'react'; import { useCDTTreeContext } from '../tree-context'; import { CDTTreeItem, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types'; +import './common.css'; +import { SearchOverlay } from './search-overlay'; +import './tree.css'; import { createActions, createHighlightedText, createLabelWithTooltip } from './utils'; export type ComponentTreeProps = { @@ -24,17 +24,19 @@ export type ComponentTreeProps = { selectedNode?: CDTTreeItem; }; -export const ComponentTree = (props: ComponentTreeProps) => { +export const ComponentTree = ({ nodes, selectedNode }: ComponentTreeProps) => { // Assemble the tree - if (props.nodes === undefined) { + if (nodes === undefined) { return
loading
; } - if (!props.nodes.length) { + if (!nodes.length) { return
No children provided
; } const treeContext = useCDTTreeContext(); + const [filter, setFilter] = React.useState(); + const searchRef = React.useRef(null); // Event handler const onToggle = (event: TreeEventNodeEvent) => { @@ -53,9 +55,9 @@ export const ComponentTree = (props: ComponentTreeProps) => { const nodeTemplate = (node: TreeNode) => { CDTTreeItem.assert(node); return
- {createLabelWithTooltip(createHighlightedText(node.label, node.options?.highlights), node.options?.tooltip)} + {createLabelWithTooltip(createHighlightedText(node.label, node.data.options?.highlights), node.data.options?.tooltip)} {createActions(treeContext, node)}
; }; @@ -70,19 +72,37 @@ export const ComponentTree = (props: ComponentTreeProps) => { ; }; - return
+ const onKeyDown = (e: React.KeyboardEvent) => { + if (e.ctrlKey && e.key === 'f') { + e.preventDefault(); + e.stopPropagation(); + searchRef.current?.show(); + } + }; + + const onSearchShow = () => setFilter(searchRef.current?.value()); + const onSearchHide = () => setFilter(undefined); + const onSearchChange = (text: string) => setFilter(text); + + return
+ onClick(event)} onExpand={event => onToggle(event)} onCollapse={event => onToggle(event)} + filter={true} + filterMode='strict' + filterValue={filter} + onFilterValueChange={() => { /* needed as otherwise the filter value is not taken into account */ }} + showHeader={false} /> -
; +
; }; diff --git a/src/components/tree/components/treetable.tsx b/src/components/tree/components/treetable.tsx index 397a556..592c434 100644 --- a/src/components/tree/components/treetable.tsx +++ b/src/components/tree/components/treetable.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { useCDTTreeContext } from '../tree-context'; import { CDTTreeItem, CDTTreeTableColumnDefinition, CDTTreeTableExpanderColumn, CDTTreeTableStringColumn, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types'; import { createActions, createHighlightedText, createIcon, createLabelWithTooltip } from './utils'; +import { SearchOverlay } from './search-overlay'; export type ComponentTreeTableProps = { nodes?: CDTTreeItem[]; @@ -37,6 +38,8 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => { } const treeContext = useCDTTreeContext(); + const [filter, setFilter] = React.useState(); + const searchRef = React.useRef(null); // Event handler const onToggle = (event: TreeTableEvent) => { @@ -55,7 +58,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => { const template = (node: TreeNode, field: string) => { CDTTreeItem.assert(node); - const column = node.columns?.[field]; + const column = node.data.columns?.[field]; if (column?.type === 'expander') { return expanderTemplate(node, column); @@ -69,7 +72,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => { const expanderTemplate = (node: TreeNode, column: CDTTreeTableExpanderColumn) => { CDTTreeItem.assert(node); - return
{ const text = createHighlightedText(column.label, column.highlight); return
{createLabelWithTooltip(text, column.tooltip)}
; @@ -109,7 +112,20 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => { const expandedState = getExpandedState(props.nodes); const selectedKey = props.selectedNode ? props.selectedNode.key as string : undefined; - return
+ const onKeyDown = (e: React.KeyboardEvent) => { + if (e.ctrlKey && e.key === 'f') { + e.preventDefault(); + e.stopPropagation(); + searchRef.current?.show(); + } + }; + + const onSearchShow = () => setFilter(searchRef.current?.value()); + const onSearchHide = () => setFilter(undefined); + const onSearchChange = (text: string) => setFilter(text); + + return
+ { onExpand={event => onToggle(event)} onCollapse={event => onToggle(event)} onRowClick={event => onClick(event)} + filterMode='strict' // continue searching on children + globalFilter={filter} > {props.columnDefinitions?.map(c => { - return template(node, c.field)} expander={c.expander} />; + return template(node, c.field)} expander={c.expander} filter={true} />; })} - +
; }; diff --git a/src/components/tree/components/utils.tsx b/src/components/tree/components/utils.tsx index d2032e8..981e9ce 100644 --- a/src/components/tree/components/utils.tsx +++ b/src/components/tree/components/utils.tsx @@ -71,7 +71,7 @@ export function createActions(context: CDTTreeContext, node: TreeNode): React.JS }; return
- {node.options?.commands?.map(a => onClick(event, a)}>)} + {node.data.options?.commands?.map(a => onClick(event, a)}>)}
; } diff --git a/src/components/tree/types.ts b/src/components/tree/types.ts index da6b119..d411301 100644 --- a/src/components/tree/types.ts +++ b/src/components/tree/types.ts @@ -33,14 +33,22 @@ export interface CDTTreeTableStringColumn { tooltip?: string; } -export interface CDTTreeItem extends PrimeTreeNode { - __type: 'CDTTreeItem' - id: string; - key: string; - icon?: string; +export interface CDTTreeItemData { path: string[]; options?: CDTTreeOptions; columns?: Record; +} + +export interface CDTTreeItemOptions extends Omit { + id: string; /* we require the user to provide an id */ + key?: string; /* we only allow string keys */ + icon?: string; /* we need to provide icons as string as they cannot be serialized otherwise */ + children?: CDTTreeItem[]; /* children are typed to our own tree items */ +} + +export interface CDTTreeItem extends CDTTreeItemOptions { + __type: 'CDTTreeItem' + data: CDTTreeItemData; children?: CDTTreeItem[]; } @@ -55,10 +63,13 @@ export namespace CDTTreeItem { } } - export function create(options: Omit): CDTTreeItem { + export function create(props: CDTTreeItemOptions & CDTTreeItemData): CDTTreeItem { + const { path, options, columns, ...itemProps } = props; return { __type: 'CDTTreeItem', - ...options + ...itemProps, + key: props.key ?? itemProps.id, + data: { path, options, columns } }; } } diff --git a/src/plugin/peripheral/nodes/messagenode.ts b/src/plugin/peripheral/nodes/messagenode.ts index c788bae..1ac6657 100644 --- a/src/plugin/peripheral/nodes/messagenode.ts +++ b/src/plugin/peripheral/nodes/messagenode.ts @@ -36,7 +36,6 @@ export class MessageNode extends PeripheralBaseNode { public getCDTTreeItem(): CDTTreeItem { return CDTTreeItem.create({ id: this.getId(), - key: this.getId(), path: this.getId().split(PERIPHERAL_ID_SEP), }); } diff --git a/src/plugin/peripheral/nodes/peripheralfieldnode.ts b/src/plugin/peripheral/nodes/peripheralfieldnode.ts index 6815b4b..d849fbc 100644 --- a/src/plugin/peripheral/nodes/peripheralfieldnode.ts +++ b/src/plugin/peripheral/nodes/peripheralfieldnode.ts @@ -121,7 +121,6 @@ export class PeripheralFieldNode extends PeripheralBaseNode { return CDTTreeItem.create({ id: this.getId(), - key: this.getId(), label: this.getLabel(), leaf: true, path: this.getId().split(PERIPHERAL_ID_SEP), diff --git a/src/plugin/peripheral/nodes/peripheralnode.ts b/src/plugin/peripheral/nodes/peripheralnode.ts index c794a5a..80cea8f 100644 --- a/src/plugin/peripheral/nodes/peripheralnode.ts +++ b/src/plugin/peripheral/nodes/peripheralnode.ts @@ -100,7 +100,6 @@ export class PeripheralNode extends PeripheralBaseNode { public getCDTTreeItem(): CDTTreeItem { return CDTTreeItem.create({ id: this.getId(), - key: this.getId(), label: this.getLabel(), icon: this.pinned ? 'codicon codicon-pinned' : undefined, expanded: this.expanded, diff --git a/src/plugin/peripheral/nodes/peripheralregisternode.ts b/src/plugin/peripheral/nodes/peripheralregisternode.ts index ff839f1..3221dfb 100644 --- a/src/plugin/peripheral/nodes/peripheralregisternode.ts +++ b/src/plugin/peripheral/nodes/peripheralregisternode.ts @@ -150,7 +150,6 @@ export class PeripheralRegisterNode extends ClusterOrRegisterBaseNode { const labelValue = this.getLabelValue(); return CDTTreeItem.create({ id: this.getId(), - key: this.getId(), label: this.getLabel(), expanded: this.expanded, path: this.getId().split(PERIPHERAL_ID_SEP), diff --git a/src/plugin/peripheral/tree/peripheral-session-tree.ts b/src/plugin/peripheral/tree/peripheral-session-tree.ts index 1bca1d3..602d453 100644 --- a/src/plugin/peripheral/tree/peripheral-session-tree.ts +++ b/src/plugin/peripheral/tree/peripheral-session-tree.ts @@ -231,7 +231,6 @@ export class PeripheralTreeForSession extends PeripheralBaseNode { public getCDTTreeItem(): MaybePromise { return CDTTreeItem.create({ id: this.getId(), - key: this.getId(), label: this.getTitle(), path: [], }); diff --git a/src/plugin/peripheral/tree/provider/peripheral-cdt-tree-data-provider.ts b/src/plugin/peripheral/tree/provider/peripheral-cdt-tree-data-provider.ts index 9297ed5..f037ecd 100644 --- a/src/plugin/peripheral/tree/provider/peripheral-cdt-tree-data-provider.ts +++ b/src/plugin/peripheral/tree/provider/peripheral-cdt-tree-data-provider.ts @@ -90,7 +90,7 @@ export class PeripheralCDTTreeDataProvider implements CDTTreeDataProvider