Skip to content

Commit bfbd3d0

Browse files
authored
feat: auto refresh with advanced control (#804)
1 parent 6488896 commit bfbd3d0

File tree

112 files changed

+1815
-3527
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+1815
-3527
lines changed

src/.eslintrc

-6
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@
33
"rules": {
44
"react/jsx-uses-react": "off",
55
"react/react-in-jsx-scope": "off",
6-
"react-hooks/exhaustive-deps": [
7-
"warn",
8-
{
9-
"additionalHooks": "(useAutofetcher)",
10-
},
11-
],
126
"valid-jsdoc": "off",
137
"react/jsx-fragments": ["error", "element"],
148
"no-restricted-syntax": [

src/components/MetricChart/MetricChart.tsx

+22-93
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,14 @@ import ChartKit, {settings} from '@gravity-ui/chartkit';
44
import type {YagrSeriesData, YagrWidgetData} from '@gravity-ui/chartkit/yagr';
55
import {YagrPlugin} from '@gravity-ui/chartkit/yagr';
66

7-
import type {IResponseError} from '../../types/api/error';
87
import {cn} from '../../utils/cn';
9-
import {useAutofetcher} from '../../utils/hooks';
108
import type {TimeFrame} from '../../utils/timeframes';
119
import {ResponseError} from '../Errors/ResponseError';
1210
import {Loader} from '../Loader';
1311

1412
import {colorToRGBA, colors} from './colors';
15-
import {convertResponse} from './convertResponse';
16-
import {getChartData} from './getChartData';
1713
import {getDefaultDataFormatter} from './getDefaultDataFormatter';
18-
import i18n from './i18n';
19-
import {
20-
chartReducer,
21-
initialChartState,
22-
setChartData,
23-
setChartDataLoading,
24-
setChartDataWasNotLoaded,
25-
setChartError,
26-
} from './reducer';
14+
import {chartApi} from './reducer';
2715
import type {
2816
ChartOptions,
2917
MetricDescription,
@@ -107,14 +95,16 @@ const prepareWidgetData = (
10795
};
10896
};
10997

98+
const emptyChartData: PreparedMetricsData = {timeline: [], metrics: []};
99+
110100
interface DiagnosticsChartProps {
111101
database: string;
112102

113103
title?: string;
114104
metrics: MetricDescription[];
115105
timeFrame?: TimeFrame;
116106

117-
autorefresh?: boolean;
107+
autorefresh?: number;
118108

119109
height?: number;
120110
width?: number;
@@ -143,90 +133,29 @@ export const MetricChart = ({
143133
onChartDataStatusChange,
144134
isChartVisible,
145135
}: DiagnosticsChartProps) => {
146-
const mounted = React.useRef(false);
147-
148-
React.useEffect(() => {
149-
mounted.current = true;
150-
return () => {
151-
mounted.current = false;
152-
};
153-
}, []);
154-
155-
const [{loading, wasLoaded, data, error}, dispatch] = React.useReducer(
156-
chartReducer,
157-
initialChartState,
158-
);
159-
160-
React.useEffect(() => {
161-
if (error) {
162-
return onChartDataStatusChange?.('error');
163-
}
164-
if (loading && !wasLoaded) {
165-
return onChartDataStatusChange?.('loading');
166-
}
167-
if (!loading && wasLoaded) {
168-
return onChartDataStatusChange?.('success');
169-
}
170-
171-
return undefined;
172-
}, [loading, wasLoaded, error, onChartDataStatusChange]);
173-
174-
const fetchChartData = React.useCallback(
175-
async (isBackground: boolean) => {
176-
dispatch(setChartDataLoading());
177-
178-
if (!isBackground) {
179-
dispatch(setChartDataWasNotLoaded());
180-
}
181-
182-
try {
183-
// maxDataPoints param is calculated based on width
184-
// should be width > maxDataPoints to prevent points that cannot be selected
185-
// more px per dataPoint - easier to select, less - chart is smoother
186-
const response = await getChartData({
187-
database,
188-
metrics,
189-
timeFrame,
190-
maxDataPoints: width / 2,
191-
});
192-
193-
// Hack to prevent setting value to state, if component unmounted
194-
if (!mounted.current) {
195-
return;
196-
}
197-
198-
// Response could be a plain html for ydb versions without charts support
199-
// Or there could be an error in response with 200 status code
200-
// It happens when request is OK, but chart data cannot be returned due to some reason
201-
// Example: charts are not enabled in the DB ('GraphShard is not enabled' error)
202-
if (Array.isArray(response)) {
203-
const preparedData = convertResponse(response, metrics);
204-
dispatch(setChartData(preparedData));
205-
} else {
206-
const err = {
207-
statusText:
208-
typeof response === 'string' ? i18n('not-supported') : response.error,
209-
};
210-
211-
throw err;
212-
}
213-
} catch (err) {
214-
if (!mounted.current) {
215-
return;
216-
}
217-
218-
dispatch(setChartError(err as IResponseError));
219-
}
136+
const {currentData, error, isFetching, status} = chartApi.useGetChartDataQuery(
137+
// maxDataPoints param is calculated based on width
138+
// should be width > maxDataPoints to prevent points that cannot be selected
139+
// more px per dataPoint - easier to select, less - chart is smoother
140+
{
141+
database,
142+
metrics,
143+
timeFrame,
144+
maxDataPoints: width / 2,
220145
},
221-
[database, metrics, timeFrame, width],
146+
{pollingInterval: autorefresh},
222147
);
223148

224-
useAutofetcher(fetchChartData, [fetchChartData], autorefresh);
149+
const loading = isFetching && !currentData;
150+
151+
React.useEffect(() => {
152+
return onChartDataStatusChange?.(status === 'fulfilled' ? 'success' : 'loading');
153+
}, [status, onChartDataStatusChange]);
225154

226-
const convertedData = prepareWidgetData(data, chartOptions);
155+
const convertedData = prepareWidgetData(currentData || emptyChartData, chartOptions);
227156

228157
const renderContent = () => {
229-
if (loading && !wasLoaded) {
158+
if (loading) {
230159
return <Loader />;
231160
}
232161

@@ -237,7 +166,7 @@ export const MetricChart = ({
237166
return (
238167
<div className={b('chart')}>
239168
<ChartKit type="yagr" data={convertedData} />
240-
{error && <ResponseError className={b('error')} error={error} />}
169+
{error ? <ResponseError className={b('error')} error={error} /> : null}
241170
</div>
242171
);
243172
};

src/components/MetricChart/getChartData.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,24 @@ import type {TimeFrame} from '../../utils/timeframes';
33

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

6-
interface GetChartDataParams {
6+
export interface GetChartDataParams {
77
database: string;
88
metrics: MetricDescription[];
99
timeFrame: TimeFrame;
1010
maxDataPoints: number;
1111
}
1212

13-
export const getChartData = async ({
14-
database,
15-
metrics,
16-
timeFrame,
17-
maxDataPoints,
18-
}: GetChartDataParams) => {
13+
export const getChartData = async (
14+
{database, metrics, timeFrame, maxDataPoints}: GetChartDataParams,
15+
{signal}: {signal?: AbortSignal} = {},
16+
) => {
1917
const targetString = metrics.map((metric) => `target=${metric.target}`).join('&');
2018

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

2422
return window.api.getChartData(
2523
{target: targetString, from, until, maxDataPoints, database},
26-
{concurrentId: `getChartData|${targetString}`},
24+
{signal},
2725
);
2826
};

src/components/MetricChart/reducer.ts

+38-86
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,38 @@
1-
import {createRequestActionTypes} from '../../store/utils';
2-
import type {IResponseError} from '../../types/api/error';
3-
4-
import type {PreparedMetricsData} from './types';
5-
6-
const FETCH_CHART_DATA = createRequestActionTypes('chart', 'FETCH_CHART_DATA');
7-
const SET_CHART_DATA_WAS_NOT_LOADED = 'chart/SET_DATA_WAS_NOT_LOADED';
8-
9-
export const setChartDataLoading = () => {
10-
return {
11-
type: FETCH_CHART_DATA.REQUEST,
12-
} as const;
13-
};
14-
15-
export const setChartData = (data: PreparedMetricsData) => {
16-
return {
17-
data,
18-
type: FETCH_CHART_DATA.SUCCESS,
19-
} as const;
20-
};
21-
22-
export const setChartError = (error: IResponseError) => {
23-
return {
24-
error,
25-
type: FETCH_CHART_DATA.FAILURE,
26-
} as const;
27-
};
28-
29-
export const setChartDataWasNotLoaded = () => {
30-
return {
31-
type: SET_CHART_DATA_WAS_NOT_LOADED,
32-
} as const;
33-
};
34-
35-
type ChartAction =
36-
| ReturnType<typeof setChartDataLoading>
37-
| ReturnType<typeof setChartData>
38-
| ReturnType<typeof setChartError>
39-
| ReturnType<typeof setChartDataWasNotLoaded>;
40-
41-
interface ChartState {
42-
loading: boolean;
43-
wasLoaded: boolean;
44-
data: PreparedMetricsData;
45-
error: IResponseError | undefined;
46-
}
47-
48-
export const initialChartState: ChartState = {
49-
// Set chart initial state as loading, in order not to mount and unmount component in between requests
50-
// as it leads to memory leak errors in console (not proper useEffect cleanups in chart component itself)
51-
// TODO: possible fix (check needed): chart component is always present, but display: none for chart while loading
52-
loading: true,
53-
wasLoaded: false,
54-
data: {timeline: [], metrics: []},
55-
error: undefined,
56-
};
57-
58-
export const chartReducer = (state: ChartState, action: ChartAction) => {
59-
switch (action.type) {
60-
case FETCH_CHART_DATA.REQUEST: {
61-
return {...state, loading: true};
62-
}
63-
case FETCH_CHART_DATA.SUCCESS: {
64-
return {...state, loading: false, wasLoaded: true, error: undefined, data: action.data};
65-
}
66-
case FETCH_CHART_DATA.FAILURE: {
67-
if (action.error?.isCancelled) {
68-
return state;
69-
}
70-
71-
return {
72-
...state,
73-
error: action.error,
74-
// Clear data, so error will be displayed with empty chart
75-
data: {timeline: [], metrics: []},
76-
loading: false,
77-
wasLoaded: true,
78-
};
79-
}
80-
case SET_CHART_DATA_WAS_NOT_LOADED: {
81-
return {...state, wasLoaded: false};
82-
}
83-
default:
84-
return state;
85-
}
86-
};
1+
import {api} from '../../store/reducers/api';
2+
3+
import {convertResponse} from './convertResponse';
4+
import type {GetChartDataParams} from './getChartData';
5+
import {getChartData} from './getChartData';
6+
import i18n from './i18n';
7+
8+
export const chartApi = api.injectEndpoints({
9+
endpoints: (builder) => ({
10+
getChartData: builder.query({
11+
queryFn: async (params: GetChartDataParams, {signal}) => {
12+
try {
13+
const response = await getChartData(params, {signal});
14+
15+
// Response could be a plain html for ydb versions without charts support
16+
// Or there could be an error in response with 200 status code
17+
// It happens when request is OK, but chart data cannot be returned due to some reason
18+
// Example: charts are not enabled in the DB ('GraphShard is not enabled' error)
19+
if (Array.isArray(response)) {
20+
const preparedData = convertResponse(response, params.metrics);
21+
return {data: preparedData};
22+
}
23+
24+
return {
25+
error: new Error(
26+
typeof response === 'string' ? i18n('not-supported') : response.error,
27+
),
28+
};
29+
} catch (error) {
30+
return {error};
31+
}
32+
},
33+
providesTags: ['All'],
34+
keepUnusedDataFor: 0,
35+
}),
36+
}),
37+
overrideExisting: 'throw',
38+
});

src/containers/App/Content.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {SlotComponent} from '../../components/slots/types';
1010
import routes from '../../routes';
1111
import type {RootState} from '../../store';
1212
import {getUser} from '../../store/reducers/authentication/authentication';
13-
import {getNodesList} from '../../store/reducers/nodesList';
13+
import {nodesListApi} from '../../store/reducers/nodesList';
1414
import {cn} from '../../utils/cn';
1515
import {useTypedDispatch, useTypedSelector} from '../../utils/hooks';
1616
import Authentication from '../Authentication/Authentication';
@@ -178,12 +178,7 @@ function GetUser() {
178178
}
179179

180180
function GetNodesList() {
181-
const dispatch = useTypedDispatch();
182-
183-
React.useEffect(() => {
184-
dispatch(getNodesList());
185-
}, [dispatch]);
186-
181+
nodesListApi.useGetNodesListQuery(undefined);
187182
return null;
188183
}
189184

0 commit comments

Comments
 (0)