Skip to content

Commit

Permalink
feat: auto refresh with advanced control (#804)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored May 3, 2024
1 parent 6488896 commit bfbd3d0
Show file tree
Hide file tree
Showing 112 changed files with 1,815 additions and 3,527 deletions.
6 changes: 0 additions & 6 deletions src/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "(useAutofetcher)",
},
],
"valid-jsdoc": "off",
"react/jsx-fragments": ["error", "element"],
"no-restricted-syntax": [
Expand Down
115 changes: 22 additions & 93 deletions src/components/MetricChart/MetricChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,14 @@ import ChartKit, {settings} from '@gravity-ui/chartkit';
import type {YagrSeriesData, YagrWidgetData} from '@gravity-ui/chartkit/yagr';
import {YagrPlugin} from '@gravity-ui/chartkit/yagr';

import type {IResponseError} from '../../types/api/error';
import {cn} from '../../utils/cn';
import {useAutofetcher} from '../../utils/hooks';
import type {TimeFrame} from '../../utils/timeframes';
import {ResponseError} from '../Errors/ResponseError';
import {Loader} from '../Loader';

import {colorToRGBA, colors} from './colors';
import {convertResponse} from './convertResponse';
import {getChartData} from './getChartData';
import {getDefaultDataFormatter} from './getDefaultDataFormatter';
import i18n from './i18n';
import {
chartReducer,
initialChartState,
setChartData,
setChartDataLoading,
setChartDataWasNotLoaded,
setChartError,
} from './reducer';
import {chartApi} from './reducer';
import type {
ChartOptions,
MetricDescription,
Expand Down Expand Up @@ -107,14 +95,16 @@ const prepareWidgetData = (
};
};

const emptyChartData: PreparedMetricsData = {timeline: [], metrics: []};

interface DiagnosticsChartProps {
database: string;

title?: string;
metrics: MetricDescription[];
timeFrame?: TimeFrame;

autorefresh?: boolean;
autorefresh?: number;

height?: number;
width?: number;
Expand Down Expand Up @@ -143,90 +133,29 @@ export const MetricChart = ({
onChartDataStatusChange,
isChartVisible,
}: DiagnosticsChartProps) => {
const mounted = React.useRef(false);

React.useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);

const [{loading, wasLoaded, data, error}, dispatch] = React.useReducer(
chartReducer,
initialChartState,
);

React.useEffect(() => {
if (error) {
return onChartDataStatusChange?.('error');
}
if (loading && !wasLoaded) {
return onChartDataStatusChange?.('loading');
}
if (!loading && wasLoaded) {
return onChartDataStatusChange?.('success');
}

return undefined;
}, [loading, wasLoaded, error, onChartDataStatusChange]);

const fetchChartData = React.useCallback(
async (isBackground: boolean) => {
dispatch(setChartDataLoading());

if (!isBackground) {
dispatch(setChartDataWasNotLoaded());
}

try {
// maxDataPoints param is calculated based on width
// should be width > maxDataPoints to prevent points that cannot be selected
// more px per dataPoint - easier to select, less - chart is smoother
const response = await getChartData({
database,
metrics,
timeFrame,
maxDataPoints: width / 2,
});

// Hack to prevent setting value to state, if component unmounted
if (!mounted.current) {
return;
}

// Response could be a plain html for ydb versions without charts support
// Or there could be an error in response with 200 status code
// It happens when request is OK, but chart data cannot be returned due to some reason
// Example: charts are not enabled in the DB ('GraphShard is not enabled' error)
if (Array.isArray(response)) {
const preparedData = convertResponse(response, metrics);
dispatch(setChartData(preparedData));
} else {
const err = {
statusText:
typeof response === 'string' ? i18n('not-supported') : response.error,
};

throw err;
}
} catch (err) {
if (!mounted.current) {
return;
}

dispatch(setChartError(err as IResponseError));
}
const {currentData, error, isFetching, status} = chartApi.useGetChartDataQuery(
// maxDataPoints param is calculated based on width
// should be width > maxDataPoints to prevent points that cannot be selected
// more px per dataPoint - easier to select, less - chart is smoother
{
database,
metrics,
timeFrame,
maxDataPoints: width / 2,
},
[database, metrics, timeFrame, width],
{pollingInterval: autorefresh},
);

useAutofetcher(fetchChartData, [fetchChartData], autorefresh);
const loading = isFetching && !currentData;

React.useEffect(() => {
return onChartDataStatusChange?.(status === 'fulfilled' ? 'success' : 'loading');
}, [status, onChartDataStatusChange]);

const convertedData = prepareWidgetData(data, chartOptions);
const convertedData = prepareWidgetData(currentData || emptyChartData, chartOptions);

const renderContent = () => {
if (loading && !wasLoaded) {
if (loading) {
return <Loader />;
}

Expand All @@ -237,7 +166,7 @@ export const MetricChart = ({
return (
<div className={b('chart')}>
<ChartKit type="yagr" data={convertedData} />
{error && <ResponseError className={b('error')} error={error} />}
{error ? <ResponseError className={b('error')} error={error} /> : null}
</div>
);
};
Expand Down
14 changes: 6 additions & 8 deletions src/components/MetricChart/getChartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,24 @@ import type {TimeFrame} from '../../utils/timeframes';

import type {MetricDescription} from './types';

interface GetChartDataParams {
export interface GetChartDataParams {
database: string;
metrics: MetricDescription[];
timeFrame: TimeFrame;
maxDataPoints: number;
}

export const getChartData = async ({
database,
metrics,
timeFrame,
maxDataPoints,
}: GetChartDataParams) => {
export const getChartData = async (
{database, metrics, timeFrame, maxDataPoints}: GetChartDataParams,
{signal}: {signal?: AbortSignal} = {},
) => {
const targetString = metrics.map((metric) => `target=${metric.target}`).join('&');

const until = Math.round(Date.now() / 1000);
const from = until - TIMEFRAMES[timeFrame];

return window.api.getChartData(
{target: targetString, from, until, maxDataPoints, database},
{concurrentId: `getChartData|${targetString}`},
{signal},
);
};
124 changes: 38 additions & 86 deletions src/components/MetricChart/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,38 @@
import {createRequestActionTypes} from '../../store/utils';
import type {IResponseError} from '../../types/api/error';

import type {PreparedMetricsData} from './types';

const FETCH_CHART_DATA = createRequestActionTypes('chart', 'FETCH_CHART_DATA');
const SET_CHART_DATA_WAS_NOT_LOADED = 'chart/SET_DATA_WAS_NOT_LOADED';

export const setChartDataLoading = () => {
return {
type: FETCH_CHART_DATA.REQUEST,
} as const;
};

export const setChartData = (data: PreparedMetricsData) => {
return {
data,
type: FETCH_CHART_DATA.SUCCESS,
} as const;
};

export const setChartError = (error: IResponseError) => {
return {
error,
type: FETCH_CHART_DATA.FAILURE,
} as const;
};

export const setChartDataWasNotLoaded = () => {
return {
type: SET_CHART_DATA_WAS_NOT_LOADED,
} as const;
};

type ChartAction =
| ReturnType<typeof setChartDataLoading>
| ReturnType<typeof setChartData>
| ReturnType<typeof setChartError>
| ReturnType<typeof setChartDataWasNotLoaded>;

interface ChartState {
loading: boolean;
wasLoaded: boolean;
data: PreparedMetricsData;
error: IResponseError | undefined;
}

export const initialChartState: ChartState = {
// Set chart initial state as loading, in order not to mount and unmount component in between requests
// as it leads to memory leak errors in console (not proper useEffect cleanups in chart component itself)
// TODO: possible fix (check needed): chart component is always present, but display: none for chart while loading
loading: true,
wasLoaded: false,
data: {timeline: [], metrics: []},
error: undefined,
};

export const chartReducer = (state: ChartState, action: ChartAction) => {
switch (action.type) {
case FETCH_CHART_DATA.REQUEST: {
return {...state, loading: true};
}
case FETCH_CHART_DATA.SUCCESS: {
return {...state, loading: false, wasLoaded: true, error: undefined, data: action.data};
}
case FETCH_CHART_DATA.FAILURE: {
if (action.error?.isCancelled) {
return state;
}

return {
...state,
error: action.error,
// Clear data, so error will be displayed with empty chart
data: {timeline: [], metrics: []},
loading: false,
wasLoaded: true,
};
}
case SET_CHART_DATA_WAS_NOT_LOADED: {
return {...state, wasLoaded: false};
}
default:
return state;
}
};
import {api} from '../../store/reducers/api';

import {convertResponse} from './convertResponse';
import type {GetChartDataParams} from './getChartData';
import {getChartData} from './getChartData';
import i18n from './i18n';

export const chartApi = api.injectEndpoints({
endpoints: (builder) => ({
getChartData: builder.query({
queryFn: async (params: GetChartDataParams, {signal}) => {
try {
const response = await getChartData(params, {signal});

// Response could be a plain html for ydb versions without charts support
// Or there could be an error in response with 200 status code
// It happens when request is OK, but chart data cannot be returned due to some reason
// Example: charts are not enabled in the DB ('GraphShard is not enabled' error)
if (Array.isArray(response)) {
const preparedData = convertResponse(response, params.metrics);
return {data: preparedData};
}

return {
error: new Error(
typeof response === 'string' ? i18n('not-supported') : response.error,
),
};
} catch (error) {
return {error};
}
},
providesTags: ['All'],
keepUnusedDataFor: 0,
}),
}),
overrideExisting: 'throw',
});
9 changes: 2 additions & 7 deletions src/containers/App/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {SlotComponent} from '../../components/slots/types';
import routes from '../../routes';
import type {RootState} from '../../store';
import {getUser} from '../../store/reducers/authentication/authentication';
import {getNodesList} from '../../store/reducers/nodesList';
import {nodesListApi} from '../../store/reducers/nodesList';
import {cn} from '../../utils/cn';
import {useTypedDispatch, useTypedSelector} from '../../utils/hooks';
import Authentication from '../Authentication/Authentication';
Expand Down Expand Up @@ -178,12 +178,7 @@ function GetUser() {
}

function GetNodesList() {
const dispatch = useTypedDispatch();

React.useEffect(() => {
dispatch(getNodesList());
}, [dispatch]);

nodesListApi.useGetNodesListQuery(undefined);
return null;
}

Expand Down
Loading

0 comments on commit bfbd3d0

Please sign in to comment.