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 quick search for logs containers #2470

Merged
merged 5 commits into from
Jan 9, 2025
Merged
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
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%' },
Meklo marked this conversation as resolved.
Show resolved Hide resolved
}) => {
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}
Meklo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading