mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-20 07:19:17 +01:00
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:
parent
afd03f87fe
commit
5c2ed85eb9
1882
app/vmui/packages/vmui/package-lock.json
generated
1882
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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 });
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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">
|
||||
|
@ -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) || [];
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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(() => {
|
||||
|
@ -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]);
|
||||
|
@ -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(() => {
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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 = () => {
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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({
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -64,6 +64,7 @@
|
||||
svg {
|
||||
width: 24px;
|
||||
padding: 4px;
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 (
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
32
app/vmui/packages/vmui/src/hooks/useCopyToClipboard.ts
Normal file
32
app/vmui/packages/vmui/src/hooks/useCopyToClipboard.ts
Normal 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;
|
@ -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();
|
||||
|
@ -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,
|
||||
|
36
app/vmui/packages/vmui/src/hooks/useElementSize.ts
Normal file
36
app/vmui/packages/vmui/src/hooks/useElementSize.ts
Normal 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;
|
81
app/vmui/packages/vmui/src/hooks/useEventListener.ts
Normal file
81
app/vmui/packages/vmui/src/hooks/useEventListener.ts
Normal 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;
|
@ -0,0 +1,5 @@
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
||||
|
||||
export default useIsomorphicLayoutEffect;
|
@ -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;
|
31
app/vmui/packages/vmui/src/hooks/useWindowSize.ts
Normal file
31
app/vmui/packages/vmui/src/hooks/useWindowSize.ts
Normal 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;
|
@ -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;
|
||||
|
@ -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 });
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 = () => {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user