Skip to content

Commit

Permalink
Add quick search for logs containers (#2470)
Browse files Browse the repository at this point in the history
Signed-off-by: Ayoub LABIDI <[email protected]>
  • Loading branch information
ayolab authored Jan 9, 2025
1 parent 16dbebd commit 281070b
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 31 deletions.
18 changes: 12 additions & 6 deletions src/components/report-viewer/QuickSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useState, useCallback, useRef, useEffect } from 'react';
import { TextField, InputAdornment, IconButton, Box } from '@mui/material';
import { Clear, KeyboardArrowUp, KeyboardArrowDown } from '@mui/icons-material';
import { useIntl } from 'react-intl';
import { useDebounce } from '@gridsuite/commons-ui';

interface QuickSearchProps {
currentResultIndex: number;
Expand All @@ -16,6 +17,8 @@ interface QuickSearchProps {
onNavigate: (direction: 'next' | 'previous') => void;
resultCount: number;
resetSearch: () => void;
placeholder?: string;
style?: React.CSSProperties;
}

const styles = {
Expand All @@ -31,16 +34,19 @@ export const QuickSearch: React.FC<QuickSearchProps> = ({
onNavigate,
resultCount,
resetSearch,
placeholder,
style = { minWidth: '30%' },
}) => {
const [searchTerm, setSearchTerm] = useState<string>('');
const [resultsCountDisplay, setResultsCountDisplay] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const intl = useIntl();
const debounceSearch = useDebounce(onSearch, 300);

const handleSearch = useCallback(() => {
onSearch(searchTerm);
debounceSearch(searchTerm);
setResultsCountDisplay(true);
}, [searchTerm, onSearch]);
}, [searchTerm, debounceSearch]);

const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
Expand All @@ -63,12 +69,12 @@ export const QuickSearch: React.FC<QuickSearchProps> = ({
setResultsCountDisplay(false);
}
setSearchTerm(value);
onSearch(value);
debounceSearch(value);
if (value.length > 0) {
setResultsCountDisplay(true);
}
},
[onSearch, resetSearch, searchTerm.length]
[debounceSearch, resetSearch, searchTerm.length]
);

