|
| 1 | +import {useCallback, useEffect, useReducer, useRef} from 'react'; |
| 2 | + |
| 3 | +import {RawSerieData, YagrPlugin, YagrWidgetData} from '@gravity-ui/chartkit/yagr'; |
| 4 | +import ChartKit, {settings} from '@gravity-ui/chartkit'; |
| 5 | + |
| 6 | +import type {IResponseError} from '../../types/api/error'; |
| 7 | +import type {TimeFrame} from '../../utils/timeframes'; |
| 8 | +import {useAutofetcher} from '../../utils/hooks'; |
| 9 | +import {COLORS} from '../../utils/versions'; |
| 10 | +import {cn} from '../../utils/cn'; |
| 11 | + |
| 12 | +import {Loader} from '../Loader'; |
| 13 | +import {ResponseError} from '../Errors/ResponseError'; |
| 14 | + |
| 15 | +import type {ChartOptions, MetricDescription, PreparedMetricsData} from './types'; |
| 16 | +import {convertResponse} from './convertReponse'; |
| 17 | +import {getDefaultDataFormatter} from './getDefaultDataFormatter'; |
| 18 | +import {getChartData} from './getChartData'; |
| 19 | +import { |
| 20 | + chartReducer, |
| 21 | + initialChartState, |
| 22 | + setChartData, |
| 23 | + setChartDataLoading, |
| 24 | + setChartDataWasNotLoaded, |
| 25 | + setChartError, |
| 26 | +} from './reducer'; |
| 27 | + |
| 28 | +import './MetricChart.scss'; |
| 29 | + |
| 30 | +const b = cn('ydb-metric-chart'); |
| 31 | + |
| 32 | +settings.set({plugins: [YagrPlugin]}); |
| 33 | + |
| 34 | +const prepareWidgetData = ( |
| 35 | + data: PreparedMetricsData, |
| 36 | + options: ChartOptions = {}, |
| 37 | +): YagrWidgetData => { |
| 38 | + const {dataType} = options; |
| 39 | + const defaultDataFormatter = getDefaultDataFormatter(dataType); |
| 40 | + |
| 41 | + const isDataEmpty = !data.metrics.length; |
| 42 | + |
| 43 | + const graphs: RawSerieData[] = data.metrics.map((metric, index) => { |
| 44 | + return { |
| 45 | + id: metric.target, |
| 46 | + name: metric.title || metric.target, |
| 47 | + color: metric.color || COLORS[index], |
| 48 | + data: metric.data, |
| 49 | + formatter: defaultDataFormatter, |
| 50 | + }; |
| 51 | + }); |
| 52 | + |
| 53 | + return { |
| 54 | + data: { |
| 55 | + timeline: data.timeline, |
| 56 | + graphs, |
| 57 | + }, |
| 58 | + |
| 59 | + libraryConfig: { |
| 60 | + chart: { |
| 61 | + size: { |
| 62 | + // When empty data chart is displayed without axes it have different paddings |
| 63 | + // To compensate it, additional paddings are applied |
| 64 | + padding: isDataEmpty ? [10, 0, 10, 0] : undefined, |
| 65 | + }, |
| 66 | + series: { |
| 67 | + type: 'line', |
| 68 | + }, |
| 69 | + select: { |
| 70 | + zoom: false, |
| 71 | + }, |
| 72 | + }, |
| 73 | + scales: { |
| 74 | + y: { |
| 75 | + type: 'linear', |
| 76 | + range: 'nice', |
| 77 | + }, |
| 78 | + }, |
| 79 | + axes: { |
| 80 | + y: { |
| 81 | + values: defaultDataFormatter |
| 82 | + ? (_, ticks) => ticks.map(defaultDataFormatter) |
| 83 | + : undefined, |
| 84 | + }, |
| 85 | + }, |
| 86 | + tooltip: { |
| 87 | + show: true, |
| 88 | + tracking: 'sticky', |
| 89 | + }, |
| 90 | + }, |
| 91 | + }; |
| 92 | +}; |
| 93 | + |
| 94 | +interface DiagnosticsChartProps { |
| 95 | + title?: string; |
| 96 | + metrics: MetricDescription[]; |
| 97 | + timeFrame?: TimeFrame; |
| 98 | + |
| 99 | + autorefresh?: boolean; |
| 100 | + |
| 101 | + height?: number; |
| 102 | + width?: number; |
| 103 | + |
| 104 | + chartOptions?: ChartOptions; |
| 105 | +} |
| 106 | + |
| 107 | +export const MetricChart = ({ |
| 108 | + title, |
| 109 | + metrics, |
| 110 | + timeFrame = '1h', |
| 111 | + autorefresh, |
| 112 | + width = 400, |
| 113 | + height = width / 1.5, |
| 114 | + chartOptions, |
| 115 | +}: DiagnosticsChartProps) => { |
| 116 | + const mounted = useRef(false); |
| 117 | + |
| 118 | + useEffect(() => { |
| 119 | + mounted.current = true; |
| 120 | + return () => { |
| 121 | + mounted.current = false; |
| 122 | + }; |
| 123 | + }, []); |
| 124 | + |
| 125 | + const [{loading, wasLoaded, data, error}, dispatch] = useReducer( |
| 126 | + chartReducer, |
| 127 | + initialChartState, |
| 128 | + ); |
| 129 | + |
| 130 | + const fetchChartData = useCallback( |
| 131 | + async (isBackground: boolean) => { |
| 132 | + dispatch(setChartDataLoading()); |
| 133 | + |
| 134 | + if (!isBackground) { |
| 135 | + dispatch(setChartDataWasNotLoaded()); |
| 136 | + } |
| 137 | + |
| 138 | + try { |
| 139 | + // maxDataPoints param is calculated based on width |
| 140 | + // should be width > maxDataPoints to prevent points that cannot be selected |
| 141 | + // more px per dataPoint - easier to select, less - chart is smoother |
| 142 | + const response = await getChartData({ |
| 143 | + metrics, |
| 144 | + timeFrame, |
| 145 | + maxDataPoints: width / 2, |
| 146 | + }); |
| 147 | + |
| 148 | + // Hack to prevent setting value to state, if component unmounted |
| 149 | + if (!mounted.current) return; |
| 150 | + |
| 151 | + // In some cases error could be in response with 200 status code |
| 152 | + // It happens when request is OK, but chart data cannot be returned due to some reason |
| 153 | + // Example: charts are not enabled in the DB ('GraphShard is not enabled' error) |
| 154 | + if (Array.isArray(response)) { |
| 155 | + const preparedData = convertResponse(response, metrics); |
| 156 | + dispatch(setChartData(preparedData)); |
| 157 | + } else { |
| 158 | + dispatch(setChartError({statusText: response.error})); |
| 159 | + } |
| 160 | + } catch (err) { |
| 161 | + if (!mounted.current) return; |
| 162 | + |
| 163 | + dispatch(setChartError(err as IResponseError)); |
| 164 | + } |
| 165 | + }, |
| 166 | + [metrics, timeFrame, width], |
| 167 | + ); |
| 168 | + |
| 169 | + useAutofetcher(fetchChartData, [fetchChartData], autorefresh); |
| 170 | + |
| 171 | + const convertedData = prepareWidgetData(data, chartOptions); |
| 172 | + |
| 173 | + const renderContent = () => { |
| 174 | + if (loading && !wasLoaded) { |
| 175 | + return <Loader />; |
| 176 | + } |
| 177 | + |
| 178 | + return ( |
| 179 | + <div className={b('chart')}> |
| 180 | + <ChartKit type="yagr" data={convertedData} /> |
| 181 | + {error && <ResponseError className={b('error')} error={error} />} |
| 182 | + </div> |
| 183 | + ); |
| 184 | + }; |
| 185 | + |
| 186 | + return ( |
| 187 | + <div |
| 188 | + className={b(null)} |
| 189 | + style={{ |
| 190 | + height, |
| 191 | + width, |
| 192 | + }} |
| 193 | + > |
| 194 | + <div className={b('title')}>{title}</div> |
| 195 | + {renderContent()} |
| 196 | + </div> |
| 197 | + ); |
| 198 | +}; |
0 commit comments