Skip to content

Commit ae24669

Browse files
authored
new-log-viewer: Add tabbed Sidebar with resizable panel; Rearrange open and settings buttons. (#74)
1 parent dc63e77 commit ae24669

27 files changed

+715
-64
lines changed

new-log-viewer/public/favicon.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.resize-handle {
2+
cursor: ew-resize;
3+
4+
z-index: var(--ylv-resize-handle-z-index);
5+
6+
box-sizing: border-box;
7+
width: var(--ylv-panel-resize-handle-width);
8+
height: 100%;
9+
10+
/* stylelint-disable-next-line custom-property-pattern */
11+
background-color: var(--joy-palette-background-surface, #fbfcfe);
12+
/* stylelint-disable-next-line custom-property-pattern */
13+
border-right: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1);
14+
}
15+
16+
.resize-handle-holding,
17+
.resize-handle:hover {
18+
box-sizing: initial;
19+
20+
/* stylelint-disable-next-line custom-property-pattern */
21+
background-color: var(--joy-palette-primary-solidHoverBg, #0258a8);
22+
border-right: initial;
23+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useState,
5+
} from "react";
6+
7+
import "./ResizeHandle.css";
8+
9+
10+
interface ResizeHandleProps {
11+
onHandleRelease: () => void,
12+
13+
/**
14+
* Gets triggered when a resize event occurs.
15+
*
16+
* @param resizeHandlePosition The horizontal distance, in pixels, between the mouse pointer
17+
* and the left edge of the viewport.
18+
*/
19+
onResize: (resizeHandlePosition: number) => void,
20+
}
21+
22+
/**
23+
* A vertical handle for resizing an object.
24+
*
25+
* @param props
26+
* @param props.onResize The method to call when a resize occurs.
27+
* @param props.onHandleRelease
28+
* @return
29+
*/
30+
const ResizeHandle = ({
31+
onResize,
32+
onHandleRelease,
33+
}: ResizeHandleProps) => {
34+
const [isMouseDown, setIsMouseDown] = useState<boolean>(false);
35+
36+
const handleMouseDown = (ev: React.MouseEvent) => {
37+
ev.preventDefault();
38+
setIsMouseDown(true);
39+
};
40+
41+
const handleMouseMove = useCallback((ev: MouseEvent) => {
42+
ev.preventDefault();
43+
onResize(ev.clientX);
44+
}, [onResize]);
45+
46+
const handleMouseUp = useCallback((ev: MouseEvent) => {
47+
ev.preventDefault();
48+
setIsMouseDown(false);
49+
onHandleRelease();
50+
}, [onHandleRelease]);
51+
52+
// On `isMouseDown` change, add / remove event listeners.
53+
useEffect(() => {
54+
if (isMouseDown) {
55+
window.addEventListener("mousemove", handleMouseMove);
56+
window.addEventListener("mouseup", handleMouseUp);
57+
}
58+
59+
return () => {
60+
// Always clean up the event listeners before the hook is re-run due to `isMouseDown`
61+
// changes / when the component is unmounted.
62+
window.removeEventListener("mousemove", handleMouseMove);
63+
window.removeEventListener("mouseup", handleMouseUp);
64+
};
65+
}, [
66+
handleMouseMove,
67+
handleMouseUp,
68+
isMouseDown,
69+
]);
70+
71+
return (
72+
<div
73+
className={`resize-handle ${isMouseDown ?
74+
"resize-handle-holding" :
75+
""}`}
76+
onMouseDown={handleMouseDown}/>
77+
);
78+
};
79+
80+
81+
export default ResizeHandle;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react";
2+
3+
import {
4+
ListItem,
5+
ListItemContent,
6+
ListItemDecorator,
7+
Typography,
8+
TypographyProps,
9+
} from "@mui/joy";
10+
11+
12+
interface CustomListItemProps {
13+
content: string,
14+
icon: React.ReactNode,
15+
slotProps?: {
16+
content?: TypographyProps
17+
},
18+
title: string
19+
}
20+
21+
/**
22+
* Renders a custom list item with an icon, a title and a context text.
23+
*
24+
* @param props
25+
* @param props.content
26+
* @param props.icon
27+
* @param props.slotProps
28+
* @param props.title
29+
* @return
30+
*/
31+
const CustomListItem = ({content, icon, slotProps, title}: CustomListItemProps) => (
32+
<ListItem>
33+
<ListItemDecorator>
34+
{icon}
35+
</ListItemDecorator>
36+
<ListItemContent>
37+
<Typography level={"title-sm"}>
38+
{title}
39+
</Typography>
40+
<Typography
41+
{...slotProps?.content}
42+
level={"body-sm"}
43+
>
44+
{content}
45+
</Typography>
46+
</ListItemContent>
47+
</ListItem>
48+
);
49+
50+
export default CustomListItem;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.sidebar-tab-panel {
2+
padding: 0.75rem;
3+
}
4+
5+
.sidebar-tab-panel-title-container {
6+
user-select: none;
7+
margin-bottom: 0.5rem !important;
8+
}
9+
10+
.sidebar-tab-panel-title {
11+
font-size: 0.875rem !important;
12+
font-weight: 400 !important;
13+
text-transform: uppercase;
14+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react";
2+
3+
import {
4+
DialogContent,
5+
DialogTitle,
6+
TabPanel,
7+
Typography,
8+
} from "@mui/joy";
9+
10+
import "./CustomTabPanel.css";
11+
12+
13+
interface CustomTabPanelProps {
14+
children: React.ReactNode,
15+
tabName: string,
16+
title: string,
17+
}
18+
19+
/**
20+
* Renders a customized tab panel to be extended for displaying extra information in the sidebar.
21+
*
22+
* @param props
23+
* @param props.children
24+
* @param props.tabName
25+
* @param props.title
26+
* @return
27+
*/
28+
const CustomTabPanel = ({children, tabName, title}: CustomTabPanelProps) => {
29+
return (
30+
<TabPanel
31+
className={"sidebar-tab-panel"}
32+
value={tabName}
33+
>
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>
45+
</TabPanel>
46+
);
47+
};
48+
49+
50+
export default CustomTabPanel;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
useContext,
3+
useMemo,
4+
} from "react";
5+
6+
import {
7+
Divider,
8+
List,
9+
} from "@mui/joy";
10+
11+
import AbcIcon from "@mui/icons-material/Abc";
12+
import StorageIcon from "@mui/icons-material/Storage";
13+
14+
import {StateContext} from "../../../../contexts/StateContextProvider";
15+
import {
16+
TAB_DISPLAY_NAMES,
17+
TAB_NAME,
18+
} from "../../../../typings/tab";
19+
import {formatSizeInBytes} from "../../../../utils/units";
20+
import CustomListItem from "./CustomListItem";
21+
import CustomTabPanel from "./CustomTabPanel";
22+
23+
24+
/**
25+
* Displays a panel containing the file name and on-disk size of the selected file.
26+
*
27+
* @return
28+
*/
29+
const FileInfoTabPanel = () => {
30+
const {fileName, onDiskFileSizeInBytes} = useContext(StateContext);
31+
32+
const isFileUnloaded = 0 === fileName.length;
33+
const formattedOnDiskSize = useMemo(
34+
() => formatSizeInBytes(onDiskFileSizeInBytes, false),
35+
[onDiskFileSizeInBytes]
36+
);
37+
38+
return (
39+
<CustomTabPanel
40+
tabName={TAB_NAME.FILE_INFO}
41+
title={TAB_DISPLAY_NAMES[TAB_NAME.FILE_INFO]}
42+
>
43+
{isFileUnloaded ?
44+
"No file is open." :
45+
<List>
46+
<CustomListItem
47+
content={fileName}
48+
icon={<AbcIcon/>}
49+
slotProps={{content: {sx: {wordBreak: "break-word"}}}}
50+
title={"Name"}/>
51+
<Divider/>
52+
<CustomListItem
53+
content={formattedOnDiskSize}
54+
icon={<StorageIcon/>}
55+
title={"On-disk Size"}/>
56+
</List>}
57+
</CustomTabPanel>
58+
);
59+
};
60+
61+
export default FileInfoTabPanel;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.sidebar-tab-button {
2+
justify-content: center !important;
3+
width: 48px;
4+
height: 48px;
5+
padding: 0 !important;
6+
}
7+
8+
.sidebar-tab-button-icon {
9+
font-size: 32px !important;
10+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
Tab,
3+
Tooltip,
4+
} from "@mui/joy";
5+
6+
import {SvgIconComponent} from "@mui/icons-material";
7+
8+
import {
9+
TAB_DISPLAY_NAMES,
10+
TAB_NAME,
11+
} from "../../../../typings/tab";
12+
13+
import "./TabButton.css";
14+
15+
16+
interface TabButtonProps {
17+
tabName: TAB_NAME,
18+
Icon: SvgIconComponent,
19+
onTabButtonClick: (tabName: TAB_NAME) => void
20+
}
21+
22+
/**
23+
* Renders a tooltip-wrapped tab button.
24+
*
25+
* @param props
26+
* @param props.tabName
27+
* @param props.Icon
28+
* @param props.onTabButtonClick
29+
* @return
30+
*/
31+
const TabButton = ({tabName, Icon, onTabButtonClick}: TabButtonProps) => {
32+
const handleClick = () => {
33+
onTabButtonClick(tabName);
34+
};
35+
36+
return (
37+
<Tooltip
38+
arrow={true}
39+
key={tabName}
40+
placement={"right"}
41+
title={TAB_DISPLAY_NAMES[tabName]}
42+
variant={"outlined"}
43+
>
44+
<Tab
45+
className={"sidebar-tab-button"}
46+
color={"neutral"}
47+
indicatorPlacement={"left"}
48+
slotProps={{root: {onClick: handleClick}}}
49+
value={tabName}
50+
>
51+
<Icon className={"sidebar-tab-button-icon"}/>
52+
</Tab>
53+
</Tooltip>
54+
);
55+
};
56+
57+
export default TabButton;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.sidebar-tabs {
2+
flex-grow: 1;
3+
width: calc(100% - var(--ylv-panel-resize-handle-width));
4+
height: 100%;
5+
}
6+
7+
.sidebar-tab-list-spacing {
8+
flex-grow: 1;
9+
}

0 commit comments

Comments
 (0)