-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(TenantOverview): add charts (#657)
- Loading branch information
1 parent
f816d60
commit 78daa0b
Showing
38 changed files
with
1,333 additions
and
148 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
.ydb-metric-chart { | ||
display: flex; | ||
flex-direction: column; | ||
|
||
padding: 16px 16px 8px; | ||
|
||
border: 1px solid var(--g-color-line-generic); | ||
border-radius: 8px; | ||
|
||
&__title { | ||
margin-bottom: 10px; | ||
} | ||
|
||
&__chart { | ||
position: relative; | ||
|
||
display: flex; | ||
overflow: hidden; | ||
|
||
width: 100%; | ||
height: 100%; | ||
} | ||
|
||
&__error { | ||
position: absolute; | ||
z-index: 1; | ||
top: 10%; | ||
left: 50%; | ||
|
||
text-align: center; | ||
|
||
transform: translateX(-50%); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import {useCallback, useEffect, useReducer, useRef} from 'react'; | ||
|
||
import {RawSerieData, YagrPlugin, YagrWidgetData} from '@gravity-ui/chartkit/yagr'; | ||
import ChartKit, {settings} from '@gravity-ui/chartkit'; | ||
|
||
import type {IResponseError} from '../../types/api/error'; | ||
import type {TimeFrame} from '../../utils/timeframes'; | ||
import {useAutofetcher} from '../../utils/hooks'; | ||
import {COLORS} from '../../utils/versions'; | ||
import {cn} from '../../utils/cn'; | ||
|
||
import {Loader} from '../Loader'; | ||
import {ResponseError} from '../Errors/ResponseError'; | ||
|
||
import type {ChartOptions, MetricDescription, PreparedMetricsData} from './types'; | ||
import {convertResponse} from './convertReponse'; | ||
import {getDefaultDataFormatter} from './getDefaultDataFormatter'; | ||
import {getChartData} from './getChartData'; | ||
import { | ||
chartReducer, | ||
initialChartState, | ||
setChartData, | ||
setChartDataLoading, | ||
setChartDataWasNotLoaded, | ||
setChartError, | ||
} from './reducer'; | ||
|
||
import './MetricChart.scss'; | ||
|
||
const b = cn('ydb-metric-chart'); | ||
|
||
settings.set({plugins: [YagrPlugin]}); | ||
|
||
const prepareWidgetData = ( | ||
data: PreparedMetricsData, | ||
options: ChartOptions = {}, | ||
): YagrWidgetData => { | ||
const {dataType} = options; | ||
const defaultDataFormatter = getDefaultDataFormatter(dataType); | ||
|
||
const isDataEmpty = !data.metrics.length; | ||
|
||
const graphs: RawSerieData[] = data.metrics.map((metric, index) => { | ||
return { | ||
id: metric.target, | ||
name: metric.title || metric.target, | ||
color: metric.color || COLORS[index], | ||
data: metric.data, | ||
formatter: defaultDataFormatter, | ||
}; | ||
}); | ||
|
||
return { | ||
data: { | ||
timeline: data.timeline, | ||
graphs, | ||
}, | ||
|
||
libraryConfig: { | ||
chart: { | ||
size: { | ||
// When empty data chart is displayed without axes it have different paddings | ||
// To compensate it, additional paddings are applied | ||
padding: isDataEmpty ? [10, 0, 10, 0] : undefined, | ||
}, | ||
series: { | ||
type: 'line', | ||
}, | ||
select: { | ||
zoom: false, | ||
}, | ||
}, | ||
scales: { | ||
y: { | ||
type: 'linear', | ||
range: 'nice', | ||
}, | ||
}, | ||
axes: { | ||
y: { | ||
values: defaultDataFormatter | ||
? (_, ticks) => ticks.map(defaultDataFormatter) | ||
: undefined, | ||
}, | ||
}, | ||
tooltip: { | ||
show: true, | ||
tracking: 'sticky', | ||
}, | ||
}, | ||
}; | ||
}; | ||
|
||
interface DiagnosticsChartProps { | ||
title?: string; | ||
metrics: MetricDescription[]; | ||
timeFrame?: TimeFrame; | ||
|
||
autorefresh?: boolean; | ||
|
||
height?: number; | ||
width?: number; | ||
|
||
chartOptions?: ChartOptions; | ||
} | ||
|
||
export const MetricChart = ({ | ||
title, | ||
metrics, | ||
timeFrame = '1h', | ||
autorefresh, | ||
width = 400, | ||
height = width / 1.5, | ||
chartOptions, | ||
}: DiagnosticsChartProps) => { | ||
const mounted = useRef(false); | ||
|
||
useEffect(() => { | ||
mounted.current = true; | ||
return () => { | ||
mounted.current = false; | ||
}; | ||
}, []); | ||
|
||
const [{loading, wasLoaded, data, error}, dispatch] = useReducer( | ||
chartReducer, | ||
initialChartState, | ||
); | ||
|
||
const fetchChartData = 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({ | ||
metrics, | ||
timeFrame, | ||
maxDataPoints: width / 2, | ||
}); | ||
|
||
// Hack to prevent setting value to state, if component unmounted | ||
if (!mounted.current) return; | ||
|
||
// In some cases error could be 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 { | ||
dispatch(setChartError({statusText: response.error})); | ||
} | ||
} catch (err) { | ||
if (!mounted.current) return; | ||
|
||
dispatch(setChartError(err as IResponseError)); | ||
} | ||
}, | ||
[metrics, timeFrame, width], | ||
); | ||
|
||
useAutofetcher(fetchChartData, [fetchChartData], autorefresh); | ||
|
||
const convertedData = prepareWidgetData(data, chartOptions); | ||
|
||
const renderContent = () => { | ||
if (loading && !wasLoaded) { | ||
return <Loader />; | ||
} | ||
|
||
return ( | ||
<div className={b('chart')}> | ||
<ChartKit type="yagr" data={convertedData} /> | ||
{error && <ResponseError className={b('error')} error={error} />} | ||
</div> | ||
); | ||
}; | ||
|
||
return ( | ||
<div | ||
className={b(null)} | ||
style={{ | ||
height, | ||
width, | ||
}} | ||
> | ||
<div className={b('title')}>{title}</div> | ||
{renderContent()} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import type {MetricData} from '../../types/api/render'; | ||
import type {MetricDescription, PreparedMetric, PreparedMetricsData} from './types'; | ||
|
||
export const convertResponse = ( | ||
data: MetricData[] = [], | ||
metrics: MetricDescription[], | ||
): PreparedMetricsData => { | ||
const preparedMetrics = data | ||
.map(({datapoints, target}) => { | ||
const metricDescription = metrics.find((metric) => metric.target === target); | ||
const chartData = datapoints.map((datapoint) => datapoint[0] || 0); | ||
|
||
if (!metricDescription) { | ||
return undefined; | ||
} | ||
|
||
return { | ||
...metricDescription, | ||
data: chartData, | ||
}; | ||
}) | ||
.filter((metric): metric is PreparedMetric => metric !== undefined); | ||
|
||
// Asuming all metrics in response have the same timeline | ||
// Backend return data in seconds, while chart needs ms | ||
const timeline = data[0].datapoints.map((datapoint) => datapoint[1] * 1000); | ||
|
||
return { | ||
timeline, | ||
metrics: preparedMetrics, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import {TIMEFRAMES, type TimeFrame} from '../../utils/timeframes'; | ||
import type {MetricDescription} from './types'; | ||
|
||
interface GetChartDataParams { | ||
metrics: MetricDescription[]; | ||
timeFrame: TimeFrame; | ||
maxDataPoints: number; | ||
} | ||
|
||
export const getChartData = async ({metrics, timeFrame, maxDataPoints}: GetChartDataParams) => { | ||
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}, | ||
{concurrentId: `getChartData|${targetString}`}, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import {formatBytes} from '../../utils/bytesParsers'; | ||
import {roundToPrecision} from '../../utils/dataFormatters/dataFormatters'; | ||
import {formatToMs} from '../../utils/timeParsers'; | ||
import {isNumeric} from '../../utils/utils'; | ||
|
||
import type {ChartDataType, ChartValue} from './types'; | ||
|
||
export const getDefaultDataFormatter = (dataType?: ChartDataType) => { | ||
switch (dataType) { | ||
case 'ms': { | ||
return formatChartValueToMs; | ||
} | ||
case 'size': { | ||
return formatChartValueToSize; | ||
} | ||
default: | ||
return undefined; | ||
} | ||
}; | ||
|
||
function formatChartValueToMs(value: ChartValue) { | ||
return formatToMs(roundToPrecision(convertToNumber(value), 2)); | ||
} | ||
|
||
function formatChartValueToSize(value: ChartValue) { | ||
return formatBytes({value: convertToNumber(value), precision: 3}); | ||
} | ||
|
||
// Numeric values expected, not numeric value should be displayd as 0 | ||
function convertToNumber(value: unknown): number { | ||
if (isNumeric(value)) { | ||
return Number(value); | ||
} | ||
|
||
return 0; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export type {MetricDescription, Metric, ChartOptions} from './types'; | ||
export {MetricChart} from './MetricChart'; |
Oops, something went wrong.