mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 05:15:57 +08:00
fix: Chart loaders on reload and change of time interval at dashboard (#2068)
* fix: Chart data logic on dashboard reloads * fix: linting issues * fix: added right side loader & css config * fix: loader condition change * fix: linting issues * fix: error state of API * fix: Resolved suggested changes * fix: Error state for API Failed * fix: Default loading state * fix: linting issues * fix: Suggested changes * feat: Added common hook for previous value * chore: usePrevious is made type safety * chore: chart data set is updated * chore: removed eslint rule * fix: commitlint issue on commit Co-authored-by: Vishal Sharma <makeavish786@gmail.com> Co-authored-by: Palash Gupta <palashgdev@gmail.com> Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
parent
fd6f9a90e1
commit
213838a021
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
cd frontend && npm run commitlint
|
cd frontend && yarn run commitlint --edit $1
|
||||||
|
@ -4,9 +4,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import { SpinerStyle } from './styles';
|
import { SpinerStyle } from './styles';
|
||||||
|
|
||||||
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
|
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SpinerStyle height={height}>
|
<SpinerStyle height={height} style={style}>
|
||||||
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
||||||
</SpinerStyle>
|
</SpinerStyle>
|
||||||
);
|
);
|
||||||
@ -16,11 +16,13 @@ interface SpinnerProps {
|
|||||||
size?: SpinProps['size'];
|
size?: SpinProps['size'];
|
||||||
tip?: SpinProps['tip'];
|
tip?: SpinProps['tip'];
|
||||||
height?: React.CSSProperties['height'];
|
height?: React.CSSProperties['height'];
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
Spinner.defaultProps = {
|
Spinner.defaultProps = {
|
||||||
size: undefined,
|
size: undefined,
|
||||||
tip: undefined,
|
tip: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
|
style: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner;
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from 'container/NewWidget/RightContainer/timeItems';
|
} from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||||
@ -58,6 +58,18 @@ function FullView({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartDataSet = useMemo(
|
||||||
|
() =>
|
||||||
|
getChartData({
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
queryData: response?.data?.payload?.data?.result || [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[response],
|
||||||
|
);
|
||||||
|
|
||||||
const isLoading = response.isLoading === true;
|
const isLoading = response.isLoading === true;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -86,25 +98,15 @@ function FullView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
{...{
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
data={chartDataSet}
|
||||||
data: getChartData({
|
isStacked={widget.isStacked}
|
||||||
queryData: [
|
opacity={widget.opacity}
|
||||||
{
|
title={widget.title}
|
||||||
queryData: response.data?.payload?.data?.result
|
onClickHandler={onClickHandler}
|
||||||
? response.data?.payload?.data?.result
|
name={name}
|
||||||
: [],
|
yAxisUnit={yAxisUnit}
|
||||||
},
|
onDragSelect={onDragSelect}
|
||||||
],
|
|
||||||
}),
|
|
||||||
isStacked: widget.isStacked,
|
|
||||||
opacity: widget.opacity,
|
|
||||||
title: widget.title,
|
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
onDragSelect,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -15,7 +15,7 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
|
|||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import getStartAndEndTime from 'lib/getStartAndEndTime';
|
import getStartAndEndTime from 'lib/getStartAndEndTime';
|
||||||
import getStep from 'lib/getStep';
|
import getStep from 'lib/getStep';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -115,6 +115,18 @@ function FullView({
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartDataSet = useMemo(
|
||||||
|
() =>
|
||||||
|
getChartData({
|
||||||
|
queryData: data.map((e) => ({
|
||||||
|
query: e?.map((e) => e.query).join(' ') || '',
|
||||||
|
queryData: e?.map((e) => e.queryData) || [],
|
||||||
|
legend: e?.map((e) => e.legend).join('') || '',
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
@ -149,23 +161,15 @@ function FullView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
{...{
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
data={chartDataSet}
|
||||||
data: getChartData({
|
isStacked={widget.isStacked}
|
||||||
queryData: data.map((e) => ({
|
opacity={widget.opacity}
|
||||||
query: e?.map((e) => e.query).join(' ') || '',
|
title={widget.title}
|
||||||
queryData: e?.map((e) => e.queryData) || [],
|
onClickHandler={onClickHandler}
|
||||||
legend: e?.map((e) => e.legend).join('') || '',
|
name={name}
|
||||||
})),
|
yAxisUnit={yAxisUnit}
|
||||||
}),
|
onDragSelect={onDragSelect}
|
||||||
isStacked: widget.isStacked,
|
|
||||||
opacity: widget.opacity,
|
|
||||||
title: widget.title,
|
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
onDragSelect,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import { ChartData } from 'chart.js';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import GridGraphComponent from 'container/GridGraphComponent';
|
import GridGraphComponent from 'container/GridGraphComponent';
|
||||||
|
import usePreviousValue from 'hooks/usePreviousValue';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
@ -18,9 +20,7 @@ import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { GlobalTime } from 'types/actions/globalTime';
|
import { GlobalTime } from 'types/actions/globalTime';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ import { LayoutProps } from '..';
|
|||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import WidgetHeader from '../WidgetHeader';
|
||||||
import FullView from './FullView/index.metricsBuilder';
|
import FullView from './FullView/index.metricsBuilder';
|
||||||
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
import { FullViewContainer, Modal } from './styles';
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
widget,
|
widget,
|
||||||
@ -39,6 +39,7 @@ function GridCardGraph({
|
|||||||
setLayout,
|
setLayout,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
@ -57,9 +58,7 @@ function GridCardGraph({
|
|||||||
const selectedData = selectedDashboard?.data;
|
const selectedData = selectedDashboard?.data;
|
||||||
const { variables } = selectedData;
|
const { variables } = selectedData;
|
||||||
|
|
||||||
const response = useQuery<
|
const queryResponse = useQuery(
|
||||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
|
||||||
>(
|
|
||||||
[
|
[
|
||||||
`GetMetricsQueryRange-${widget.timePreferance}-${globalSelectedInterval}-${widget.id}`,
|
`GetMetricsQueryRange-${widget.timePreferance}-${globalSelectedInterval}-${widget.id}`,
|
||||||
{
|
{
|
||||||
@ -81,6 +80,11 @@ function GridCardGraph({
|
|||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
|
onError: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -89,15 +93,15 @@ function GridCardGraph({
|
|||||||
getChartData({
|
getChartData({
|
||||||
queryData: [
|
queryData: [
|
||||||
{
|
{
|
||||||
queryData: response?.data?.payload?.data?.result
|
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||||
? response?.data?.payload?.data?.result
|
|
||||||
: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[response?.data?.payload],
|
[queryResponse],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
const onToggleModal = useCallback(
|
||||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||||
func((value) => !value);
|
func((value) => !value);
|
||||||
@ -154,10 +158,12 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||||
|
|
||||||
if (response.isError && !isEmptyLayout) {
|
if (queryResponse.isError && !isEmptyLayout) {
|
||||||
return (
|
return (
|
||||||
<>
|
<span>
|
||||||
{getModals()}
|
{getModals()}
|
||||||
|
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||||
|
<>
|
||||||
<div className="drag-handle">
|
<div className="drag-handle">
|
||||||
<WidgetHeader
|
<WidgetHeader
|
||||||
parentHover={hovered}
|
parentHover={hovered}
|
||||||
@ -165,28 +171,55 @@ function GridCardGraph({
|
|||||||
widget={widget}
|
widget={widget}
|
||||||
onView={handleOnView}
|
onView={handleOnView}
|
||||||
onDelete={handleOnDelete}
|
onDelete={handleOnDelete}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<GridGraphComponent
|
||||||
<ErrorContainer>
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
{response.isError && 'Something went wrong'}
|
data={prevChartDataSetRef}
|
||||||
</ErrorContainer>
|
isStacked={widget.isStacked}
|
||||||
|
opacity={widget.opacity}
|
||||||
|
title={' '}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.isFetching) {
|
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
|
||||||
return (
|
return (
|
||||||
|
<span>
|
||||||
|
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||||
<>
|
<>
|
||||||
|
<div className="drag-handle">
|
||||||
<WidgetHeader
|
<WidgetHeader
|
||||||
parentHover={hovered}
|
parentHover={hovered}
|
||||||
title={widget?.title}
|
title={widget?.title}
|
||||||
widget={widget}
|
widget={widget}
|
||||||
onView={handleOnView}
|
onView={handleOnView}
|
||||||
onDelete={handleOnDelete}
|
onDelete={handleOnDelete}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<GridGraphComponent
|
||||||
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
|
data={prevChartDataSetRef}
|
||||||
|
isStacked={widget.isStacked}
|
||||||
|
opacity={widget.opacity}
|
||||||
|
title={' '}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
/>
|
/>
|
||||||
<Spinner height="20vh" tip="Loading..." />
|
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<Spinner height="20vh" tip="Loading..." />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,13 +246,15 @@ function GridCardGraph({
|
|||||||
widget={widget}
|
widget={widget}
|
||||||
onView={handleOnView}
|
onView={handleOnView}
|
||||||
onDelete={handleOnDelete}
|
onDelete={handleOnDelete}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isEmptyLayout && getModals()}
|
{!isEmptyLayout && getModals()}
|
||||||
|
|
||||||
{!isEmpty(widget) && !!response.data?.payload?.data?.result && (
|
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
GRAPH_TYPES={widget.panelTypes}
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
|
||||||
|
const positionCss: React.CSSProperties['position'] = 'fixed';
|
||||||
|
|
||||||
|
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
|
||||||
|
export const tooltipStyles = {
|
||||||
|
fontSize: '1rem',
|
||||||
|
top: '0.313rem',
|
||||||
|
position: positionCss,
|
||||||
|
right: '0.313rem',
|
||||||
|
color: themeColors.errorColor,
|
||||||
|
};
|
||||||
|
export const errorTooltipPosition = 'top';
|
@ -2,17 +2,23 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
EditFilled,
|
EditFilled,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Dropdown, Menu, Typography } from 'antd';
|
import { Dropdown, Menu, Tooltip, Typography } from 'antd';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import { errorTooltipPosition, spinnerStyles, tooltipStyles } from './config';
|
||||||
import {
|
import {
|
||||||
ArrowContainer,
|
ArrowContainer,
|
||||||
HeaderContainer,
|
HeaderContainer,
|
||||||
@ -27,6 +33,10 @@ interface IWidgetHeaderProps {
|
|||||||
onView: VoidFunction;
|
onView: VoidFunction;
|
||||||
onDelete: VoidFunction;
|
onDelete: VoidFunction;
|
||||||
parentHover: boolean;
|
parentHover: boolean;
|
||||||
|
queryResponse: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||||
|
>;
|
||||||
|
errorMessage: string | undefined;
|
||||||
}
|
}
|
||||||
function WidgetHeader({
|
function WidgetHeader({
|
||||||
title,
|
title,
|
||||||
@ -34,6 +44,8 @@ function WidgetHeader({
|
|||||||
onView,
|
onView,
|
||||||
onDelete,
|
onDelete,
|
||||||
parentHover,
|
parentHover,
|
||||||
|
queryResponse,
|
||||||
|
errorMessage,
|
||||||
}: IWidgetHeaderProps): JSX.Element {
|
}: IWidgetHeaderProps): JSX.Element {
|
||||||
const [localHover, setLocalHover] = useState(false);
|
const [localHover, setLocalHover] = useState(false);
|
||||||
|
|
||||||
@ -106,6 +118,7 @@ function WidgetHeader({
|
|||||||
overlayStyle={{ minWidth: 100 }}
|
overlayStyle={{ minWidth: 100 }}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
|
<>
|
||||||
<HeaderContainer
|
<HeaderContainer
|
||||||
onMouseOver={(): void => setLocalHover(true)}
|
onMouseOver={(): void => setLocalHover(true)}
|
||||||
onMouseOut={(): void => setLocalHover(false)}
|
onMouseOut={(): void => setLocalHover(false)}
|
||||||
@ -120,6 +133,15 @@ function WidgetHeader({
|
|||||||
</ArrowContainer>
|
</ArrowContainer>
|
||||||
</HeaderContentContainer>
|
</HeaderContentContainer>
|
||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
|
{queryResponse.isFetching && !queryResponse.isError && (
|
||||||
|
<Spinner height="5vh" style={spinnerStyles} />
|
||||||
|
)}
|
||||||
|
{queryResponse.isError && (
|
||||||
|
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
|
||||||
|
<ExclamationCircleOutlined style={tooltipStyles} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import RouteTab from 'components/RouteTab';
|
import RouteTab from 'components/RouteTab';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
@ -85,4 +85,4 @@ function ServiceMetrics(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(ServiceMetrics);
|
export default memo(ServiceMetrics);
|
||||||
|
13
frontend/src/hooks/usePreviousValue.ts
Normal file
13
frontend/src/hooks/usePreviousValue.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
function usePreviousValue<T>(value: T): T {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return ref.current as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePreviousValue;
|
Loading…
x
Reference in New Issue
Block a user