Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto refresh with advanced control #804

Merged
merged 22 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
artemmufazalov marked this conversation as resolved.
Show resolved Hide resolved
},
[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
Loading