vmui: refactor code using custom hooks (#4145)

* refactor: replace boolean useState with useBoolean

* refactor: replace useResize with useWindowSize/useElementSize

* refactor: replace addEventListener with useEventListener

* refactor: replace navigator.clipboard.writeText with useCopyToClipboard

* fix: prevent redirect loop
This commit is contained in:
Yury Molodov 2023-05-16 15:41:06 +02:00 committed by Aliaksandr Valialkin
parent afd03f87fe
commit 5c2ed85eb9
No known key found for this signature in database
GPG Key ID: A72BEC6CD3D0DED1
54 changed files with 1572 additions and 1361 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,17 @@
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import uPlot, { Options as uPlotOptions } from "uplot";
import useResize from "../../../hooks/useResize";
import { BarChartProps } from "./types";
import "./style.scss";
import { useAppState } from "../../../state/common/StateContext";
const BarChart: FC<BarChartProps> = ({
data,
container,
layoutSize,
configs }) => {
const { isDarkTheme } = useAppState();
const uPlotRef = useRef<HTMLDivElement>(null);
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const layoutSize = useResize(container);
const options: uPlotOptions ={
...configs,

View File

@ -1,7 +1,8 @@
import { AlignedData as uPlotData, Options as uPlotOptions } from "uplot";
import { ElementSize } from "../../../hooks/useElementSize";
export interface BarChartProps {
data: uPlotData;
container: HTMLDivElement | null,
layoutSize: ElementSize,
configs: uPlotOptions,
}

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
import uPlot from "uplot";
import ReactDOM from "react-dom";
import Button from "../../../Main/Button/Button";
@ -6,6 +6,7 @@ import { CloseIcon, DragIcon } from "../../../Main/Icons";
import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import "../../Line/ChartTooltip/style.scss";
import useEventListener from "../../../../hooks/useEventListener";
export interface TooltipHeatmapProps {
cursor: {left: number, top: number}
@ -45,24 +46,22 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
const [moving, setMoving] = useState(false);
const [moved, setMoved] = useState(false);
const targetPortal = useMemo(() => u.root.querySelector(".u-wrap"), [u]);
const handleClose = () => {
onClose && onClose(id);
};
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
const handleMouseDown = (e: ReactMouseEvent) => {
setMoved(true);
setMoving(true);
const { clientX, clientY } = e;
setPosition({ top: clientY, left: clientX });
};
const handleMouseMove = (e: MouseEvent) => {
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!moving) return;
const { clientX, clientY } = e;
setPosition({ top: clientY, left: clientX });
};
}, [moving]);
const handleMouseUp = () => {
setMoving(false);
@ -88,19 +87,10 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
useEffect(calcPosition, [u, cursor, tooltipOffset, tooltipRef]);
useEffect(() => {
if (moving) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
useEventListener("mousemove", handleMouseMove);
useEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [moving]);
if (!targetPortal || !cursor.left || !cursor.top || !value) return null;
if (!cursor?.left || !cursor?.top || !value) return null;
return ReactDOM.createPortal((
<div
@ -146,7 +136,7 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
{bucket}
</div>
</div>
), targetPortal);
), u.root);
};
export default ChartTooltipHeatmap;

View File