const handleClear = useCallback(() => {
Expand All @@ -91,8 +97,8 @@ export const QuickSearch: React.FC<QuickSearchProps> = ({
value={searchTerm}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={intl.formatMessage({ id: 'searchPlaceholderLog' })}
sx={{ minWidth: '30%' }}
placeholder={placeholder ? intl.formatMessage({ id: placeholder }) : ''}
sx={{ ...style }}
size="small"
InputProps={{
endAdornment: (
Expand Down
1 change: 1 addition & 0 deletions src/components/report-viewer/log-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ const LogTable = ({ selectedReportId, reportType, reportNature, severities, onRo
onNavigate={handleNavigate}
resultCount={searchResults.length}
resetSearch={resetSearch}
placeholder="searchPlaceholderLog"
/>
</Box>
<Box sx={styles.chipContainer}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/report-viewer/report-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function ReportViewer({ report, reportType, severities }: ReportV
'px)',
}}
>
<Grid item sm={3}>
<Grid item sm={3} sx={{ borderRight: (theme) => `1px solid ${theme.palette.divider}` }}>
{reportTree && (
<VirtualizedTreeview
expandedTreeReports={expandedTreeReports}
Expand Down
57 changes: 50 additions & 7 deletions src/components/report-viewer/treeview-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { CSSProperties, FunctionComponent, ReactNode } from 'react';
import { Box, Stack, Typography, styled, Theme } from '@mui/material';
import { CSSProperties, FunctionComponent, ReactNode, useCallback, useMemo } from 'react';
import { Box, Stack, Typography, styled, Theme, useTheme } from '@mui/material';
import * as React from 'react';
import { mergeSx } from '@gridsuite/commons-ui';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import { ListChildComponentProps } from 'react-window';

export interface ReportItem {
id: string;
Expand Down Expand Up @@ -83,9 +84,12 @@ export interface TreeViewItemData {
onSelectedItem: (node: ReportItem) => void;
onExpandItem: (node: ReportItem) => void;
highlightedReportId?: string;
searchTerm: string;
currentResultIndex: number;
searchResults: number[];
}

export interface TreeViewItemProps {
export interface TreeViewItemProps extends ListChildComponentProps {
data: TreeViewItemData;
index: number;
style: CSSProperties;
Expand All @@ -95,10 +99,49 @@ const ITEM_DEPTH_OFFSET = 12;

export const TreeviewItem: FunctionComponent<TreeViewItemProps> = (props) => {
const { data, index } = props;
const { nodes, onSelectedItem, onExpandItem, highlightedReportId } = data;
const { nodes, onSelectedItem, onExpandItem, highlightedReportId, searchTerm, currentResultIndex, searchResults } =
data;
const currentNode = nodes[index];
const left = currentNode.depth * ITEM_DEPTH_OFFSET;
const isCollapsable = currentNode.isCollapsable ?? true;
const theme = useTheme();

const handleClick = useCallback(() => {
onSelectedItem(currentNode);
}, [onSelectedItem, currentNode]);

const handleExpand = useCallback(() => {
onExpandItem(currentNode);
}, [onExpandItem, currentNode]);

const highlightText = useMemo(
() => (text: string, highlight: string) => {
if (!highlight) {
return text;
}
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return parts.map((part, partIndex) => {
if (part.toLowerCase() === highlight.toLowerCase()) {
const isCurrentOccurrence = searchResults[currentResultIndex] === index;
return (
<span
key={`${part}-${partIndex}`}
style={{
backgroundColor: isCurrentOccurrence
? theme.searchedText.currentHighlightColor
: theme.searchedText.highlightColor,
}}
>
{part}
</span>
);
}
return part;
});
},
[searchResults, currentResultIndex, index, theme]
);

return (
<TreeViewItemBox sx={mergeSx(styles.content, styles.labelRoot)} style={props.style}>
<TreeViewItemStack
Expand All @@ -111,12 +154,12 @@ export const TreeviewItem: FunctionComponent<TreeViewItemProps> = (props) => {
<Box
component={currentNode.collapsed ? ArrowRightIcon : ArrowDropDownIcon}
sx={{ visibility: currentNode.isLeaf ? 'hidden' : 'visible', fontSize: '18px' }}
onClick={() => onExpandItem(currentNode)}
onClick={handleExpand}
/>
)}
{currentNode.icon}
<Typography variant="body2" sx={styles.labelText} onClick={() => onSelectedItem(currentNode)}>
{currentNode.label}
<Typography variant="body2" sx={styles.labelText} onClick={handleClick}>
{highlightText(currentNode.label, searchTerm)}
</Typography>
</TreeViewItemStack>
</TreeViewItemBox>
Expand Down
136 changes: 119 additions & 17 deletions src/components/report-viewer/virtualized-treeview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { FunctionComponent, useCallback, useMemo, useRef } from 'react';
import { FunctionComponent, useCallback, useMemo, useRef, useState, useEffect } from 'react';
import { FixedSizeList } from 'react-window';
import { ReportItem, TreeviewItem } from './treeview-item';
import { AutoSizer } from 'react-virtualized';
import { ReportTree } from '../../utils/report/report.type';
import Label from '@mui/icons-material/Label';
import { Theme } from '@mui/system';
import { useTreeViewScroll } from './use-treeview-scroll';
import { QuickSearch } from './QuickSearch';
import { Box } from '@mui/material';

const styles = {
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
treeItem: {
whiteSpace: 'nowrap',
},
labelIcon: (theme: Theme) => ({
marginRight: theme.spacing(1),
}),
quickSearch: { minWidth: '100%', flexShrink: 0, marginBottom: 1 },
};

export interface TreeViewProps {
Expand All @@ -40,6 +48,9 @@ export const VirtualizedTreeview: FunctionComponent<TreeViewProps> = ({
reportTree,
}) => {
const listRef = useRef<FixedSizeList>(null);
const [searchTerm, setSearchTerm] = useState<string>('');
const [searchResults, setSearchResults] = useState<number[]>([]);
const [currentResultIndex, setCurrentResultIndex] = useState(-1);

const onExpandItem = useCallback(
(node: ReportItem) => {
Expand Down Expand Up @@ -81,22 +92,113 @@ export const VirtualizedTreeview: FunctionComponent<TreeViewProps> = ({

useTreeViewScroll(highlightedReportId, nodes, listRef);

const expandIfMatch = useCallback((item: ReportTree, searchTerm: string, newExpandedTreeReports: Set<string>) => {
let hasMatchingChild = false;
item.subReports.forEach((subReport) => {
if (expandIfMatch(subReport, searchTerm, newExpandedTreeReports)) {
hasMatchingChild = true;
}
});
if (item.message.toLowerCase().includes(searchTerm.toLowerCase()) || hasMatchingChild) {
newExpandedTreeReports.add(item.id);
return true;
}
return false;
}, []);

const handleSearch = useCallback(
(searchTerm: string) => {
setSearchTerm(searchTerm);
const matches: number[] = [];
const newExpandedTreeReports = new Set(expandedTreeReports);

expandIfMatch(reportTree, searchTerm, newExpandedTreeReports);

const expandedNodes = toTreeNodes(reportTree, 0);
expandedNodes.forEach((node, index) => {
if (node.label.toLowerCase().includes(searchTerm.toLowerCase())) {
matches.push(index);
}
});

setExpandedTreeReports(Array.from(newExpandedTreeReports));
setSearchResults(matches);
setCurrentResultIndex(matches.length > 0 ? 0 : -1);
},
[expandedTreeReports, expandIfMatch, reportTree, toTreeNodes, setExpandedTreeReports]
);

useEffect(() => {
if (currentResultIndex >= 0 && searchResults.length > 0) {
listRef.current?.scrollToItem(searchResults[currentResultIndex], 'end');
}
}, [currentResultIndex, searchResults]);

const handleNavigate = useCallback(
(direction: 'next' | 'previous') => {
if (searchResults.length === 0) {
return;
}

let newIndex;

if (direction === 'next') {
newIndex = (currentResultIndex + 1) % searchResults.length;
} else {
newIndex = (currentResultIndex - 1 + searchResults.length) % searchResults.length;
}

setCurrentResultIndex(newIndex);
},
[currentResultIndex, searchResults]
);

const resetSearch = useCallback(() => {
setSearchTerm('');
setSearchResults([]);
setCurrentResultIndex(-1);
}, []);

return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
ref={listRef}
height={height}
width={width}
style={styles.treeItem}
itemCount={nodes.length}
itemSize={32}
itemKey={(index) => nodes[index].id}
itemData={{ nodes, onSelectedItem, onExpandItem, highlightedReportId }}
>
{TreeviewItem}
</FixedSizeList>
)}
</AutoSizer>
<Box sx={styles.container}>
<Box sx={styles.quickSearch}>
<QuickSearch
currentResultIndex={currentResultIndex}
selectedReportId={reportTree.id}
onSearch={handleSearch}
onNavigate={handleNavigate}
resultCount={searchResults.length}
resetSearch={resetSearch}
placeholder="searchPlaceholderLogsContainers"
style={{ minWidth: '80%' }}
/>
</Box>
<Box sx={{ flexGrow: 1 }}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
ref={listRef}
height={height}
width={width}
style={styles.treeItem}
itemCount={nodes.length}
itemSize={32}
itemKey={(index) => nodes[index].id}
itemData={{
nodes,
onSelectedItem,
onExpandItem,
highlightedReportId,
searchTerm,
currentResultIndex,
searchResults,
}}
>
{TreeviewItem}
</FixedSizeList>
)}
</AutoSizer>
</Box>
</Box>
);
};
1 change: 1 addition & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,7 @@
"NoFilter": "No filter",
"searchPlaceholder": "Search",
"searchPlaceholderLog": "Search in Logs",
"searchPlaceholderLogsContainers": "Search in Logs containers",
"GeneratedModification": "Generated-modification",
"guidancePopUp.title": "Selection on the map",
"guidancePopUp.firstVariant":"{symbol} Click on the map to start drawing a polygon.",
Expand Down
1 change: 1 addition & 0 deletions src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,7 @@
"NoFilter": "Aucun filtre",
"searchPlaceholder": "Recherche",
"searchPlaceholderLog": "Rechercher dans les Logs",
"searchPlaceholderLogsContainers": "Rechercher dans les conteneurs de Logs",
"GeneratedModification": "Modification-générée",
"guidancePopUp.title": "Sélection sur la carte",
"guidancePopUp.firstVariant":"{symbol} Cliquez sur la carte pour commencer à dessiner un polygone.",
Expand Down

0 comments on commit 281070b

Please sign in to comment.