Skip to content

Commit 9927069

Browse files
authored
feat(query): Add a "Search" tab in the Sidebar for wildcard queries. (#107)
1 parent 417236e commit 9927069

18 files changed

+595
-53
lines changed

src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css

+7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
padding: 0.75rem;
33
}
44

5+
.sidebar-tab-panel-container {
6+
display: flex;
7+
flex-direction: column;
8+
height: 100%;
9+
}
10+
511
.sidebar-tab-panel-title-container {
612
user-select: none;
713
margin-bottom: 0.5rem !important;
814
}
915

1016
.sidebar-tab-panel-title {
17+
flex-grow: 1;
1118
font-size: 0.875rem !important;
1219
font-weight: 400 !important;
1320
text-transform: uppercase;

src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx

+30-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from "react";
22

33
import {
4+
Box,
5+
ButtonGroup,
46
DialogContent,
57
DialogTitle,
68
TabPanel,
@@ -14,6 +16,7 @@ interface CustomTabPanelProps {
1416
children: React.ReactNode,
1517
tabName: string,
1618
title: string,
19+
titleButtons?: React.ReactNode,
1720
}
1821

1922
/**
@@ -23,25 +26,40 @@ interface CustomTabPanelProps {
2326
* @param props.children
2427
* @param props.tabName
2528
* @param props.title
29+
* @param props.titleButtons
2630
* @return
2731
*/
28-
const CustomTabPanel = ({children, tabName, title}: CustomTabPanelProps) => {
32+
const CustomTabPanel = ({
33+
children,
34+
tabName,
35+
title,
36+
titleButtons,
37+
}: CustomTabPanelProps) => {
2938
return (
3039
<TabPanel
3140
className={"sidebar-tab-panel"}
3241
value={tabName}
3342
>
34-
<DialogTitle className={"sidebar-tab-panel-title-container"}>
35-
<Typography
36-
className={"sidebar-tab-panel-title"}
37-
level={"body-md"}
38-
>
39-
{title}
40-
</Typography>
41-
</DialogTitle>
42-
<DialogContent>
43-
{children}
44-
</DialogContent>
43+
<Box className={"sidebar-tab-panel-container"}>
44+
<DialogTitle className={"sidebar-tab-panel-title-container"}>
45+
<Typography
46+
className={"sidebar-tab-panel-title"}
47+
level={"body-md"}
48+
>
49+
{title}
50+
</Typography>
51+
<ButtonGroup
52+
size={"sm"}
53+
spacing={"1px"}
54+
variant={"plain"}
55+
>
56+
{titleButtons}
57+
</ButtonGroup>
58+
</DialogTitle>
59+
<DialogContent>
60+
{children}
61+
</DialogContent>
62+
</Box>
4563
</TabPanel>
4664
);
4765
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.tab-panel-title-button {
2+
min-width: 0 !important;
3+
min-height: 0 !important;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
IconButton,
3+
IconButtonProps,
4+
} from "@mui/joy";
5+
6+
import "./PanelTitleButton.css";
7+
8+
9+
/**
10+
* Renders an IconButton for use in sidebar tab titles.
11+
*
12+
* @param props
13+
* @return
14+
*/
15+
const PanelTitleButton = (props: IconButtonProps) => {
16+
const {className, ...rest} = props;
17+
return (
18+
<IconButton
19+
className={`tab-panel-title-button ${className ?? ""}`}
20+
{...rest}/>
21+
);
22+
};
23+
24+
25+
export default PanelTitleButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.result-button {
2+
user-select: none;
3+
4+
overflow-x: hidden;
5+
6+
width: 100%;
7+
padding-left: 12px;
8+
9+
text-align: left;
10+
text-overflow: ellipsis;
11+
white-space: nowrap;
12+
}
13+
14+
.result-button:hover {
15+
cursor: default;
16+
}
17+
18+
.result-button-text {
19+
font-family: Inter, sans-serif !important;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
ListItemButton,
3+
Typography,
4+
} from "@mui/joy";
5+
6+
import {updateWindowUrlHashParams} from "../../../../../contexts/UrlContextProvider";
7+
8+
import "./Result.css";
9+
10+
11+
interface ResultProps {
12+
logEventNum: number,
13+
message: string,
14+
matchRange: [number, number]
15+
}
16+
17+
const QUERY_RESULT_PREFIX_MAX_CHARACTERS = 20;
18+
19+
/**
20+
* Renders a query result as a button with a message, highlighting the first matching text range.
21+
*
22+
* @param props
23+
* @param props.message
24+
* @param props.matchRange A two-element array [begin, end) representing the indices of the matching
25+
* text range.
26+
* @param props.logEventNum
27+
* @return
28+
*/
29+
const Result = ({logEventNum, message, matchRange}: ResultProps) => {
30+
const [
31+
beforeMatch,
32+
match,
33+
afterMatch,
34+
] = [
35+
message.slice(0, matchRange[0]),
36+
message.slice(...matchRange),
37+
message.slice(matchRange[1]),
38+
];
39+
const handleResultButtonClick = () => {
40+
updateWindowUrlHashParams({logEventNum});
41+
};
42+
43+
return (
44+
<ListItemButton
45+
className={"result-button"}
46+
onClick={handleResultButtonClick}
47+
>
48+
<Typography
49+
className={"result-button-text"}
50+
level={"body-sm"}
51+
>
52+
<span>
53+
{(QUERY_RESULT_PREFIX_MAX_CHARACTERS < beforeMatch.length) && "..."}
54+
{beforeMatch.slice(-QUERY_RESULT_PREFIX_MAX_CHARACTERS)}
55+
</span>
56+
<Typography
57+
className={"result-button-text"}
58+
level={"body-sm"}
59+
sx={{backgroundColor: "warning.softBg"}}
60+
>
61+
{match}
62+
</Typography>
63+
{afterMatch}
64+
</Typography>
65+
</ListItemButton>
66+
);
67+
};
68+
69+
70+
export default Result;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.results-group-summary-button {
2+
cursor: default !important;
3+
flex-direction: row-reverse !important;
4+
gap: 2px !important;
5+
padding-inline-start: 0 !important;
6+
}
7+
8+
.results-group-summary-container {
9+
display: flex;
10+
flex-grow: 1;
11+
}
12+
13+
.results-group-summary-text-container {
14+
flex-grow: 1;
15+
gap: 0.2rem;
16+
align-items: center;
17+
}
18+
19+
.results-group-summary-count {
20+
border-radius: 4px !important;
21+
}
22+
23+
.results-group-details {
24+
margin-left: 1.5px !important;
25+
/* stylelint-disable-next-line custom-property-pattern */
26+
border-left: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1);
27+
}
28+
29+
.results-group-details-content {
30+
padding-block: 0 !important;
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, {
2+
memo,
3+
useEffect,
4+
useState,
5+
} from "react";
6+
7+
import {
8+
Accordion,
9+
AccordionDetails,
10+
AccordionSummary,
11+
Box,
12+
Chip,
13+
List,
14+
Stack,
15+
Typography,
16+
} from "@mui/joy";
17+
18+
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
19+
20+
import {QueryResultsType} from "../../../../../typings/worker";
21+
import Result from "./Result";
22+
23+
import "./ResultsGroup.css";
24+
25+
26+
interface ResultsGroupProps {
27+
isAllExpanded: boolean,
28+
pageNum: number,
29+
results: QueryResultsType[],
30+
}
31+
32+
/**
33+
* Renders a group of results, where each group represents a list of results from a single page.
34+
*
35+
* @param props
36+
* @param props.isAllExpanded
37+
* @param props.pageNum
38+
* @param props.results
39+
* @return
40+
*/
41+
const ResultsGroup = memo(({
42+
isAllExpanded,
43+
pageNum,
44+
results,
45+
}: ResultsGroupProps) => {
46+
const [isExpanded, setIsExpanded] = useState<boolean>(isAllExpanded);
47+
48+
const handleAccordionChange = (
49+
_: React.SyntheticEvent,
50+
newValue: boolean
51+
) => {
52+
setIsExpanded(newValue);
53+
};
54+
55+
// On `isAllExpanded` update, sync current results group's expand status.
56+
useEffect(() => {
57+
setIsExpanded(isAllExpanded);
58+
}, [isAllExpanded]);
59+
60+
return (
61+
<Accordion
62+
expanded={isExpanded}
63+
onChange={handleAccordionChange}
64+
>
65+
<AccordionSummary
66+
slotProps={{
67+
button: {className: "results-group-summary-button"},
68+
}}
69+
>
70+
<Box className={"results-group-summary-container"}>
71+
<Stack
72+
className={"results-group-summary-text-container"}
73+
direction={"row"}
74+
>
75+
<DescriptionOutlinedIcon fontSize={"inherit"}/>
76+
<Typography
77+
fontFamily={"Inter"}
78+
level={"title-sm"}
79+
>
80+
{"Page "}
81+
{pageNum}
82+
</Typography>
83+
</Stack>
84+
<Chip
85+
className={"results-group-summary-count"}
86+
size={"sm"}
87+
variant={"solid"}
88+
>
89+
{results.length}
90+
</Chip>
91+
</Box>
92+
</AccordionSummary>
93+
<AccordionDetails
94+
className={"results-group-details"}
95+
slotProps={{content: {className: "results-group-details-content"}}}
96+
>
97+
<List size={"sm"}>
98+
{results.map((r, index) => (
99+
<Result
100+
key={index}
101+
logEventNum={r.logEventNum}
102+
matchRange={r.matchRange}
103+
message={r.message}/>
104+
))}
105+
</List>
106+
</AccordionDetails>
107+
</Accordion>
108+
);
109+
});
110+
111+
ResultsGroup.displayName = "ResultsGroup";
112+
113+
114+
export default ResultsGroup;

0 commit comments

Comments
 (0)