Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search capabilities to Peripheral Inspector view #25

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
);
});
84 changes: 84 additions & 0 deletions src/components/tree/components/search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/********************************************************************************
* 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: 20px;
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: top 0.2s ease, opacity 0.2s ease;
-moz-transition: top 0.2s ease, opacity 0.2s ease;
-ms-transition: top 0.2s ease, opacity 0.2s ease;
-o-transition: top 0.2s ease, opacity 0.2s ease;
transition: top 0.2s ease, opacity 0.2s ease;
}

.search-overlay.visible {
top: 5px;
opacity: 1;
}

body.has-scrollbar .search-overlay {
right: 5px;
}

.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);
}
54 changes: 41 additions & 13 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 @@ -28,18 +30,27 @@ const PROGRESS_BAR_HIDE_DELAY = 200;
export const ComponentTree = (props: ComponentTreeProps) => {
const treeContext = useCDTTreeContext();
const [showProgressBar, setShowProgressBar] = useState(false);
const [filter, setFilter] = React.useState<string | undefined>();
const searchRef = React.useRef<SearchOverlay>(null);

useEffect(() => {
// Slightly delay showing/hiding the progress bar to avoid flickering
const timer = setTimeout(() => setShowProgressBar(props.isLoading), PROGRESS_BAR_HIDE_DELAY);
return () => clearTimeout(timer);
}, [props.isLoading]);

useEffect(() => {
if (!props.isLoading) {
// Delay hiding the progress bar to allow the animation to complete
const timer = setTimeout(() => {
setShowProgressBar(false);
}, PROGRESS_BAR_HIDE_DELAY);
return () => clearTimeout(timer);
if (document.documentElement.scrollHeight > document.documentElement.clientHeight) {
document.body.classList.add('has-scrollbar');
} else {
setShowProgressBar(true);
document.body.classList.remove('has-scrollbar');
}
}, [props.isLoading]);
});

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

// Assemble the tree
if (props.nodes === undefined) {
Expand All @@ -52,7 +63,6 @@ export const ComponentTree = (props: ComponentTreeProps) => {
return <div>No children provided</div>;
}


// Event handler
const onToggle = async (event: TreeEventNodeEvent) => {
if (event.node.leaf) {
Expand All @@ -70,9 +80,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,12 +97,25 @@ 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}
className="w-full md:w-30rem"
Expand All @@ -104,7 +127,12 @@ export const ComponentTree = (props: ComponentTreeProps) => {
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>;
};

49 changes: 34 additions & 15 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,18 +31,22 @@ 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) {
// Delay hiding the progress bar to allow the animation to complete
const timer = setTimeout(() => {
setShowProgressBar(false);
}, PROGRESS_BAR_HIDE_DELAY);
return () => clearTimeout(timer);
// Slightly delay showing/hiding the progress bar to avoid flickering
const timer = setTimeout(() => setShowProgressBar(props.isLoading), PROGRESS_BAR_HIDE_DELAY);
return () => clearTimeout(timer);
}, [props.isLoading]);

useEffect(() => {
if (document.documentElement.scrollHeight > document.documentElement.clientHeight) {
document.body.classList.add('has-scrollbar');
} else {
setShowProgressBar(true);
document.body.classList.remove('has-scrollbar');
}
}, [props.isLoading]);
});

// Assemble the treetable
if (props.nodes === undefined) {
Expand All @@ -54,7 +59,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 +76,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 +90,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 +111,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 +130,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 +163,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
Loading
Loading