@ -10,7 +10,6 @@ import { getAxes } from "../../../../utils/uplot/axes";
import { MetricResult } from "../../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
import throttle from "lodash.throttle";
import useResize from "../../../../hooks/useResize";
import { TimeParams } from "../../../../types";
import { YaxisState } from "../../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
@ -23,6 +22,8 @@ import ChartTooltipHeatmap, {
ChartTooltipHeatmapProps,
TooltipHeatmapProps
} from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
import { ElementSize } from "../../../../hooks/useElementSize";
import useEventListener from "../../../../hooks/useEventListener";
export interface HeatmapChartProps {
metrics: MetricResult[];
@ -31,7 +32,7 @@ export interface HeatmapChartProps {
yaxis: YaxisState;
unit?: string;
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
container: HTMLDivElement | null;
layoutSize: ElementSize,
height?: number;
onChangeLegend: (val: TooltipHeatmapProps) => void;
}
@ -45,7 +46,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
yaxis,
unit,
setPeriod,
container,
layoutSize,
height,
onChangeLegend,
}) => {
@ -56,7 +57,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const [startTouchDistance, setStartTouchDistance] = useState(0);
const layoutSize = useResize(container);
const [tooltipProps, setTooltipProps] = useState<TooltipHeatmapProps | null>(null);
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
@ -116,7 +116,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
});
};
const handleKeyDown = (e: KeyboardEvent) => {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const { target, ctrlKey, metaKey, key } = e;
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
if (!uPlotInst || isInput) return;
@ -131,10 +131,10 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
max: xRange.max - factor
});
}
};
}, [uPlotInst, xRange]);
const handleClick = () => {
if (!tooltipProps) return;
const handleClick = useCallback(() => {
if (!tooltipProps?.value) return;
const id = `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
const props = {
id,
@ -147,13 +147,12 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
const res = JSON.parse(JSON.stringify(props));
setStickyToolTips(prev => [...prev, res]);
}
};
}, [stickyTooltips, tooltipProps, tooltipOffset, unit]);
const handleUnStick = (id:string) => {
const handleUnStick = (id: string) => {
setStickyToolTips(prev => prev.filter(t => t.id !== id));
};
const setCursor = (u: uPlot) => {
const left = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
const top = u.cursor.top && u.cursor.top > 0 ? u.cursor.top : 0;
@ -263,21 +262,14 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
useEffect(() => {
setStickyToolTips([]);
setTooltipProps(null);
if (!uPlotRef.current || !layoutSize.width || !layoutSize.height) return;
const isValidData = data[0] === null && Array.isArray(data[1]);
if (!uPlotRef.current || !layoutSize.width || !layoutSize.height || !isValidData) return;
const u = new uPlot(options, data, uPlotRef.current);
setUPlotInst(u);
setXRange({ min: period.start, max: period.end });
return u.destroy;
}, [uPlotRef.current, layoutSize, height, isDarkTheme, data]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [xRange]);
const handleTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 2) return;
e.preventDefault();
@ -287,7 +279,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
};
const handleTouchMove = (e: TouchEvent) => {
const handleTouchMove = useCallback((e: TouchEvent) => {
if (e.touches.length !== 2 || !uPlotInst) return;
e.preventDefault();
@ -307,34 +299,20 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
min: min + zoomFactor,
max: max - zoomFactor
}));
};
useEffect(() => {
window.addEventListener("touchmove", handleTouchMove);
window.addEventListener("touchstart", handleTouchStart);
return () => {
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchstart", handleTouchStart);
};
}, [uPlotInst, startTouchDistance]);
}, [uPlotInst, startTouchDistance, xRange]);
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
useEffect(() => {
const show = !!tooltipProps?.value;
if (show) window.addEventListener("click", handleClick);
return () => {
window.removeEventListener("click", handleClick);
};
}, [tooltipProps, stickyTooltips]);
useEffect(() => {
if (tooltipProps) onChangeLegend(tooltipProps);
}, [tooltipProps]);
useEventListener("click", handleClick);
useEventListener("keydown", handleKeyDown);
useEventListener("touchmove", handleTouchMove);
useEventListener("touchstart", handleTouchStart);
return (
<div
className={classNames({

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot from "uplot";
import { MetricResult } from "../../../../api/types";
import { formatPrettyNumber } from "../../../../utils/uplot/helpers";
@ -12,6 +12,7 @@ import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import "./style.scss";
import { SeriesItem } from "../../../../utils/uplot/series";
import useEventListener from "../../../../hooks/useEventListener";
export interface ChartTooltipProps {
id: string,
@ -47,8 +48,6 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
const [seriesIdx, setSeriesIdx] = useState(tooltipIdx.seriesIdx);
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
const targetPortal = useMemo(() => u.root.querySelector(".u-wrap"), [u]);
const value = get(u, ["data", seriesIdx, dataIdx], 0);
const valueFormat = formatPrettyNumber(value, get(yRange, [0]), get(yRange, [1]));
const dataTime = u.data[0][dataIdx];
@ -85,11 +84,11 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
setPosition({ top: clientY, left: clientX });
};
const handleMouseMove = (e: MouseEvent) => {
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!moving) return;
const { clientX, clientY } = e;
setPosition({ top: clientY, left: clientX });
};
}, [moving]);
const handleMouseUp = () => {
setMoving(false);
@ -125,19 +124,10 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
setDataIdx(tooltipIdx.dataIdx);
}, [tooltipIdx]);
useEffect(() => {
if (moving) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
useEventListener("mousemove", handleMouseMove);
useEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [moving]);
if (!targetPortal || tooltipIdx.seriesIdx < 0 || tooltipIdx.dataIdx < 0) return null;
if (tooltipIdx.seriesIdx < 0 || tooltipIdx.dataIdx < 0) return null;
return ReactDOM.createPortal((
<div
@ -190,7 +180,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
{fullMetricName}
</div>
</div>
), targetPortal);
), u.root);
};
export default ChartTooltip;

View File

@ -5,6 +5,7 @@ import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../../Main/Tooltip/Tooltip";
import { getFreeFields } from "./helpers";
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
interface LegendItemProps {
legend: LegendItemType;
@ -13,16 +14,20 @@ interface LegendItemProps {
}
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
const copyToClipboard = useCopyToClipboard();
const [copiedValue, setCopiedValue] = useState("");
const freeFormFields = useMemo(() => {
const result = getFreeFields(legend);
return isHeatmap ? result.filter(f => f.key !== "vmrange") : result;
}, [legend, isHeatmap]);
const calculations = legend.calculations;
const showCalculations = Object.values(calculations).some(v => v);
const handleClickFreeField = async (val: string, id: string) => {
await navigator.clipboard.writeText(val);
const copied = await copyToClipboard(val);
if (!copied) return;
setCopiedValue(id);
setTimeout(() => setCopiedValue(""), 2000);
};

View File

@ -13,7 +13,6 @@ import { getAxes, getMinMaxBuffer } from "../../../../utils/uplot/axes";
import { MetricResult } from "../../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
import throttle from "lodash.throttle";
import useResize from "../../../../hooks/useResize";
import { TimeParams } from "../../../../types";
import { YaxisState } from "../../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
@ -23,6 +22,8 @@ import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
import dayjs from "dayjs";
import { useAppState } from "../../../../state/common/StateContext";
import { SeriesItem } from "../../../../utils/uplot/series";
import { ElementSize } from "../../../../hooks/useElementSize";
import useEventListener from "../../../../hooks/useEventListener";
export interface LineChartProps {
metrics: MetricResult[];
@ -32,7 +33,7 @@ export interface LineChartProps {
series: uPlotSeries[];
unit?: string;
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
container: HTMLDivElement | null;
layoutSize: ElementSize;
height?: number;
}
@ -46,7 +47,7 @@ const LineChart: FC<LineChartProps> = ({
yaxis,
unit,
setPeriod,
container,
layoutSize,
height
}) => {
const { isDarkTheme } = useAppState();
@ -57,7 +58,6 @@ const LineChart: FC<LineChartProps> = ({
const [yRange, setYRange] = useState([0, 1]);
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const [startTouchDistance, setStartTouchDistance] = useState(0);
const layoutSize = useResize(container);
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
@ -115,7 +115,7 @@ const LineChart: FC<LineChartProps> = ({
});
};
const handleKeyDown = (e: KeyboardEvent) => {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const { target, ctrlKey, metaKey, key } = e;
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
if (!uPlotInst || isInput) return;
@ -130,9 +130,10 @@ const LineChart: FC<LineChartProps> = ({
max: xRange.max - factor
});
}
};
}, [uPlotInst, xRange]);
const handleClick = () => {
const handleClick = useCallback(() => {
if (!showTooltip) return;
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
const props = {
id,
@ -148,7 +149,7 @@ const LineChart: FC<LineChartProps> = ({
const tooltipProps = JSON.parse(JSON.stringify(props));
setStickyToolTips(prev => [...prev, tooltipProps]);
}
};
}, [metrics, series, stickyTooltips, tooltipIdx, tooltipOffset, showTooltip, unit, yRange]);
const handleUnStick = (id:string) => {
setStickyToolTips(prev => prev.filter(t => t.id !== id));
@ -231,14 +232,6 @@ const LineChart: FC<LineChartProps> = ({
return u.destroy;
}, [uPlotRef.current, series, layoutSize, height, isDarkTheme]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [xRange]);
const handleTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 2) return;
e.preventDefault();
@ -248,7 +241,7 @@ const LineChart: FC<LineChartProps> = ({
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
};
const handleTouchMove = (e: TouchEvent) => {
const handleTouchMove = useCallback((e: TouchEvent) => {
if (e.touches.length !== 2 || !uPlotInst) return;
e.preventDefault();
@ -268,17 +261,7 @@ const LineChart: FC<LineChartProps> = ({
min: min + zoomFactor,
max: max - zoomFactor
}));
};
useEffect(() => {
window.addEventListener("touchmove", handleTouchMove);
window.addEventListener("touchstart", handleTouchStart);
return () => {
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchstart", handleTouchStart);
};
}, [uPlotInst, startTouchDistance]);
}, [uPlotInst, startTouchDistance, xRange]);
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
@ -286,14 +269,13 @@ const LineChart: FC<LineChartProps> = ({
useEffect(() => {
const show = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
setShowTooltip(show);
if (show) window.addEventListener("click", handleClick);
return () => {
window.removeEventListener("click", handleClick);
};
}, [tooltipIdx, stickyTooltips]);
useEventListener("click", handleClick);
useEventListener("keydown", handleKeyDown);
useEventListener("touchmove", handleTouchMove);
useEventListener("touchstart", handleTouchStart);
return (
<div
className={classNames({

View File

@ -7,13 +7,13 @@ $color-bar-highest: #F79420;
display: grid;
grid-template-columns: auto 1fr;
height: 100%;
padding-bottom: #{$font-size-small/2};
padding-bottom: calc($font-size-small/2);
overflow: hidden;
&-y-axis {
position: relative;
display: grid;
transform: translateY(#{$font-size-small});
transform: translateY($font-size-small);
&__tick {
position: relative;

View File

@ -1,4 +1,4 @@
import React, { FC, useRef, useState } from "preact/compat";
import React, { FC, useRef } from "preact/compat";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import "./style.scss";
@ -8,6 +8,7 @@ import Popper from "../../Main/Popper/Popper";
import { TuneIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import classNames from "classnames";
import useBoolean from "../../../hooks/useBoolean";
const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
const { autocomplete } = useQueryState();
@ -59,16 +60,13 @@ const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => {
const AdditionalSettings: FC = () => {
const { isMobile } = useDeviceDetect();
const [openList, setOpenList] = useState(false);
const targetRef = useRef<HTMLDivElement>(null);
const handleToggleList = () => {
setOpenList(prev => !prev);
};
const handleCloseList = () => {
setOpenList(false);
};
const {
value: openList,
toggle: handleToggleList,
setFalse: handleCloseList,
} = useBoolean(false);
if (isMobile) {
return (

View File

@ -15,6 +15,7 @@ import Timezones from "./Timezones/Timezones";
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
import ThemeControl from "../ThemeControl/ThemeControl";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean";
const title = "Settings";
@ -40,15 +41,14 @@ const GlobalSettings: FC = () => {
setTimezone(stateTimezone);
};
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => {
setOpen(false);
setDefaultsValues();
};
const {
value: open,
setTrue: handleOpen,
setFalse: handleClose,
} = useBoolean(false);
const handleCloseForce = () => {
setOpen(false);
const handleCloseAndReset = () => {
handleClose();
setDefaultsValues();
};
@ -60,7 +60,7 @@ const GlobalSettings: FC = () => {
dispatch({ type: "SET_SERVER", payload: serverUrl });
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
setOpen(false);
handleClose();
};
useEffect(() => {
@ -97,7 +97,7 @@ const GlobalSettings: FC = () => {
{open && (
<Modal
title={title}
onClose={handleClose}
onClose={handleCloseAndReset}
>
<div
className={classNames({
@ -140,7 +140,7 @@ const GlobalSettings: FC = () => {
<Button
color="error"
variant="outlined"
onClick={handleCloseForce}
onClick={handleCloseAndReset}
>
Cancel
</Button>

View File

@ -11,6 +11,7 @@ import Tooltip from "../../../Main/Tooltip/Tooltip";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import TextField from "../../../Main/TextField/TextField";
import { getTenantIdFromUrl, replaceTenantId } from "../../../../utils/tenants";
import useBoolean from "../../../../hooks/useBoolean";
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
const appModeEnable = getAppModeEnable();
@ -21,9 +22,14 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
const timeDispatch = useTimeDispatch();
const [search, setSearch] = useState("");
const [openOptions, setOpenOptions] = useState(false);
const optionsButtonRef = useRef<HTMLDivElement>(null);
const {
value: openOptions,
toggle: toggleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const accountIdsFiltered = useMemo(() => {
if (!search) return accountIds;
try {
@ -37,14 +43,6 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
const showTenantSelector = useMemo(() => accountIds.length > 1, [accountIds]);
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
const createHandlerChange = (value: string) => () => {
const tenant = value;
dispatch({ type: "SET_TENANT_ID", payload: tenant });

View File

@ -9,6 +9,7 @@ import TextField from "../../../Main/TextField/TextField";
import { Timezone } from "../../../../types";
import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
interface TimezonesProps {
timezoneState: string
@ -19,10 +20,15 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
const { isMobile } = useDeviceDetect();
const timezones = getTimezoneList();
const [openList, setOpenList] = useState(false);
const [search, setSearch] = useState("");
const targetRef = useRef<HTMLDivElement>(null);
const {
value: openList,
toggle: toggleOpenList,
setFalse: handleCloseList,
} = useBoolean(false);
const searchTimezones = useMemo(() => {
if (!search) return timezones;
try {
@ -44,14 +50,6 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
utc: getUTCByTimezone(timezoneState)
}), [timezoneState]);
const toggleOpenList = () => {
setOpenList(prev => !prev);
};
const handleCloseList = () => {
setOpenList(false);
};
const handleChangeSearch = (val: string) => {
setSearch(val);
};

View File

@ -1,4 +1,4 @@
import React, { FC, useRef, useState } from "preact/compat";
import React, { FC, useRef } from "preact/compat";
import AxesLimitsConfigurator from "./AxesLimitsConfigurator/AxesLimitsConfigurator";
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
import { SettingsIcon } from "../../Main/Icons";
@ -6,6 +6,7 @@ import Button from "../../Main/Button/Button";
import Popper from "../../Main/Popper/Popper";
import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip";
import useBoolean from "../../../hooks/useBoolean";
const title = "Axes settings";
@ -17,16 +18,13 @@ interface GraphSettingsProps {
const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits }) => {
const popperRef = useRef<HTMLDivElement>(null);
const [openPopper, setOpenPopper] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
const toggleOpen = () => {
setOpenPopper(prev => !prev);
};
const handleClose = () => {
setOpenPopper(false);
};
const {
value: openPopper,
toggle: toggleOpen,
setFalse: handleClose,
} = useBoolean(false);
return (
<div className="vm-graph-settings">

View File

@ -13,6 +13,7 @@ import { getAppModeEnable } from "../../../utils/app-mode";
import Popper from "../../Main/Popper/Popper";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
import useBoolean from "../../../hooks/useBoolean";
const StepConfigurator: FC = () => {
const appModeEnable = getAppModeEnable();
@ -28,20 +29,17 @@ const StepConfigurator: FC = () => {
return getStepFromDuration(end - start, isHistogram);
}, [step, isHistogram]);
const [openOptions, setOpenOptions] = useState(false);
const [customStep, setCustomStep] = useState(value || defaultStep);
const [error, setError] = useState("");
const {
value: openOptions,
toggle: toggleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const buttonRef = useRef<HTMLDivElement>(null);
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
const handleApply = (value?: string) => {
const step = value || customStep || defaultStep || "1s";
const durations = step.match(/[a-zA-Z]+/g) || [];

View File

@ -8,6 +8,7 @@ import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
interface AutoRefreshOption {
seconds: number
@ -38,12 +39,19 @@ export const ExecutionControls: FC = () => {
const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
const {
value: openOptions,
toggle: toggleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const optionsButtonRef = useRef<HTMLDivElement>(null);
const handleChange = (d: AutoRefreshOption) => {
if ((autoRefresh && !d.seconds) || (!autoRefresh && d.seconds)) {
setAutoRefresh(prev => !prev);
}
setSelectedDelay(d);
setOpenOptions(false);
handleCloseOptions();
};
const handleUpdate = () => {
@ -65,17 +73,6 @@ export const ExecutionControls: FC = () => {
};
}, [selectedDelay, autoRefresh]);
const [openOptions, setOpenOptions] = useState(false);
const optionsButtonRef = useRef<HTMLDivElement>(null);
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
const createHandlerChange = (d: AutoRefreshOption) => () => {
handleChange(d);
};

View File

@ -9,20 +9,21 @@ import Button from "../../../Main/Button/Button";
import Popper from "../../../Main/Popper/Popper";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { DATE_TIME_FORMAT } from "../../../../constants/date";
import useResize from "../../../../hooks/useResize";
import "./style.scss";
import useClickOutside from "../../../../hooks/useClickOutside";
import classNames from "classnames";
import { useAppState } from "../../../../state/common/StateContext";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import DateTimeInput from "../../../Main/DatePicker/DateTimeInput/DateTimeInput";
import useBoolean from "../../../../hooks/useBoolean";
import useWindowSize from "../../../../hooks/useWindowSize";
export const TimeSelector: FC = () => {
const { isMobile } = useDeviceDetect();
const { isDarkTheme } = useAppState();
const wrapperRef = useRef<HTMLDivElement>(null);
const documentSize = useResize(document.body);
const displayFullDate = useMemo(() => documentSize.width > 1280, [documentSize]);
const documentSize = useWindowSize();
const displayFullDate = useMemo(() => documentSize.width > 1120, [documentSize]);
const [until, setUntil] = useState<string>();
const [from, setFrom] = useState<string>();
@ -31,6 +32,12 @@ export const TimeSelector: FC = () => {
const dispatch = useTimeDispatch();
const appModeEnable = getAppModeEnable();
const {
value: openOptions,
toggle: toggleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const activeTimezone = useMemo(() => ({
region: timezone,
utc: getUTCByTimezone(timezone)
@ -46,7 +53,7 @@ export const TimeSelector: FC = () => {
const setDuration = ({ duration, until, id }: {duration: string, until: Date, id: string}) => {
dispatch({ type: "SET_RELATIVE_TIME", payload: { duration, until, id } });
setOpenOptions(false);
handleCloseOptions();
};
const formatRange = useMemo(() => {
@ -62,7 +69,6 @@ export const TimeSelector: FC = () => {
const fromPickerRef = useRef<HTMLDivElement>(null);
const untilPickerRef = useRef<HTMLDivElement>(null);
const [openOptions, setOpenOptions] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
const setTimeAndClosePicker = () => {
@ -72,7 +78,7 @@ export const TimeSelector: FC = () => {
to: dayjs.tz(until).toDate()
} });
}
setOpenOptions(false);
handleCloseOptions();
};
const onSwitchToNow = () => dispatch({ type: "RUN_QUERY_TO_NOW" });
@ -80,15 +86,7 @@ export const TimeSelector: FC = () => {
const onCancelClick = () => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
setOpenOptions(false);
};
const toggleOpenOptions = () => {
setOpenOptions(prev => !prev);
};
const handleCloseOptions = () => {
setOpenOptions(false);
handleCloseOptions();
};
useEffect(() => {

View File

@ -11,6 +11,7 @@ import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { getDurationFromMilliseconds, getSecondsFromDuration, getStepFromDuration } from "../../../utils/time";
import useBoolean from "../../../hooks/useBoolean";
interface ExploreMetricItemGraphProps {
name: string,
@ -41,7 +42,10 @@ const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
const [isHeatmap, setIsHeatmap] = useState(false);
const step = isHeatmap && customStep === defaultStep ? heatmapStep : customStep;
const [showAllSeries, setShowAllSeries] = useState(false);
const {
value: showAllSeries,
setTrue: handleShowAll,
} = useBoolean(false);
const query = useMemo(() => {
const params = Object.entries({ job, instance })
@ -81,10 +85,6 @@ with (q = ${queryBase}) (
timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
};
const handleShowAll = () => {
setShowAllSeries(true);
};
useEffect(() => {
setIsHeatmap(isHistogram);
}, [isHistogram]);

View File

@ -2,8 +2,8 @@ import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import ExploreMetricItemGraph from "../ExploreMetricGraph/ExploreMetricItemGraph";
import ExploreMetricItemHeader from "../ExploreMetricItemHeader/ExploreMetricItemHeader";
import "./style.scss";
import useResize from "../../../hooks/useResize";
import { GraphSize } from "../../../types";
import useWindowSize from "../../../hooks/useWindowSize";
interface ExploreMetricItemProps {
name: string
@ -32,7 +32,7 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
const [rateEnabled, setRateEnabled] = useState(isCounter);
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const graphHeight = useMemo(size.height, [size, windowSize]);
useEffect(() => {

View File

@ -1,4 +1,4 @@
import React, { FC, useState } from "preact/compat";
import React, { FC } from "preact/compat";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import Tooltip from "../../Main/Tooltip/Tooltip";
@ -6,6 +6,7 @@ import Button from "../../Main/Button/Button";
import { ArrowDownIcon, CloseIcon, MinusIcon, MoreIcon, PlusIcon } from "../../Main/Icons";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Modal from "../../Main/Modal/Modal";
import useBoolean from "../../../hooks/useBoolean";
interface ExploreMetricItemControlsProps {
name: string
@ -30,7 +31,12 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
onChangeOrder,
}) => {
const { isMobile } = useDeviceDetect();
const [openOptions, setOpenOptions] = useState(false);
const {
value: openOptions,
setTrue: handleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const handleClickRemove = () => {
onRemoveItem(name);
@ -44,14 +50,6 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
onChangeOrder(name, index, index - 1);
};
const handleOpenOptions = () => {
setOpenOptions(true);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
if (isMobile) {
return (
<div className="vm-explore-metrics-item-header vm-explore-metrics-item-header_mobile">

View File

@ -8,15 +8,15 @@ import "./style.scss";
import classNames from "classnames";
import { useAppState } from "../../../state/common/StateContext";
import HeaderNav from "./HeaderNav/HeaderNav";
import useResize from "../../../hooks/useResize";
import SidebarHeader from "./SidebarNav/SidebarHeader";
import HeaderControls from "./HeaderControls/HeaderControls";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useWindowSize from "../../../hooks/useWindowSize";
const Header: FC = () => {
const { isMobile } = useDeviceDetect();
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
const { isDarkTheme } = useAppState();

View File

@ -1,4 +1,4 @@
import React, { FC, useMemo, useState } from "preact/compat";
import React, { FC, useMemo } from "preact/compat";
import { RouterOptions, routerOptions, RouterOptionsHeader } from "../../../../router";
import TenantsConfiguration from "../../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
import StepConfigurator from "../../../Configurators/StepConfigurator/StepConfigurator";
@ -15,6 +15,7 @@ import "./style.scss";
import classNames from "classnames";
import { getAppModeEnable } from "../../../../utils/app-mode";
import Modal from "../../../Main/Modal/Modal";
import useBoolean from "../../../../hooks/useBoolean";
interface HeaderControlsProp {
displaySidebar: boolean
@ -50,22 +51,19 @@ const Controls: FC<HeaderControlsProp> = ({
const HeaderControls: FC<HeaderControlsProp> = (props) => {
const appModeEnable = getAppModeEnable();
const [openList, setOpenList] = useState(false);
const { pathname } = useLocation();
const { accountIds } = useFetchAccountIds();
const {
value: openList,
toggle: handleToggleList,
setFalse: handleCloseList,
} = useBoolean(false);
const headerSetup = useMemo(() => {
return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
}, [pathname]);
const handleToggleList = () => {
setOpenList(prev => !prev);
};
const handleCloseList = () => {
setOpenList(false);
};
if (props.isMobile) {
return (
<>

View File

@ -5,6 +5,7 @@ import { ArrowDropDownIcon } from "../../../Main/Icons";
import Popper from "../../../Main/Popper/Popper";
import NavItem from "./NavItem";
import { useEffect } from "react";
import useBoolean from "../../../../hooks/useBoolean";
interface NavItemProps {
activeMenu: string,
@ -25,17 +26,18 @@ const NavSubItem: FC<NavItemProps> = ({
}) => {
const { pathname } = useLocation();
const [openSubmenu, setOpenSubmenu] = useState(false);
const [menuTimeout, setMenuTimeout] = useState<NodeJS.Timeout | null>(null);
const buttonRef = useRef<HTMLDivElement>(null);
const handleOpenSubmenu = () => {
setOpenSubmenu(true);
if (menuTimeout) clearTimeout(menuTimeout);
};
const {
value: openSubmenu,
setFalse: handleCloseSubmenu,
setTrue: setOpenSubmenu,
} = useBoolean(false);
const handleCloseSubmenu = () => {
setOpenSubmenu(false);
const handleOpenSubmenu = () => {
setOpenSubmenu();
if (menuTimeout) clearTimeout(menuTimeout);
};
const handleMouseLeave = () => {

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import React, { FC, useEffect, useRef } from "preact/compat";
import { useLocation } from "react-router-dom";
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
import classNames from "classnames";
@ -7,6 +7,7 @@ import useClickOutside from "../../../../hooks/useClickOutside";
import MenuBurger from "../../../Main/MenuBurger/MenuBurger";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import "./style.scss";
import useBoolean from "../../../../hooks/useBoolean";
interface SidebarHeaderProps {
background: string
@ -21,15 +22,12 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
const { isMobile } = useDeviceDetect();
const sidebarRef = useRef<HTMLDivElement>(null);
const [openMenu, setOpenMenu] = useState(false);
const handleToggleMenu = () => {
setOpenMenu(prev => !prev);
};
const handleCloseMenu = () => {
setOpenMenu(false);
};
const {
value: openMenu,
toggle: handleToggleMenu,
setFalse: handleCloseMenu,
} = useBoolean(false);
useEffect(handleCloseMenu, [pathname]);

View File

@ -26,16 +26,15 @@ const Layout: FC = () => {
// for support old links with search params
const redirectSearchToHashParams = () => {
const { search } = window.location;
const { search, href } = window.location;
if (search) {
const query = qs.parse(search, { ignoreQueryPrefix: true });
Object.entries(query).forEach(([key, value]) => {
searchParams.set(key, value as string);
setSearchParams(searchParams);
});
Object.entries(query).forEach(([key, value]) => searchParams.set(key, value as string));
setSearchParams(searchParams);
window.location.search = "";
}
window.location.replace(window.location.href.replace(/\/\?#\//, "/#/"));
const newHref = href.replace(/\/\?#\//, "/#/");
if (newHref !== href) window.location.replace(newHref);
};
useEffect(setDocumentTitle, [pathname]);

View File

@ -1,9 +1,11 @@
import React, { FC, Ref, useEffect, useMemo, useRef, useState } from "preact/compat";
import React, { FC, Ref, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import classNames from "classnames";
import Popper from "../Popper/Popper";
import "./style.scss";
import { DoneIcon } from "../Icons";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener";
interface AutocompleteProps {
value: string
@ -39,9 +41,14 @@ const Autocomplete: FC<AutocompleteProps> = ({
const { isMobile } = useDeviceDetect();
const wrapperEl = useRef<HTMLDivElement>(null);
const [openAutocomplete, setOpenAutocomplete] = useState(false);
const [focusOption, setFocusOption] = useState(-1);
const {
value: openAutocomplete,
setValue: setOpenAutocomplete,
setFalse: handleCloseAutocomplete,
} = useBoolean(false);
const foundOptions = useMemo(() => {
if (!openAutocomplete) return [];
try {
@ -57,10 +64,6 @@ const Autocomplete: FC<AutocompleteProps> = ({
return noOptionsText && !foundOptions.length;
}, [noOptionsText,foundOptions]);
const handleCloseAutocomplete = () => {
setOpenAutocomplete(false);
};
const createHandlerSelect = (item: string) => () => {
if (disabled) return;
onSelect(item);
@ -73,7 +76,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
if (target?.scrollIntoView) target.scrollIntoView({ block: "center" });
};
const handleKeyDown = (e: KeyboardEvent) => {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, shiftKey } = e;
const modifiers = ctrlKey || metaKey || shiftKey;
const hasOptions = foundOptions.length;
@ -98,22 +101,16 @@ const Autocomplete: FC<AutocompleteProps> = ({
if (key === "Escape") {
handleCloseAutocomplete();
}
};
}, [focusOption, foundOptions, handleCloseAutocomplete, onSelect, selected]);
useEffect(() => {
const words = (value.match(/[a-zA-Z_:.][a-zA-Z0-9_:.]*/gm) || []).length;
setOpenAutocomplete(value.length > minLength && words <= maxWords);
}, [value]);
useEffect(() => {
scrollToValue();
useEventListener("keydown", handleKeyDown);
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [focusOption, foundOptions]);
useEffect(scrollToValue, [focusOption, foundOptions]);
useEffect(() => {
setFocusOption(-1);

View File

@ -1,9 +1,11 @@
import React, { Ref, useEffect, useMemo, useState, forwardRef } from "preact/compat";
import React, { Ref, useMemo, forwardRef } from "preact/compat";
import Calendar from "../../Main/DatePicker/Calendar/Calendar";
import dayjs, { Dayjs } from "dayjs";
import Popper from "../../Main/Popper/Popper";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener";
interface DatePickerProps {
date: string | Date | Dayjs,
@ -20,17 +22,14 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
onChange,
label
}, ref) => {
const [openCalendar, setOpenCalendar] = useState(false);
const dateDayjs = useMemo(() => dayjs(date).isValid() ? dayjs.tz(date) : dayjs().tz(), [date]);
const { isMobile } = useDeviceDetect();
const toggleOpenCalendar = () => {
setOpenCalendar(prev => !prev);
};
const handleCloseCalendar = () => {
setOpenCalendar(false);
};
const {
value: openCalendar,
toggle: toggleOpenCalendar,
setFalse: handleCloseCalendar,
} = useBoolean(false);
const handleChangeDate = (val: string) => {
onChange(val);
@ -41,21 +40,8 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
if (e.key === "Escape" || e.key === "Enter") handleCloseCalendar();
};
useEffect(() => {
targetRef.current?.addEventListener("click", toggleOpenCalendar);
return () => {
targetRef.current?.removeEventListener("click", toggleOpenCalendar);
};
}, [targetRef]);
useEffect(() => {
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
useEventListener("click", toggleOpenCalendar, targetRef);
useEventListener("keyup", handleKeyUp);
return (<>
<Popper

View File

@ -1,18 +1,13 @@
import React, { FC } from "preact/compat";
import * as icons from "./index";
import { useSnack } from "../../../contexts/Snackbar";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import "./style.scss";
const PreviewIcons: FC = () => {
const { showInfoMessage } = useSnack();
const copyToClipboard = useCopyToClipboard();
const handleClickIcon = (copyValue: string) => {
navigator.clipboard.writeText(`<${copyValue}/>`);
showInfoMessage({ text: `<${copyValue}/> has been copied`, type: "success" });
};
const createHandlerClickIcon = (key: string) => () => {
handleClickIcon(key);
const createHandlerClickIcon = (key: string) => async () => {
await copyToClipboard(`<${key}/>`, `<${key}/> has been copied`);
};
return (

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect } from "preact/compat";
import React, { FC, useCallback, useEffect } from "preact/compat";
import ReactDOM from "react-dom";
import { CloseIcon } from "../Icons";
import Button from "../Button/Button";
@ -7,6 +7,7 @@ import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
import { useLocation, useNavigate } from "react-router-dom";
import useEventListener from "../../../hooks/useEventListener";
interface ModalProps {
title?: string
@ -21,48 +22,42 @@ const Modal: FC<ModalProps> = ({
children,
onClose,
className,
isOpen= true
isOpen = true
}) => {
const { isMobile } = useDeviceDetect();
const navigate = useNavigate();
const location = useLocation();
const handleKeyUp = (e: KeyboardEvent) => {
const handleKeyUp = useCallback((e: KeyboardEvent) => {
if (!isOpen) return;
if (e.key === "Escape") onClose();
};
}, [isOpen]);
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const handlePopstate = () => {
const handlePopstate = useCallback(() => {
if (isOpen) {
navigate(location, { replace: true });
onClose();
}
};
useEffect(() => {
window.addEventListener("popstate", handlePopstate);
return () => {
window.removeEventListener("popstate", handlePopstate);
};
}, [isOpen, location]);
}, [isOpen, location, onClose]);
const handleDisplayModal = () => {
if (!isOpen) return;
document.body.style.overflow = "hidden";
window.addEventListener("keyup", handleKeyUp);
return () => {
document.body.style.overflow = "auto";
window.removeEventListener("keyup", handleKeyUp);
};
};
useEffect(handleDisplayModal, [isOpen]);
useEventListener("popstate", handlePopstate);
useEventListener("keyup", handleKeyUp);
return ReactDOM.createPortal((
<div
className={classNames({

View File

@ -7,6 +7,9 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Button from "../Button/Button";
import { CloseIcon } from "../Icons";
import { useLocation, useNavigate } from "react-router-dom";
import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener";
import { useCallback } from "preact/compat";
interface PopperProps {
children: ReactNode
@ -37,23 +40,16 @@ const Popper: FC<PopperProps> = ({
const { isMobile } = useDeviceDetect();
const navigate = useNavigate();
const location = useLocation();
const [isOpen, setIsOpen] = useState(false);
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
const {
value: isOpen,
setValue: setIsOpen,
setFalse: handleClose,
} = useBoolean(false);
const popperRef = useRef<HTMLDivElement>(null);
const onScrollWindow = () => {
setIsOpen(false);
};
useEffect(() => {
window.addEventListener("scroll", onScrollWindow);
return () => {
window.removeEventListener("scroll", onScrollWindow);
};
}, []);
useEffect(() => {
setIsOpen(open);
}, [open]);
@ -137,32 +133,25 @@ const Popper: FC<PopperProps> = ({
}
}, [isOpen, popperRef]);
const handlePopstate = () => {
const handlePopstate = useCallback(() => {
if (isOpen && isMobile && !disabledFullScreen) {
navigate(location, { replace: true });
onClose();
}
};
}, [isOpen, isMobile, disabledFullScreen, location, onClose]);
useEffect(() => {
window.addEventListener("popstate", handlePopstate);
return () => {
window.removeEventListener("popstate", handlePopstate);
};
}, [isOpen, isMobile, disabledFullScreen, location]);
const popperClasses = classNames({
"vm-popper": true,
"vm-popper_mobile": isMobile && !disabledFullScreen,
"vm-popper_open": (isMobile || Object.keys(popperStyle).length) && isOpen,
});
useEventListener("scroll", handleClose);
useEventListener("popstate", handlePopstate);
return (
<>
{(isOpen || !popperSize.width) && ReactDOM.createPortal((
<div
className={popperClasses}
className={classNames({
"vm-popper": true,
"vm-popper_mobile": isMobile && !disabledFullScreen,
"vm-popper_open": (isMobile || Object.keys(popperStyle).length) && isOpen,
})}
ref={popperRef}
style={(isMobile && !disabledFullScreen) ? {} : popperStyle}
>

View File

@ -7,6 +7,7 @@ import { useAppState } from "../../../state/common/StateContext";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import MultipleSelectedValue from "./MultipleSelectedValue/MultipleSelectedValue";
import useEventListener from "../../../hooks/useEventListener";
interface SelectProps {
value: string | string[]
@ -105,13 +106,7 @@ const Select: FC<SelectProps> = ({
inputRef.current.focus();
}, [autofocus, inputRef]);
useEffect(() => {
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
useEventListener("keyup", handleKeyUp);
return (
<div

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useState } from "preact/compat";
import React, { FC, useCallback } from "preact/compat";
import { getAppModeEnable } from "../../../utils/app-mode";
import Button from "../Button/Button";
import { KeyboardIcon } from "../Icons";
@ -7,38 +7,31 @@ import "./style.scss";
import Tooltip from "../Tooltip/Tooltip";
import keyList from "./constants/keyList";
import { isMacOs } from "../../../utils/detect-device";
import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener";
const title = "Shortcut keys";
const isMac = isMacOs();
const keyOpenHelp = isMac ? "Cmd + /" : "F1";
const ShortcutKeys: FC<{ showTitle?: boolean }> = ({ showTitle }) => {
const [openList, setOpenList] = useState(false);
const appModeEnable = getAppModeEnable();
const handleOpen = () => {
setOpenList(true);
};
const {
value: openList,
setTrue: handleOpen,
setFalse: handleClose,
} = useBoolean(false);
const handleClose = () => {
setOpenList(false);
};
const handleKeyDown = (e: KeyboardEvent) => {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const openOnMac = isMac && e.key === "/" && e.metaKey;
const openOnOther = !isMac && e.key === "F1" && !e.metaKey;
if (openOnMac || openOnOther) {
handleOpen();
}
};
}, [handleOpen]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
useEventListener("keydown", handleKeyDown);
return <>
<Tooltip

View File

@ -64,6 +64,7 @@
svg {
width: 24px;
padding: 4px;
color: $color-primary;
}
}

View File

@ -1,9 +1,9 @@
import React, { Component, FC, useRef, useState } from "preact/compat";
import { ReactNode, useEffect } from "react";
import { getCssVariable } from "../../../utils/theme";
import useResize from "../../../hooks/useResize";
import TabItem from "./TabItem";
import "./style.scss";
import useWindowSize from "../../../hooks/useWindowSize";
export interface TabItemType {
value: string
@ -29,7 +29,7 @@ const Tabs: FC<TabsProps> = ({
indicatorPlacement = "bottom",
isNavLink,
}) => {
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const activeNavRef = useRef<Component>(null);
const [indicatorPosition, setIndicatorPosition] = useState({ left: 0, width: 0, bottom: 0 });

View File

@ -7,7 +7,7 @@ import { darkPalette, lightPalette } from "../../../constants/palette";
import { Theme } from "../../../types";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import useSystemTheme from "../../../hooks/useSystemTheme";
import useResize from "../../../hooks/useResize";
import useWindowSize from "../../../hooks/useWindowSize";
interface ThemeProviderProps {
onLoaded: (val: boolean) => void
@ -29,7 +29,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
const { theme } = useAppState();
const isDarkTheme = useSystemTheme();
const dispatch = useAppDispatch();
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const [palette, setPalette] = useState({
[Theme.dark]: darkPalette,

View File

@ -4,6 +4,7 @@ import "./style.scss";
import { ReactNode } from "react";
import { ExoticComponent } from "react";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useEventListener from "../../../hooks/useEventListener";
interface TooltipProps {
children: ReactNode
@ -29,14 +30,7 @@ const Tooltip: FC<TooltipProps> = ({
const popperRef = useRef<HTMLDivElement>(null);
const onScrollWindow = () => setIsOpen(false);
useEffect(() => {
window.addEventListener("scroll", onScrollWindow);
return () => {
window.removeEventListener("scroll", onScrollWindow);
};
}, []);
useEventListener("scroll", onScrollWindow);
useEffect(() => {
if (!popperRef.current || !isOpen) return;

View File

@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import React, { FC, useCallback, useEffect, useMemo, useState } from "preact/compat";
import { MetricResult } from "../../../api/types";
import LineChart from "../../Chart/Line/LineChart/LineChart";
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
@ -18,6 +18,7 @@ import { promValueToNumber } from "../../../utils/metric";
import { normalizeData } from "../../../utils/uplot/heatmap";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { TooltipHeatmapProps } from "../../Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap";
import useElementSize from "../../../hooks/useElementSize";
export interface GraphViewProps {
data?: MetricResult[];
@ -164,7 +165,7 @@ const GraphView: FC<GraphViewProps> = ({
setLegend(tempLegend);
}, [hideSeries]);
const containerRef = useRef<HTMLDivElement>(null);
const [containerRef, containerSize] = useElementSize();
return (
<div
@ -175,7 +176,7 @@ const GraphView: FC<GraphViewProps> = ({
})}
ref={containerRef}
>
{containerRef?.current && !isHistogram && (
{!isHistogram && (
<LineChart
data={dataChart}
series={series}
@ -184,11 +185,11 @@ const GraphView: FC<GraphViewProps> = ({
yaxis={yaxis}
unit={unit}
setPeriod={setPeriod}
container={containerRef?.current}
layoutSize={containerSize}
height={height}
/>
)}
{containerRef?.current && isHistogram && (
{isHistogram && (
<HeatmapChart
data={dataChart}
metrics={data}
@ -196,7 +197,7 @@ const GraphView: FC<GraphViewProps> = ({
yaxis={yaxis}
unit={unit}
setPeriod={setPeriod}
container={containerRef?.current}
layoutSize={containerSize}
height={height}
onChangeLegend={handleChangeLegend}
/>

View File

@ -1,6 +1,6 @@
import React, { FC, useMemo } from "preact/compat";
import { InstantMetricResult } from "../../../api/types";
import { useSnack } from "../../../contexts/Snackbar";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { TopQuery } from "../../../types";
import Button from "../../Main/Button/Button";
import "./style.scss";
@ -10,13 +10,12 @@ export interface JsonViewProps {
}
const JsonView: FC<JsonViewProps> = ({ data }) => {
const { showInfoMessage } = useSnack();
const copyToClipboard = useCopyToClipboard();
const formattedJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
const handlerCopy = () => {
navigator.clipboard.writeText(formattedJson);
showInfoMessage({ text: "Formatted JSON has been copied", type: "success" });
const handlerCopy = async () => {
await copyToClipboard(formattedJson, "Formatted JSON has been copied");
};
return (

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import { InstantMetricResult } from "../../../api/types";
import { InstantDataSeries } from "../../../types";
import { useSortedCategories } from "../../../hooks/useSortedCategories";
@ -7,12 +7,13 @@ import classNames from "classnames";
import { ArrowDropDownIcon, CopyIcon } from "../../Main/Icons";
import Tooltip from "../../Main/Tooltip/Tooltip";
import Button from "../../Main/Button/Button";
import { useSnack } from "../../../contexts/Snackbar";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { getNameForMetric } from "../../../utils/metric";
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import "./style.scss";
import useResize from "../../../hooks/useResize";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useWindowSize from "../../../hooks/useWindowSize";
import useEventListener from "../../../hooks/useEventListener";
export interface GraphViewProps {
data: InstantMetricResult[];
@ -20,11 +21,11 @@ export interface GraphViewProps {
}
const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
const { showInfoMessage } = useSnack();
const copyToClipboard = useCopyToClipboard();
const { isMobile } = useDeviceDetect();
const { tableCompact } = useCustomPanelState();
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const tableRef = useRef<HTMLTableElement>(null);
const [tableTop, setTableTop] = useState(0);
const [headTop, setHeadTop] = useState(0);
@ -74,33 +75,22 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
setOrderBy(key);
};
const copyHandler = async (copyValue: string) => {
await navigator.clipboard.writeText(copyValue);
showInfoMessage({ text: "Row has been copied", type: "success" });
};
const createSortHandler = (key: string) => () => {
sortHandler(key);
};
const createCopyHandler = (copyValue: string) => () => {
copyHandler(copyValue);
const createCopyHandler = (copyValue: string) => async () => {
await copyToClipboard(copyValue, "Row has been copied");
};
const handleScroll = () => {
const handleScroll = useCallback(() => {
if (!tableRef.current) return;
const { top } = tableRef.current.getBoundingClientRect();
setHeadTop(top < 0 ? window.scrollY - tableTop : 0);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [tableRef, tableTop, windowSize]);
useEventListener("scroll", handleScroll);
useEffect(() => {
if (!tableRef.current) return;
const { top } = tableRef.current.getBoundingClientRect();

View File

@ -1,4 +1,6 @@
import { useEffect, RefObject } from "react";
import { RefObject } from "react";
import useEventListener from "./useEventListener";
import { useCallback } from "preact/compat";
type Event = MouseEvent | TouchEvent;
@ -7,26 +9,19 @@ const useClickOutside = <T extends HTMLElement = HTMLElement>(
handler: (event: Event) => void,
preventRef?: RefObject<T>
) => {
useEffect(() => {
const listener = (event: Event) => {
const el = ref?.current;
const target = event.target as HTMLElement;
const isPreventRef = preventRef?.current && preventRef.current.contains(target);
if (!el || el.contains((event?.target as Node) || null) || isPreventRef) {
return;
}
const listener = useCallback((event: Event) => {
const el = ref?.current;
const target = event.target as HTMLElement;
const isPreventRef = preventRef?.current && preventRef.current.contains(target);
if (!el || el.contains((event?.target as Node) || null) || isPreventRef) {
return;
}
handler(event); // Call the handler only if the click is outside of the element passed.
};
handler(event); // Call the handler only if the click is outside of the element passed.
}, [ref, handler]);
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]); // Reload only if ref or handler changes
useEventListener("mousedown", listener);
useEventListener("touchstart", listener);
};
export default useClickOutside;

View File

@ -0,0 +1,32 @@
import { useSnack } from "../contexts/Snackbar";
type CopyFn = (text: string, msgInfo?: string) => Promise<boolean> // Return success
const useCopyToClipboard = (): CopyFn => {
const { showInfoMessage } = useSnack();
return async (text, msgInfo) => {
if (!navigator?.clipboard) {
showInfoMessage({ text: "Clipboard not supported", type: "error" });
console.warn("Clipboard not supported");
return false;
}
// Try to save to clipboard then save it in the state if worked
try {
await navigator.clipboard.writeText(text);
if (msgInfo) {
showInfoMessage({ text: msgInfo, type: "success" });
}
return true;
} catch (error) {
if (error instanceof Error) {
showInfoMessage({ text: `${error.name}: ${error.message}`, type: "error" });
}
console.warn("Copy failed", error);
return false;
}
};
};
export default useCopyToClipboard;

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { isMobileAgent } from "../utils/detect-device";
import useResize from "./useResize";
import useWindowSize from "./useWindowSize";
export default function useDeviceDetect() {
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const getIsMobile = () => {
const mobileAgent = isMobileAgent();

View File

@ -1,8 +1,11 @@
import { useState, useEffect } from "preact/compat";
import { useState } from "preact/compat";
import useEventListener from "./useEventListener";
import { useRef } from "react";
const useDropzone = (node: HTMLElement | null): {dragging: boolean, files: File[]} => {
const useDropzone = (): { dragging: boolean, files: File[] } => {
const [files, setFiles] = useState<File[]>([]);
const [dragging, setDragging] = useState(false);
const bodyRef = useRef(document.body);
const handleAddFiles = (fileList: FileList) => {
const filesArray = Array.from(fileList || []);
@ -43,21 +46,11 @@ const useDropzone = (node: HTMLElement | null): {dragging: boolean, files: File[
setFiles(jsonFiles);
};
useEffect(() => {
node?.addEventListener("dragenter", handleDrag);
node?.addEventListener("dragleave", handleDrag);
node?.addEventListener("dragover", handleDrag);
node?.addEventListener("drop", handleDrop);
node?.addEventListener("paste", handlePaste);
return () => {
node?.removeEventListener("dragenter", handleDrag);
node?.removeEventListener("dragleave", handleDrag);
node?.removeEventListener("dragover", handleDrag);
node?.removeEventListener("drop", handleDrop);
node?.removeEventListener("paste", handlePaste);
};
}, [node]);
useEventListener("dragenter", handleDrag, bodyRef);
useEventListener("dragleave", handleDrag, bodyRef);
useEventListener("dragover", handleDrag, bodyRef);
useEventListener("drop", handleDrop, bodyRef);
useEventListener("paste", handlePaste, bodyRef);
return {
files,

View File

@ -0,0 +1,36 @@
import { useCallback, useState } from "react";
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect";
import useEventListener from "./useEventListener";
export interface ElementSize {
width: number
height: number
}
const useElementSize = <T extends HTMLElement = HTMLDivElement>(): [(node: T | null) => void, ElementSize] => {
// Mutable values like 'ref.current' aren't valid dependencies
// because mutating them doesn't re-render the component.
// Instead, we use a state as a ref to be reactive.
const [ref, setRef] = useState<T | null>(null);
const [size, setSize] = useState<ElementSize>({
width: 0,
height: 0,
});
// Prevent too many rendering using useCallback
const handleSize = useCallback(() => {
setSize({
width: ref?.offsetWidth || 0,
height: ref?.offsetHeight || 0,
});
}, [ref?.offsetHeight, ref?.offsetWidth]);
useEventListener("resize", handleSize);
useIsomorphicLayoutEffect(handleSize, [ref?.offsetHeight, ref?.offsetWidth]);
return [setRef, size];
};
export default useElementSize;

View File

@ -0,0 +1,81 @@
import { RefObject, useEffect, useRef } from "react";
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect";
// MediaQueryList Event based useEventListener interface
function useEventListener<K extends keyof MediaQueryListEventMap>(
eventName: K,
handler: (event: MediaQueryListEventMap[K]) => void,
element: RefObject<MediaQueryList>,
options?: boolean | AddEventListenerOptions,
): void
// Window Event based useEventListener interface
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions,
): void
// Element Event based useEventListener interface
function useEventListener<
K extends keyof HTMLElementEventMap,
T extends HTMLElement = HTMLDivElement,
>(
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void,
element: RefObject<T>,
options?: boolean | AddEventListenerOptions,
): void
// Document Event based useEventListener interface
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: RefObject<Document>,
options?: boolean | AddEventListenerOptions,
): void
function useEventListener<
KW extends keyof WindowEventMap,
KH extends keyof HTMLElementEventMap,
KM extends keyof MediaQueryListEventMap,
T extends HTMLElement | MediaQueryList | void = void,
>(
eventName: KW | KH | KM,
handler: (
event:
| WindowEventMap[KW]
| HTMLElementEventMap[KH]
| MediaQueryListEventMap[KM]
| Event,
) => void,
element?: RefObject<T>,
options?: boolean | AddEventListenerOptions,
) {
// Create a ref that stores handler
const savedHandler = useRef(handler);
useIsomorphicLayoutEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current ?? window;
if (!(targetElement && targetElement.addEventListener)) return;
// Create event listener that calls handler function stored in ref
const listener: typeof handler = event => savedHandler.current(event);
targetElement.addEventListener(eventName, listener, options);
// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, listener, options);
};
}, [eventName, element, options]);
}
export default useEventListener;

View File

@ -0,0 +1,5 @@
import { useEffect, useLayoutEffect } from "react";
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;

View File

@ -1,21 +0,0 @@
import { useState, useEffect } from "preact/compat";
const useResize = (node: HTMLElement | null): {width: number, height: number} => {
const [windowSize, setWindowSize] = useState({
width: 0,
height: 0,
});
useEffect(() => {
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
setWindowSize({ width, height });
});
if (node) observer.observe(node);
return () => {
if (node) observer.unobserve(node);
};
}, [node]);
return windowSize;
};
export default useResize;

View File

@ -0,0 +1,31 @@
import { useState } from "react";
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect";
import useEventListener from "./useEventListener";
interface WindowSize {
width: number
height: number
}
const useWindowSize = (): WindowSize => {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: 0,
height: 0,
});
const handleSize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
useEventListener("resize", handleSize);
// Set size at the first client-side load
useIsomorphicLayoutEffect(handleSize, []);
return windowSize;
};
export default useWindowSize;

View File

@ -1,4 +1,4 @@
import React, { FC, useEffect, useState, useRef, useMemo } from "preact/compat";
import React, { FC, useEffect, useRef, useMemo } from "preact/compat";
import { useSortedCategories } from "../../../../hooks/useSortedCategories";
import { InstantMetricResult } from "../../../../api/types";
import Button from "../../../../components/Main/Button/Button";
@ -12,6 +12,7 @@ import Switch from "../../../../components/Main/Switch/Switch";
import { arrayEquals } from "../../../../utils/array";
import classNames from "classnames";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
const title = "Table settings";
@ -30,7 +31,11 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onCh
const buttonRef = useRef<HTMLDivElement>(null);
const [openSettings, setOpenSettings] = useState(false);
const {
value: openSettings,
toggle: toggleOpenSettings,
setFalse: handleClose,
} = useBoolean(false);
const disabledButton = useMemo(() => !columns.length, [columns]);
@ -38,16 +43,12 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onCh
onChange(defaultColumns.includes(key) ? defaultColumns.filter(col => col !== key) : [...defaultColumns, key]);
};
const handleClose = () => {
setOpenSettings(false);
};
const toggleTableCompact = () => {
customPanelDispatch({ type: "TOGGLE_TABLE_COMPACT" });
};
const handleResetColumns = () => {
setOpenSettings(false);
handleClose();
onChange(columns.map(col => col.key));
};
@ -55,10 +56,6 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onCh
handleChange(key);
};
const toggleOpenSettings = () => {
setOpenSettings(prev => !prev);
};
useEffect(() => {
const values = columns.map(col => col.key);
if (arrayEquals(values, defaultColumns)) return;

View File

@ -24,6 +24,7 @@ import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import GraphTips from "../../components/Chart/GraphTips/GraphTips";
import InstantQueryTip from "./InstantQueryTip/InstantQueryTip";
import useBoolean from "../../hooks/useBoolean";
const CustomPanel: FC = () => {
const { displayType, isTracingEnabled } = useCustomPanelState();
@ -36,9 +37,14 @@ const CustomPanel: FC = () => {
const [displayColumns, setDisplayColumns] = useState<string[]>();
const [tracesState, setTracesState] = useState<Trace[]>([]);
const [hideQuery, setHideQuery] = useState<number[]>([]);
const [showAllSeries, setShowAllSeries] = useState(false);
const [hideError, setHideError] = useState(!query[0]);
const {
value: showAllSeries,
setTrue: handleShowAll,
setFalse: handleHideSeries,
} = useBoolean(false);
const { customStep, yaxis } = useGraphState();
const graphDispatch = useGraphDispatch();
@ -72,10 +78,6 @@ const CustomPanel: FC = () => {
timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
};
const handleShowAll = () => {
setShowAllSeries(true);
};
const handleTraceDelete = (trace: Trace) => {
const updatedTraces = tracesState.filter((data) => data.idValue !== trace.idValue);
setTracesState([...updatedTraces]);
@ -99,9 +101,7 @@ const CustomPanel: FC = () => {
setTracesState([]);
}, [displayType]);
useEffect(() => {
setShowAllSeries(false);
}, [query]);
useEffect(handleHideSeries, [query]);
useEffect(() => {
graphDispatch({ type: "SET_IS_HISTOGRAM", payload: isHistogram });

View File

@ -1,12 +1,13 @@
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import { MouseEvent as ReactMouseEvent } from "react";
import { MouseEvent as ReactMouseEvent, useCallback } from "react";
import { DashboardRow } from "../../../types";
import PredefinedPanel from "../PredefinedPanel/PredefinedPanel";
import useResize from "../../../hooks/useResize";
import Accordion from "../../../components/Main/Accordion/Accordion";
import "./style.scss";
import classNames from "classnames";
import Alert from "../../../components/Main/Alert/Alert";
import useWindowSize from "../../../hooks/useWindowSize";
import useEventListener from "../../../hooks/useEventListener";
export interface PredefinedDashboardProps extends DashboardRow {
filename: string;
@ -20,7 +21,7 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({
filename
}) => {
const windowSize = useResize(document.body);
const windowSize = useWindowSize();
const sizeSection = useMemo(() => {
return windowSize.width / 12;
}, [windowSize]);
@ -34,16 +35,14 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({
const [resize, setResize] = useState({ start: 0, target: 0, enable: false });
const handleMouseMove = (e: MouseEvent) => {
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!resize.enable) return;
const { start } = resize;
const sectionCount = Math.ceil((start - e.clientX)/sizeSection);
if (Math.abs(sectionCount) >= 12) return;
const width = panelsWidth.map((p, i) => {
return p - (i === resize.target ? sectionCount : 0);
});
const width = panelsWidth.map((p, i) => p - (i === resize.target ? sectionCount : 0));
setPanelsWidth(width);
};
}, [resize, sizeSection]);
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>, i: number) => {
setResize({
@ -52,12 +51,13 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({
enable: true,
});
};
const handleMouseUp = () => {
const handleMouseUp = useCallback(() => {
setResize({
...resize,
enable: false
});
};
}, [resize]);
const handleChangeExpanded = (val: boolean) => setExpanded(val);
@ -65,14 +65,8 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({
handleMouseDown(e, index);
};
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [resize]);
useEventListener("mousemove", handleMouseMove);
useEventListener("mouseup", handleMouseUp);
const HeaderAccordion = () => (
<div

View File

@ -61,7 +61,7 @@
height: 20px;
transform: scale(0);
transition: transform 200ms ease-in-out;
cursor: se-resize;
cursor: ew-resize;
z-index: 1;
&:after {

View File

@ -5,7 +5,7 @@ import Button from "../../../components/Main/Button/Button";
import Trace from "../../../components/TraceQuery/Trace";
import { ErrorTypes } from "../../../types";
import classNames from "classnames";
import { useSnack } from "../../../contexts/Snackbar";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { CopyIcon, RestartIcon } from "../../../components/Main/Icons";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
@ -28,7 +28,7 @@ const JsonForm: FC<JsonFormProps> = ({
onClose,
onUpload,
}) => {
const { showInfoMessage } = useSnack();
const copyToClipboard = useCopyToClipboard();
const { isMobile } = useDeviceDetect();
const [json, setJson] = useState(defaultJson);
@ -58,8 +58,7 @@ const JsonForm: FC<JsonFormProps> = ({
};
const handlerCopy = async () => {
await navigator.clipboard.writeText(json);
showInfoMessage({ text: "Formatted JSON has been copied", type: "success" });
await copyToClipboard(json, "Formatted JSON has been copied");
};
const handleReset = () => {

View File

@ -12,21 +12,19 @@ import { ErrorTypes } from "../../types";
import { useSearchParams } from "react-router-dom";
import useDropzone from "../../hooks/useDropzone";
import TraceUploadButtons from "./TraceUploadButtons/TraceUploadButtons";
import useBoolean from "../../hooks/useBoolean";
const TracePage: FC = () => {
const [openModal, setOpenModal] = useState(false);
const [tracesState, setTracesState] = useState<Trace[]>([]);
const [errors, setErrors] = useState<{filename: string, text: string}[]>([]);
const hasTraces = useMemo(() => !!tracesState.length, [tracesState]);
const [, setSearchParams] = useSearchParams();
const handleOpenModal = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const {
value: openModal,
setTrue: handleOpenModal,
setFalse: handleCloseModal,
} = useBoolean(false);
const handleError = (e: Error, filename = "") => {
setErrors(prev => [{ filename, text: `: ${e.message}` }, ...prev]);
@ -84,7 +82,7 @@ const TracePage: FC = () => {
}, []);
const { files, dragging } = useDropzone(document.body);
const { files, dragging } = useDropzone();
useEffect(() => {
handleReadFiles(files);