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:
Chintan Sudani 2023-01-25 20:31:42 +05:30 committed by GitHub
parent fd6f9a90e1
commit 213838a021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 187 additions and 96 deletions

View File

@ -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

View File

@ -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;

View File

@ -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,
}}
/> />
</> </>
); );

View File

@ -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,
}}
/> />
</> </>
); );

View File

@ -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}

View File

@ -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';

View File

@ -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>
); );
} }

View File

@ -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);

View 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;