Skip to content

Commit

Permalink
Add search capabilities to Peripheral Inspector view
Browse files Browse the repository at this point in the history
- 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 eclipse-cdt-cloud#23
  • Loading branch information
martin-fleck-at committed Sep 3, 2024
1 parent 61550b2 commit 0fb6d4c
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 36 deletions.
86 changes: 86 additions & 0 deletions src/components/tree/components/search-overlay.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchOverlay, SearchOverlayProps>((props, ref) => {
const [showSearch, setShowSearch] = React.useState(false);
const searchTextRef = React.useRef<HTMLInputElement>(null);
const previousFocusedElementRef = React.useRef<HTMLElement | null>(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<HTMLInputElement>) => {
const value = e.target.value;
props.onChange?.(value);
};

const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
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<HTMLInputElement>) => {
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 (<div className={showSearch ? 'search-overlay visible' : 'search-overlay'} onKeyDown={onKeyDown}>
<input ref={searchTextRef} onChange={onTextChange} onFocus={onFocus} placeholder="Find" className="search-input" />
<VSCodeButton title='Close (Escape)' appearance='icon' aria-label='Close (Escape)'><span className='codicon codicon-close' onClick={() => hide()} /></VSCodeButton>
</div>
);
});
80 changes: 80 additions & 0 deletions src/components/tree/components/search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/********************************************************************************
* 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);
}
50 changes: 38 additions & 12 deletions src/components/tree/components/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { classNames } from 'primereact/utils';
import React, { useEffect, useState } from 'react';
import { useCDTTreeContext } from '../tree-context';
import { CDTTreeItem, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import { SearchOverlay } from './search-overlay';

import { createActions, createHighlightedText, createLabelWithTooltip } from './utils';
import { ProgressBar } from 'primereact/progressbar';

Expand All @@ -25,12 +27,14 @@ export type ComponentTreeProps = {

const PROGRESS_BAR_HIDE_DELAY = 200;

export const ComponentTree = (props: ComponentTreeProps) => {
export const ComponentTree = ({ nodes, selectedNode, isLoading }: ComponentTreeProps) => {
const treeContext = useCDTTreeContext();
const [showProgressBar, setShowProgressBar] = useState(false);
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

useEffect(() => {
if (!props.isLoading) {
if (!isLoading) {
// Delay hiding the progress bar to allow the animation to complete
const timer = setTimeout(() => {
setShowProgressBar(false);
Expand All @@ -39,20 +43,24 @@ export const ComponentTree = (props: ComponentTreeProps) => {
} else {
setShowProgressBar(true);
}
}, [props.isLoading]);
}, [isLoading]);

// Assemble the tree
if (props.nodes === undefined) {
if (nodes === undefined) {
return <div>loading</div>;
}

// Assemble the tree
if (nodes === undefined) {
return <div>
<ProgressBar mode="indeterminate" className='sticky top-0'></ProgressBar>
</div>;
}

if (!props.nodes.length) {
if (!nodes.length) {
return <div>No children provided</div>;
}


// Event handler
const onToggle = async (event: TreeEventNodeEvent) => {
if (event.node.leaf) {
Expand All @@ -70,9 +78,9 @@ export const ComponentTree = (props: ComponentTreeProps) => {
const nodeTemplate = (node: TreeNode) => {
CDTTreeItem.assert(node);
return <div className='tree-node'
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.path })}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.data.path })}
>
{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)}
</div>;
};
Expand All @@ -87,24 +95,42 @@ export const ComponentTree = (props: ComponentTreeProps) => {
</div>;
};

return <div>
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
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 <div onKeyDown={onKeyDown}>
<div className='progress-bar-container'>
{showProgressBar &&
<ProgressBar mode="indeterminate" className='sticky top-0'></ProgressBar>
}
</div>
<SearchOverlay key={'search'} ref={searchRef} onHide={onSearchHide} onShow={onSearchShow} onChange={onSearchChange} />
<Tree
value={props.nodes}
value={nodes}
className="w-full md:w-30rem"
style={{ minWidth: '10rem' }}
nodeTemplate={nodeTemplate}
togglerTemplate={togglerTemplate}
selectionMode='single'
selectionKeys={props.selectedNode?.key?.toString()}
selectionKeys={selectedNode?.key?.toString()}
onNodeClick={event => 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}
/>
</div >;
</div>;
};

31 changes: 24 additions & 7 deletions src/components/tree/components/treetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import React, { useEffect, useState } 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';
import { ProgressBar } from 'primereact/progressbar';

export type ComponentTreeTableProps = {
Expand All @@ -30,6 +31,8 @@ const PROGRESS_BAR_HIDE_DELAY = 200;
export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const treeContext = useCDTTreeContext();
const [showProgressBar, setShowProgressBar] = useState(false);
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

useEffect(() => {
if (!props.isLoading) {
Expand All @@ -54,7 +57,6 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
return <div>No children provided</div>;
}


// Event handler
const onToggle = (event: TreeTableEvent) => {
if (event.node.leaf) {
Expand All @@ -72,7 +74,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);
Expand All @@ -86,7 +88,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const expanderTemplate = (node: TreeNode, column: CDTTreeTableExpanderColumn) => {
CDTTreeItem.assert(node);

return <div style={{ paddingLeft: `${((node.path.length ?? 1)) * 8}px` }}
return <div style={{ paddingLeft: `${((node.data.path.length ?? 1)) * 8}px` }}
>
<div className='treetable-node' >
<div
Expand All @@ -107,7 +109,7 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const text = createHighlightedText(column.label, column.highlight);

return <div
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.path })}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: node.id, cdtTreeItemPath: node.data.path })}
>
{createLabelWithTooltip(text, column.tooltip)}
</div>;
Expand All @@ -126,12 +128,25 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
const expandedState = getExpandedState(props.nodes);
const selectedKey = props.selectedNode ? props.selectedNode.key as string : undefined;

return <div>
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
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 <div onKeyDown={onKeyDown}>
<div className='progress-bar-container'>
{showProgressBar &&
<ProgressBar mode="indeterminate" className='sticky top-0'></ProgressBar>
}
</div>
<SearchOverlay key={'search'} ref={searchRef} onHide={onSearchHide} onShow={onSearchShow} onChange={onSearchChange} />
<TreeTable
value={props.nodes}
selectionKeys={selectedKey}
Expand All @@ -146,11 +161,13 @@ export const ComponentTreeTable = (props: ComponentTreeTableProps) => {
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 <Column key={`${c.field}_column`} field={c.field} body={(node) => template(node, c.field)} expander={c.expander} />;
return <Column key={`${c.field}_column`} field={c.field} body={(node) => template(node, c.field)} expander={c.expander} filter={true} />;
})}
<Column field="actions" style={{ width: '64px' }} body={actionsTemplate} />
<Column key={'actions'} field="actions" style={{ width: '64px' }} body={actionsTemplate} />
</TreeTable>
</div>;
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/tree/components/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function createHighlightedText(label?: string, highlights?: [number, numb
export function createLabelWithTooltip(child: React.JSX.Element, tooltip?: string): React.JSX.Element {
const label = <div className="tree-label flex-auto flex align-items-center">
{child}
</div >;
</div>;

if (tooltip === undefined) {
return label;
Expand All @@ -72,7 +72,7 @@ export function createActions(context: CDTTreeContext, node: TreeNode): React.JS
};

return <div className="tree-actions">
{node.options?.commands?.map(a => <i key={a.commandId} className={`codicon codicon-${a.icon}`} onClick={(event) => onClick(event, a)}></i>)}
{node.data.options?.commands?.map(a => <i key={a.commandId} className={`codicon codicon-${a.icon}`} onClick={(event) => onClick(event, a)}></i>)}
</div>;
}

Expand Down
Loading

0 comments on commit 0fb6d4c

Please sign in to comment.