366 lines
12 KiB
JavaScript
366 lines
12 KiB
JavaScript
import request, { formatResponse } from "@/utils/request";
|
|
import { cloneDeep } from "lodash";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { formatMessage } from "umi/locale";
|
|
import styles from "./Metrics.scss";
|
|
import { Alert, Dropdown, Empty, Icon, Menu, message, Spin, Tooltip } from "antd";
|
|
import {
|
|
Axis,
|
|
Chart,
|
|
CurveType,
|
|
LineSeries,
|
|
BarSeries,
|
|
niceTimeFormatByDay,
|
|
Position,
|
|
ScaleType,
|
|
Settings,
|
|
timeFormatter,
|
|
} from "@elastic/charts";
|
|
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
|
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
|
import moment from "moment";
|
|
import { ESPrefix } from "@/services/common";
|
|
|
|
export default (props) => {
|
|
|
|
const {
|
|
timezone,
|
|
timeRange,
|
|
timeout,
|
|
refresh,
|
|
handleTimeChange,
|
|
fetchUrl,
|
|
metricKey,
|
|
title,
|
|
queryParams,
|
|
className,
|
|
style,
|
|
formatMetric,
|
|
height = 200,
|
|
customRenderChart,
|
|
instance,
|
|
pointerUpdate,
|
|
handleTimeIntervalChange
|
|
} = props;
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const [metric, setMetric] = useState()
|
|
|
|
const [isInView, setIsInView] = useState(false);
|
|
|
|
const [error, setError] = useState();
|
|
|
|
const observerRef = useRef({ isInView: false })
|
|
|
|
const containerRef = useRef(null)
|
|
|
|
const firstFetchRef = useRef(true)
|
|
|
|
const [timeInterval, setTimeInterval] = useState()
|
|
|
|
const fetchData = async (queryParams, fetchUrl, metricKey, timeInterval, showLoading) => {
|
|
if (!observerRef.current.isInView || !fetchUrl) return;
|
|
if (firstFetchRef.current || showLoading) {
|
|
setLoading(true)
|
|
}
|
|
const newQueryParams = {
|
|
...queryParams,
|
|
key: metricKey,
|
|
timeout
|
|
}
|
|
if (timeInterval) {
|
|
newQueryParams.bucket_size = timeInterval
|
|
}
|
|
const res = await request(fetchUrl, {
|
|
method: 'GET',
|
|
queryParams: newQueryParams,
|
|
ignoreTimeout: true
|
|
}, false, false)
|
|
if (res?.error) {
|
|
const error = formatResponse(res);
|
|
setError(error?.errorObject?.key ? formatMessage({ id: `${error?.errorObject?.key}` }) : res?.error?.reason)
|
|
} else if (res && !res.error) {
|
|
const { metrics = {} } = res || {};
|
|
const metric = metrics[metricKey]
|
|
setError()
|
|
setMetric(formatMetric && metric ? formatMetric(metric) : metric);
|
|
}
|
|
if (firstFetchRef.current || showLoading) {
|
|
setLoading(false)
|
|
firstFetchRef.current = false
|
|
}
|
|
}
|
|
|
|
const fixFormat = (formatType, format) => {
|
|
return formatType === 'num' && format ? `${format}a` : format
|
|
}
|
|
|
|
useEffect(() => {
|
|
observerRef.current.deps = cloneDeep([queryParams, fetchUrl, metricKey, timeInterval, refresh])
|
|
fetchData(queryParams, fetchUrl, metricKey, timeInterval, refresh)
|
|
}, [JSON.stringify(queryParams), fetchUrl, metricKey, timeInterval, refresh])
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
entries => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
observerRef.current.isInView = true
|
|
if (JSON.stringify(observerRef.current.deps) !== JSON.stringify(observerRef.current.lastDeps)) {
|
|
observerRef.current.lastDeps = cloneDeep(observerRef.current.deps)
|
|
fetchData(...observerRef.current.deps)
|
|
}
|
|
} else {
|
|
observerRef.current.isInView = false
|
|
}
|
|
});
|
|
},
|
|
{
|
|
root: null,
|
|
threshold: 0,
|
|
}
|
|
);
|
|
|
|
if (containerRef.current) {
|
|
observer.observe(containerRef.current);
|
|
}
|
|
|
|
return () => {
|
|
if (containerRef.current) {
|
|
observer.unobserve(containerRef.current);
|
|
}
|
|
};
|
|
}, [isInView]);
|
|
|
|
const handleChartBrush = ({ x }) => {
|
|
if (!x) {
|
|
return;
|
|
}
|
|
let [from, to] = x;
|
|
if (typeof handleTimeChange == "function") {
|
|
if (to - from < 20 * 1000) {
|
|
from -= 10 * 1000;
|
|
to += 10 * 1000;
|
|
}
|
|
handleTimeChange({
|
|
start: moment(from).toISOString(),
|
|
end: moment(to).toISOString(),
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderChart = () => {
|
|
if (error) {
|
|
return (
|
|
<div style={{ height, padding: 10 }}>
|
|
<Alert style={{ maxHeight: '100%', overflow: 'auto', wordBreak: 'break-all'}} message={error} type="error"/>
|
|
</div>
|
|
)
|
|
}
|
|
const axis = metric?.axis || [];
|
|
const lines = metric?.lines || [];
|
|
if (lines.every((item) => !item.data || item.data.length === 0)) {
|
|
const emptyProps = {}
|
|
if (metric?.min_bucket_size > 0 && metric?.hits_total > 0) {
|
|
emptyProps.description = (
|
|
<>
|
|
<div style={{ wordBreak: 'break-all', textAlign: 'left', marginBotton: 2 }} >
|
|
{formatMessage({ id: "cluster.metrics.time_interval.empty" }, { min_bucket_size: metric.min_bucket_size})}
|
|
</div>
|
|
<Dropdown overlay={(
|
|
<Menu>
|
|
<Menu.Item onClick={() => handleTimeIntervalChange(`${metric?.min_bucket_size}s`)}>
|
|
{formatMessage({ id: `cluster.metrics.time_interval.set.global`})}
|
|
</Menu.Item>
|
|
<Menu.Item onClick={() => setTimeInterval(`${metric?.min_bucket_size}s`)}>
|
|
{formatMessage({ id: `cluster.metrics.time_interval.set.current`})}
|
|
</Menu.Item>
|
|
</Menu>
|
|
)}>
|
|
<a onClick={e => e.preventDefault()}>
|
|
{formatMessage({ id: `cluster.metrics.time_interval.apply`})} <Icon type="down" />
|
|
</a>
|
|
</Dropdown>
|
|
</>
|
|
)
|
|
}
|
|
return (
|
|
<div style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Empty style={{ margin: 0}} image={Empty.PRESENTED_IMAGE_SIMPLE} {...emptyProps} />
|
|
</div>
|
|
)
|
|
}
|
|
if (customRenderChart) {
|
|
return customRenderChart(metric)
|
|
}
|
|
return (
|
|
<Chart
|
|
size={[, height]}
|
|
className={styles.vizChartItem}
|
|
ref={instance}
|
|
>
|
|
<Settings
|
|
pointerUpdateDebounce={0}
|
|
pointerUpdateTrigger="x"
|
|
onPointerUpdate={pointerUpdate}
|
|
showLegend
|
|
legendPosition={Position.Bottom}
|
|
onBrushEnd={handleChartBrush}
|
|
tooltip={{
|
|
headerFormatter: ({ value }) =>
|
|
`${formatter.full_dates(value)}`,
|
|
}}
|
|
debug={false}
|
|
/>
|
|
<Axis
|
|
id={`${metricKey}-bottom`}
|
|
position={Position.Bottom}
|
|
showOverlappingTicks
|
|
labelFormat={timeRange.timeFormatter}
|
|
tickFormat={timeRange.timeFormatter}
|
|
/>
|
|
{metricKey == "cluster_health" ? (
|
|
<Axis
|
|
id="cluster_health"
|
|
position={Position.Left}
|
|
tickFormat={(d) => Number(d).toFixed(0) + "%"}
|
|
/>
|
|
) : null}
|
|
|
|
{axis.map((item) => {
|
|
return (
|
|
<Axis
|
|
key={metricKey + "-" + item.id}
|
|
id={metricKey + "-" + item.id}
|
|
showGridLines={item.showGridLines}
|
|
groupId={item.group}
|
|
position={item.position}
|
|
ticks={item.ticks}
|
|
labelFormat={getFormatter(
|
|
item.formatType,
|
|
fixFormat(item.formatType, item.tickFormat)
|
|
)}
|
|
tickFormat={getFormatter(
|
|
item.formatType,
|
|
fixFormat(item.formatType, item.tickFormat)
|
|
)}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{lines.map((item) => {
|
|
if (item.type == "Bar") {
|
|
return (
|
|
<BarSeries
|
|
key={item.metric.label}
|
|
xScaleType={ScaleType.Time}
|
|
yScaleType={ScaleType.Linear}
|
|
xAccessor="x"
|
|
yAccessors={["y"]}
|
|
stackAccessors={["x"]}
|
|
splitSeriesAccessors={["g"]}
|
|
data={item.data}
|
|
color={({ specId, yAccessor, splitAccessors }) => {
|
|
const g = splitAccessors.get("g");
|
|
if (
|
|
yAccessor === "y"
|
|
|
|
) {
|
|
if( ["red", "yellow", "green"].includes(g)){
|
|
return g;
|
|
}
|
|
if(g == "online" || g == "available"){
|
|
return "green";
|
|
}
|
|
if(g == "offline" || g == "unavailable" || g == "N/A"){
|
|
return "gray";
|
|
}
|
|
|
|
}
|
|
return null;
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<LineSeries
|
|
key={item.metric.label}
|
|
id={item.metric.label}
|
|
groupId={item.metric.group}
|
|
timeZone={timezone}
|
|
color={item.color}
|
|
xScaleType={ScaleType.Time}
|
|
yScaleType={ScaleType.Linear}
|
|
xAccessor={0}
|
|
tickFormat={getFormatter(
|
|
item.metric.formatType,
|
|
fixFormat(item.metric.formatType, item.metric.format),
|
|
item.metric.units
|
|
)}
|
|
yAccessors={[1]}
|
|
data={item.data}
|
|
curve={CurveType.CURVE_MONOTONE_X}
|
|
/>
|
|
);
|
|
})}
|
|
</Chart>
|
|
)
|
|
}
|
|
|
|
const chartTitle = { title };
|
|
const lines = metric?.lines || [];
|
|
if (metricKey == "cluster_health") {
|
|
chartTitle.units = "%";
|
|
} else {
|
|
if (lines[0]?.metric) {
|
|
if (lines[0].metric.formatType.toLowerCase == "bytes") {
|
|
chartTitle.units = lines[0].metric.formatType;
|
|
} else {
|
|
chartTitle.units = lines[0].metric.units;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div key={metricKey} ref={containerRef} className={className} style={style}>
|
|
<Spin spinning={loading}>
|
|
<div className={styles.vizChartItemTitle}>
|
|
<span>
|
|
{chartTitle.title}
|
|
{chartTitle.units ? `(${chartTitle.units})` : ""}
|
|
</span>
|
|
{
|
|
<span>
|
|
{
|
|
timeInterval && (
|
|
<Tooltip title={formatMessage({id: "cluster.metrics.time_interval.reload"}, { time_interval: queryParams.bucket_size })}>
|
|
<Icon className={styles.copy} style={{ marginRight: 12 }} type="history" onClick={() => setTimeInterval()}/>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
{
|
|
metric?.request && (
|
|
<CopyToClipboard text={`GET .infini_metrics/_search\n${metric.request}`}>
|
|
<Tooltip title={formatMessage({id: "cluster.metrics.request.copy"})}>
|
|
<Icon
|
|
className={styles.copy}
|
|
style={{ marginRight: 12 }}
|
|
type="copy"
|
|
onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}
|
|
/>
|
|
</Tooltip>
|
|
</CopyToClipboard>
|
|
)
|
|
}
|
|
<Tooltip title={formatMessage({id: "form.button.refresh"})}>
|
|
<Icon className={styles.copy} type="sync" onClick={() => fetchData(...observerRef.current.deps, true)}/>
|
|
</Tooltip>
|
|
</span>
|
|
}
|
|
</div>
|
|
{renderChart()}
|
|
</Spin>
|
|
</div>
|
|
);
|
|
} |