Skip to content

Commit e167e9c

Browse files
authored
app/vmui: add hotkeys to toggle series on hits chart (#863)
1 parent 52db36a commit e167e9c

File tree

20 files changed

+171
-105
lines changed

20 files changed

+171
-105
lines changed

app/vmui/packages/vmui/eslint.config.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,15 @@ export default [...compat.extends(
8282
"react/jsx-first-prop-new-line": [1, "multiline"],
8383

8484
// Disable core indent rule due to recursion issues in ESLint 9; use JSX-specific rules instead
85-
indent: "off",
85+
indent: ["error", 2, {
86+
SwitchCase: 1,
87+
ignoredNodes: [
88+
"JSXElement",
89+
"JSXElement *",
90+
"JSXFragment",
91+
"JSXFragment *",
92+
],
93+
}],
8694
"react/jsx-indent": ["error", 2],
8795
"react/jsx-indent-props": ["error", 2],
8896

app/vmui/packages/vmui/src/api/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export interface LegendLogHits {
5151

5252
export interface LegendLogHitsMenu {
5353
title: string;
54-
icon?: ReactNode;
54+
iconStart?: ReactNode;
55+
iconEnd?: ReactNode;
56+
shortcut?: string;
5557
handler?: () => void;
5658
}
5759

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsLegend/BarHitsLegendItem.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import Popper from "../../../Main/Popper/Popper";
88
import useBoolean from "../../../../hooks/useBoolean";
99
import LegendHitsMenu from "../LegendHitsMenu/LegendHitsMenu";
1010
import { ExtraFilter } from "../../../../pages/OverviewPage/FiltersBar/types";
11+
import { useCallback } from "react";
12+
import useLegendHitsVisibilityMenu from "./hooks/useLegendHitsVisibilityMenu";
1113

1214
interface Props {
1315
legend: LegendLogHits;
@@ -27,6 +29,7 @@ const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFi
2729
const [clickPosition, setClickPosition] = useState<{ top: number; left: number } | null>(null);
2830

2931
const targetSeries = useMemo(() => series.find(s => s.label === legend.label), [series]);
32+
const isOnlyTargetVisible = series.every(s => s === targetSeries || !s.show);
3033

3134
const fields = useMemo(() => getStreamPairs(legend.label), [legend.label]);
3235

@@ -39,6 +42,47 @@ const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFi
3942
handleOpenContextMenu();
4043
};
4144

45+
const handleVisibilityToggle = useCallback(() => {
46+
if (!targetSeries) return;
47+
targetSeries.show = !targetSeries.show;
48+
onRedrawGraph();
49+
handleCloseContextMenu();
50+
}, [targetSeries, onRedrawGraph, handleCloseContextMenu]);
51+
52+
const handleFocusToggle = useCallback(() => {
53+
series.forEach(s => {
54+
s.show = isOnlyTargetVisible || (s === targetSeries);
55+
});
56+
onRedrawGraph();
57+
handleCloseContextMenu();
58+
}, [series, isOnlyTargetVisible, targetSeries, onRedrawGraph, handleCloseContextMenu]);
59+
60+
const handleClickByLegend = (e: MouseEvent<HTMLDivElement>) => {
61+
const { ctrlKey, metaKey, altKey } = e;
62+
63+
// alt + key // see useLegendHitsVisibilityMenu.tsx
64+
if (altKey) {
65+
handleVisibilityToggle();
66+
return;
67+
}
68+
69+
// cmd/ctrl + click // see useLegendHitsVisibilityMenu.tsx
70+
const ctrlMetaKey = ctrlKey || metaKey;
71+
if (ctrlMetaKey) {
72+
handleFocusToggle();
73+
return;
74+
}
75+
76+
handleContextMenu(e);
77+
};
78+
79+
const optionsVisibilitySection = useLegendHitsVisibilityMenu({
80+
targetSeries,
81+
isOnlyTargetVisible,
82+
handleVisibilityToggle,
83+
handleFocusToggle
84+
});
85+
4286
return (
4387
<div
4488
ref={legendRef}
@@ -48,7 +92,7 @@ const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFi
4892
"vm-bar-hits-legend-item_active": openContextMenu,
4993
"vm-bar-hits-legend-item_hide": !targetSeries?.show,
5094
})}
51-
onClick={handleContextMenu}
95+
onClick={handleClickByLegend}
5296
>
5397
<div
5498
className="vm-bar-hits-legend-item__marker"
@@ -67,9 +111,8 @@ const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFi
67111
<LegendHitsMenu
68112
legend={legend}
69113
fields={fields}
70-
series={series}
114+
optionsVisibilitySection={optionsVisibilitySection}
71115
onApplyFilter={onApplyFilter}
72-
onRedrawGraph={onRedrawGraph}
73116
onClose={handleCloseContextMenu}
74117
/>
75118
</Popper>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { LegendLogHitsMenu } from "../../../../../api/types";
2+
import { useMemo } from "react";
3+
import { FocusIcon, UnfocusIcon, VisibilityIcon, VisibilityOffIcon } from "../../../../Main/Icons";
4+
import { altKeyLabel, ctrlKeyLabel } from "../../../../../utils/keyboard";
5+
import { Series } from "uplot";
6+
7+
type Props = {
8+
targetSeries?: Series;
9+
isOnlyTargetVisible: boolean;
10+
handleVisibilityToggle: () => void;
11+
handleFocusToggle: () => void;
12+
}
13+
14+
const useLegendHitsVisibilityMenu = ({
15+
targetSeries,
16+
isOnlyTargetVisible,
17+
handleVisibilityToggle,
18+
handleFocusToggle
19+
}: Props): LegendLogHitsMenu[] => {
20+
const isShow = Boolean(targetSeries?.show);
21+
22+
return useMemo(() => [
23+
{
24+
title: isShow ? "Hide this series" : "Show this series",
25+
iconStart: isShow ? <VisibilityOffIcon/> : <VisibilityIcon/>,
26+
shortcut: `${altKeyLabel} + Click`, // handled in BarHitsLegendItem.tsx
27+
handler: handleVisibilityToggle,
28+
},
29+
{
30+
title: isOnlyTargetVisible ? "Show all series" : "Show only this series",
31+
iconStart: isOnlyTargetVisible ? <UnfocusIcon/> : <FocusIcon/>,
32+
shortcut: `${ctrlKeyLabel} + Click`, // handled in BarHitsLegendItem.tsx
33+
handler: handleFocusToggle,
34+
},
35+
], [isOnlyTargetVisible, isShow, handleVisibilityToggle, handleFocusToggle]);
36+
};
37+
38+
export default useLegendHitsVisibilityMenu;

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/LegendHitsMenu/LegendHitsMenu.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { FC } from "preact/compat";
2-
import { Series } from "uplot";
32
import "./style.scss";
4-
import { LegendLogHits } from "../../../../api/types";
3+
import { LegendLogHits, LegendLogHitsMenu } from "../../../../api/types";
54
import LegendHitsMenuStats from "./LegendHitsMenuStats";
65
import LegendHitsMenuBase from "./LegendHitsMenuBase";
76
import LegendHitsMenuRow from "./LegendHitsMenuRow";
@@ -10,32 +9,20 @@ import { LOGS_LIMIT_HITS } from "../../../../constants/logs";
109
import LegendHitsMenuVisibility from "./LegendHitsMenuVisibility";
1110
import { ExtraFilter } from "../../../../pages/OverviewPage/FiltersBar/types";
1211

13-
const otherDescription = `aggregated results for fields not in the top ${LOGS_LIMIT_HITS}`;
12+
const otherDescription = `Aggregated results for fields not in the top ${LOGS_LIMIT_HITS}`;
1413

1514
interface Props {
1615
legend: LegendLogHits;
1716
fields: string[];
18-
series: Series[];
17+
optionsVisibilitySection: LegendLogHitsMenu[];
1918
onApplyFilter: (value: ExtraFilter) => void;
20-
onRedrawGraph: () => void;
2119
onClose: () => void;
2220
}
2321

24-
const LegendHitsMenu: FC<Props> = ({ legend, fields, series, onApplyFilter, onRedrawGraph, onClose }) => {
22+
const LegendHitsMenu: FC<Props> = ({ legend, fields, optionsVisibilitySection, onApplyFilter, onClose }) => {
2523
return (
2624
<div className="vm-legend-hits-menu">
27-
{legend.isOther && (
28-
<div className="vm-legend-hits-menu-section vm-legend-hits-menu-section_info">
29-
<LegendHitsMenuRow title={otherDescription}/>
30-
</div>
31-
)}
32-
33-
<LegendHitsMenuVisibility
34-
legend={legend}
35-
series={series}
36-
onRedrawGraph={onRedrawGraph}
37-
onClose={onClose}
38-
/>
25+
<LegendHitsMenuVisibility options={optionsVisibilitySection} />
3926

4027
{!legend.isOther && (
4128
<LegendHitsMenuBase
@@ -54,6 +41,12 @@ const LegendHitsMenu: FC<Props> = ({ legend, fields, series, onApplyFilter, onRe
5441
)}
5542

5643
<LegendHitsMenuStats legend={legend}/>
44+
45+
{legend.isOther && (
46+
<div className="vm-legend-hits-menu-section vm-legend-hits-menu-section_info">
47+
<LegendHitsMenuRow title={otherDescription}/>
48+
</div>
49+
)}
5750
</div>
5851
);
5952
};

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/LegendHitsMenu/LegendHitsMenuBase.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,27 @@ const LegendHitsMenuBase: FC<Props> = ({ legend, onApplyFilter, onClose }) => {
3333
const options: LegendLogHitsMenu[] = [
3434
{
3535
title: `Copy ${groupFieldHits} name`,
36-
icon: <CopyIcon/>,
36+
iconStart: <CopyIcon/>,
3737
handler: handlerCopyLabel,
3838
},
3939
{
4040
title: `Add ${groupFieldHits} to filter`,
41-
icon: <FilterIcon/>,
41+
iconStart: <FilterIcon/>,
4242
handler: handleAddStreamToFilter(ExtraFilterOperator.Equals),
4343
},
4444
{
4545
title: `Exclude ${groupFieldHits} to filter`,
46-
icon: <FilterOffIcon/>,
46+
iconStart: <FilterOffIcon/>,
4747
handler: handleAddStreamToFilter(ExtraFilterOperator.NotEquals),
4848
}
4949
];
5050

5151
return (
5252
<div className="vm-legend-hits-menu-section">
53-
{options.map(({ icon, title, handler }) => (
53+
{options.map(({ ...menuProps }) => (
5454
<LegendHitsMenuRow
55-
key={title}
56-
iconStart={icon}
57-
title={title}
58-
handler={handler}
55+
key={menuProps.title}
56+
{...menuProps}
5957
/>
6058
))}
6159
</div>

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/LegendHitsMenu/LegendHitsMenuFields.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@ const LegendHitsMenuFields: FC<Props> = ({ fields, onApplyFilter, onClose }) =>
4747
return [
4848
{
4949
title: "Copy",
50-
icon: <CopyIcon/>,
50+
iconStart: <CopyIcon/>,
5151
handler: handleCopy(field),
5252
},
5353
{
5454
title: "Add to filter",
55-
icon: <FilterIcon/>,
55+
iconStart: <FilterIcon/>,
5656
handler: handleAddToFilter(field, ExtraFilterOperator.Equals),
5757
},
5858
{
5959
title: "Exclude to filter",
60-
icon: <FilterOffIcon/>,
60+
iconStart: <FilterOffIcon/>,
6161
handler: handleAddToFilter(field, ExtraFilterOperator.NotEquals),
6262
}
6363
];

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/LegendHitsMenu/LegendHitsMenuRow.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import useClickOutside from "../../../../hooks/useClickOutside";
77

88
interface Props {
99
title: string;
10+
shortcut?: string;
1011
handler?: () => void;
1112
iconStart?: ReactNode;
1213
iconEnd?: ReactNode;
1314
className?: string;
1415
submenu?: LegendLogHitsMenu[];
1516
}
1617

17-
const LegendHitsMenuRow: FC<Props> = ({ title, handler, iconStart, iconEnd, className, submenu }) => {
18+
const LegendHitsMenuRow: FC<Props> = ({ title, shortcut, handler, iconStart, iconEnd, className, submenu }) => {
1819
const containerRef = useRef<HTMLDivElement>(null);
1920
const titleRef = useRef<HTMLDivElement>(null);
2021
const submenuRef = useRef<HTMLDivElement>(null);
@@ -79,6 +80,7 @@ const LegendHitsMenuRow: FC<Props> = ({ title, handler, iconStart, iconEnd, clas
7980
{iconStart && <div className="vm-legend-hits-menu-row__icon">{iconStart}</div>}
8081
{isOverflownTitle ? (<Tooltip title={title}>{titleContent}</Tooltip>) : titleContent}
8182
{iconEnd && !hasSubmenu && <div className="vm-legend-hits-menu-row__icon">{iconEnd}</div>}
83+
{shortcut && <div className="vm-legend-hits-menu-row__shortcut">{shortcut}</div>}
8284

8385
{hasSubmenu && (
8486
<div className="vm-legend-hits-menu-row__icon vm-legend-hits-menu-row__icon_drop">
@@ -96,12 +98,10 @@ const LegendHitsMenuRow: FC<Props> = ({ title, handler, iconStart, iconEnd, clas
9698
})}
9799
>
98100
<div className="vm-legend-hits-menu-section">
99-
{submenu.map(({ icon, title, handler }) => (
101+
{submenu.map(({ ...menuProps }) => (
100102
<LegendHitsMenuRow
101-
key={title}
102-
iconStart={icon}
103-
title={title}
104-
handler={handler}
103+
key={menuProps.title}
104+
{...menuProps}
105105
/>
106106
))}
107107
</div>

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/LegendHitsMenu/LegendHitsMenuVisibility.tsx

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,18 @@
1-
import { FC, useCallback } from "preact/compat";
1+
import { FC } from "preact/compat";
22
import LegendHitsMenuRow from "./LegendHitsMenuRow";
3-
import { FocusIcon, UnfocusIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons";
4-
import { LegendLogHits, LegendLogHitsMenu } from "../../../../api/types";
5-
import { Series } from "uplot";
6-
import { useMemo } from "react";
3+
import { LegendLogHitsMenu } from "../../../../api/types";
74

8-
interface Props {
9-
legend: LegendLogHits;
10-
series: Series[];
11-
onRedrawGraph: () => void;
12-
onClose: () => void;
5+
export interface LegendMenuVisibilityProps {
6+
options: LegendLogHitsMenu[]
137
}
148

15-
const LegendHitsMenuVisibility: FC<Props> = ({ legend, series, onRedrawGraph, onClose }) => {
16-
17-
const targetSeries = useMemo(() => series.find(s => s.label === legend.label), [series]);
18-
19-
const isShow = Boolean(targetSeries?.show);
20-
const isOnlyTargetVisible = series.every(s => s === targetSeries || !s.show);
21-
22-
const handleVisibilityToggle = useCallback(() => {
23-
if (!targetSeries) return;
24-
targetSeries.show = !targetSeries.show;
25-
onRedrawGraph();
26-
onClose();
27-
}, [targetSeries, onRedrawGraph, onClose]);
28-
29-
const handleFocusToggle = useCallback(() => {
30-
series.forEach(s => {
31-
s.show = isOnlyTargetVisible || (s === targetSeries);
32-
});
33-
onRedrawGraph();
34-
onClose();
35-
}, [series, isOnlyTargetVisible, targetSeries, onRedrawGraph, onClose]);
36-
37-
const options: LegendLogHitsMenu[] = useMemo(() => [
38-
{
39-
title: isShow ? "Hide series" : "Show series",
40-
icon: isShow ? <VisibilityOffIcon/> : <VisibilityIcon/>,
41-
handler: handleVisibilityToggle,
42-
},
43-
{
44-
title: isOnlyTargetVisible ? "Show all series" : "Focus on series",
45-
icon: isOnlyTargetVisible ? <UnfocusIcon/> : <FocusIcon/>,
46-
handler: handleFocusToggle,
47-
},
48-
], [isOnlyTargetVisible, isShow, handleFocusToggle, handleVisibilityToggle]);
49-
9+
const LegendHitsMenuVisibility: FC<LegendMenuVisibilityProps> = ({ options }) => {
5010
return (
5111
<div className="vm-legend-hits-menu-section">
52-
{options.map(({ icon, title, handler }) => (
12+
{options.map(({ ...menuProps }) => (
5313
<LegendHitsMenuRow
54-
key={title}
55-
iconStart={icon}
56-
title={title}
57-
handler={handler}
14+
key={menuProps.title}
15+
{...menuProps}
5816
/>
5917
))}
6018
</div>

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/LegendHitsMenu/style.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
&_info {
3535
font-size: $font-size-small;
3636
font-weight: 500;
37+
font-style: italic;
3738
}
3839
}
3940

@@ -78,6 +79,12 @@
7879
overflow: hidden;
7980
text-overflow: ellipsis;
8081
}
82+
83+
&__shortcut {
84+
font-size: $font-size-small;
85+
color: $color-text-secondary;
86+
white-space: nowrap;
87+
}
8188
}
8289

8390
&-other-list {

0 commit comments

Comments
 (0)