diff --git a/web/src/components/Graph/Graph/Legend.tsx b/web/src/components/Graph/Graph/Legend.tsx index 5ffa43d4..34bb177b 100644 --- a/web/src/components/Graph/Graph/Legend.tsx +++ b/web/src/components/Graph/Graph/Legend.tsx @@ -3,6 +3,7 @@ import { Table, Input, Button, Modal } from 'antd'; import { ColumnProps, TableRowSelection } from 'antd/es/table'; import Color from 'color'; import _ from 'lodash'; +import { injectIntl } from 'react-intl'; import clipboard from '@common/clipboard'; import ContextMenu from '@cpts/ContextMenu'; import { SerieInterface, PointInterface } from '../interface'; @@ -35,7 +36,7 @@ interface LegendDataItem extends SerieInterface { last: number | null, } -export default class Legend extends Component { +class Legend extends Component { static defaultProps = { style: {}, series: [], @@ -125,7 +126,7 @@ export default class Legend extends Component { } render() { - const { onSelectedChange } = this.props; + const { comparisonOptions, onSelectedChange } = this.props; const { searchText, selectedKeys, highlightedKeys } = this.state; const counterSelectedKeys = highlightedKeys; const data = this.filterData(); @@ -148,7 +149,7 @@ export default class Legend extends Component { filterDropdownVisible: this.state.filterDropdownVisible, onFilterDropdownVisibleChange: (visible: boolean) => this.setState({ filterDropdownVisible: visible }), render: (text, record) => { - const legendName = getLengendName(record); + const legendName = getLengendName(record, comparisonOptions, this.props.intl); return ( { export function normalizeLegendData(series: SerieInterface[] = []) { const tableData = _.map(series, (serie) => { - const { id, metric, tags, data } = serie; + const { id, metric, tags, data, comparison } = serie; const { last, avg, max, min, sum } = getLegendNums(data); return { id, metric, tags, + comparison, last, avg, max, @@ -348,9 +350,17 @@ function getLegendNums(points: PointInterface[]) { return { last, avg, max, min, sum }; } -function getLengendName(serie: SerieInterface) { - const { tags } = serie; +function getLengendName(serie: SerieInterface, comparisonOptions: any, intl: any) { + const { tags, comparison } = serie; let lname = tags; + // display comparison + if (comparison && typeof comparison === 'number') { + const currentComparison = _.find(comparisonOptions, { value: `${comparison}000` }); + if (currentComparison && currentComparison.label) { + const postfix = intl.locale === 'en' ? currentComparison.labelEn : `环比${currentComparison.label}`; + lname += ` ${postfix}`; + } + } // shorten name if (lname.length > 80) { const leftStr = lname.substr(0, 40); @@ -369,3 +379,5 @@ function isEqualSeries(series: SerieInterface[], nextSeries: SerieInterface[]) { }); return _.isEqual(pureSeries, pureNextSeries); } + +export default injectIntl(Legend); diff --git a/web/src/components/Graph/Graph/index.tsx b/web/src/components/Graph/Graph/index.tsx index 4c527a62..0a6956f1 100644 --- a/web/src/components/Graph/Graph/index.tsx +++ b/web/src/components/Graph/Graph/index.tsx @@ -93,6 +93,7 @@ export default class Graph extends Component { || aggrFuncChanged || aggrGroupChanged || consolFuncChanged + || !_.isEqual(nextData.comparison, thisData.comparison) ) { const isFetchCounter = selectedNsChanged || selectedMetricChanged || selectedTagkvChanged; this.fetchData(nextProps.data, isFetchCounter, (series: SerieInterface[]) => { @@ -256,6 +257,8 @@ export default class Graph extends Component { return util.getTooltipsContent({ points, chartWidth: this.graphWrapEle.offsetWidth - 40, + comparison: graphConfig.comparison, + isComparison: !!_.get(graphConfig.comparison, 'length'), }); }, }, @@ -292,6 +295,8 @@ export default class Graph extends Component { return util.getTooltipsContent({ points, chartWidth: this.graphWrapEle.offsetWidth - 40, + comparison: graphConfig.comparison, + isComparison: !!_.get(graphConfig.comparison, 'length'), }); }, }, @@ -391,6 +396,7 @@ export default class Graph extends Component { style={{ display: graphConfig.legend ? 'block' : 'none' }} series={this.getZoomedSeries()} onSelectedChange={this.handleLegendRowSelectedChange} + comparisonOptions={graphConfig.comparisonOptions} /> ); diff --git a/web/src/components/Graph/GraphConfig/Comparison.tsx b/web/src/components/Graph/GraphConfig/Comparison.tsx new file mode 100644 index 00000000..453f8b8d --- /dev/null +++ b/web/src/components/Graph/GraphConfig/Comparison.tsx @@ -0,0 +1,206 @@ +import React, { Component } from 'react'; +import _ from 'lodash'; +import moment from 'moment'; +import { Icon, Button, Select, Popover, Input, InputNumber } from 'antd'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import { injectIntl } from 'react-intl'; + +interface Props { + size: 'small' | 'default' | 'large' | undefined, + comparison: string[], + relativeTimeComparison: boolean, + comparisonOptions: any[], + graphConfig: any, + onChange: (values: any) => void, +} + +interface State { + customValue?: number, + customType: string, + errorText: string, +} + +const Option = Select.Option; +const customTypeOptions = [ + { + value: 'hour', + label: '小时', + labelEn: 'hour', + ms: 3600000, + }, { + value: 'day', + label: '天', + labelEn: 'day', + ms: 86400000, + }, +]; + +class Comparison extends Component { + static defaultProps = { + size: 'small', + comparison: [], + relativeTimeComparison: false, + comparisonOptions: [], + graphConfig: null, + onChange: _.noop, + }; + + constructor(props: Props) { + super(props); + this.state = { + customValue: undefined, // 自定义环比值(不带单位) + customType: 'hour', // 自定义环比值单位 hour | day + errorText: '', // 错误提示文本 + }; + } + + refresh = () => { + const { graphConfig } = this.props; + if (graphConfig) { + const now = moment(); + const start = (Number(now.format('x')) - Number(graphConfig.end)) + Number(graphConfig.start) + ''; + const end = now.format('x'); + + return { now: end, start, end }; + } + return {}; + } + + handleComparisonChange = (value: string[]) => { + const { onChange, relativeTimeComparison, comparisonOptions } = this.props; + onChange({ + ...this.refresh(), + comparison: value, + relativeTimeComparison, + comparisonOptions, + }); + } + + handleRelativeTimeComparisonChange = (e: CheckboxChangeEvent) => { + const { onChange, comparison, comparisonOptions } = this.props; + onChange({ + ...this.refresh(), + comparison, + relativeTimeComparison: e.target.checked, + comparisonOptions, + }); + } + + handleCustomValueChange = (value: number | undefined) => { + if (value) { + this.setState({ + customValue: value, + errorText: '', + }); + } else { + this.setState({ + customValue: value, + errorText: 'Custom value is required', + }); + } + } + + handleCustomTypeChange = (value: string) => { + this.setState({ customType: value }); + } + + handleCustomBtnClick = () => { + const { onChange, comparison, relativeTimeComparison, comparisonOptions } = this.props; + const { customValue, customType } = this.state; + const currentCustomTypeObj = _.find(customTypeOptions, { value: customType }); + + if (!customValue || !currentCustomTypeObj) { + this.setState({ + errorText: 'Custom value is required', + }); + } else { + this.setState({ + errorText: '', + }, () => { + const ms = currentCustomTypeObj.ms * customValue; + const comparisonOptionsClone = _.cloneDeep(comparisonOptions); + const comparisonClone = _.cloneDeep(comparison); + comparisonClone.push(_.toString(ms)); + comparisonOptionsClone.push({ + label: `${customValue}${currentCustomTypeObj.label}`, + value: _.toString(ms), + }); + const newComparisonOptions = _.unionBy(comparisonOptionsClone, 'value'); + onChange({ + ...this.refresh(), + comparison: comparisonClone, + relativeTimeComparison, + comparisonOptions: newComparisonOptions, + }); + }); + } + } + + render() { + const { size, comparison, comparisonOptions } = this.props; + const { customValue, customType, errorText } = this.state; + console.log(this.props.intl.locale); + const addonUid = _.uniqueId('inputNumber-addon-'); + return ( +
+ + +
+ + + + + + +
+ +

{errorText}

+
+ }> + + + + + + ); + } +} + +export default injectIntl(Comparison); diff --git a/web/src/components/Graph/GraphConfig/GraphConfigForm.tsx b/web/src/components/Graph/GraphConfig/GraphConfigForm.tsx index bba1c24a..67aefefb 100644 --- a/web/src/components/Graph/GraphConfig/GraphConfigForm.tsx +++ b/web/src/components/Graph/GraphConfig/GraphConfigForm.tsx @@ -9,6 +9,7 @@ import { normalizeTreeData, renderTreeNodes } from '@cpts/Layout/utils'; import request from '@common/request'; import api from '@common/api'; import Tagkv from './Tagkv'; +import Comparison from './Comparison'; import * as config from '../config'; import { getTimeLabelVal } from '../util'; import hasDtag from '../util/hasDtag'; @@ -213,6 +214,18 @@ export default class GraphConfigForm extends Component { } } + handleCommonFieldChange = (changedObj) => { + const newChangedObj = {}; + _.each(changedObj, (val, key) => { + newChangedObj[key] = { + $set: val, + }; + }); + this.setState(update(this.state, { + graphConfig: newChangedObj, + })); + } + handleNsChange = async (selectedNid: number[], currentMetricObj: MetricInterface) => { try { this.setLoading(true); @@ -835,6 +848,30 @@ export default class GraphConfigForm extends Component { ] : false } + } + style={{ marginBottom: 0 }} + > + { + this.handleCommonFieldChange({ + start: values.start, + end: values.end, + now: values.now, + comparison: values.comparison, + relativeTimeComparison: values.relativeTimeComparison, + comparisonOptions: values.comparisonOptions, + }); + }} + /> + {this.renderMetrics()} { }); } + handleComparisonChange = (values: any) => { + const { data, onChange } = this.props; + onChange('update', data.id, { + start: values.start, + end: values.end, + now: values.now, + comparison: values.comparison, + relativeTimeComparison: values.relativeTimeComparison, + comparisonOptions: values.comparisonOptions, + }); + } + handleconsolFuncChange = (val: string) => { const { data, onChange } = this.props; onChange('update', data.id, { @@ -178,7 +191,7 @@ export default class GraphConfigInner extends Component { render() { const { data, onChange } = this.props; - const { now, start, end } = data; + const { now, start, end, comparison } = data; const timeLabel = now === end ? util.getTimeLabelVal(start, end, 'label') : '其他'; const timeVal = now === end ? util.getTimeLabelVal(start, end, 'value') : 'custom'; const datePickerStartVal = moment(Number(start)).format(config.timeFormatMap.moment); @@ -284,21 +297,23 @@ export default class GraphConfigInner extends Component { : null } - {/*
- 采样函数: - -
*/} +
+ : + + +
Legend diff --git a/web/src/components/Graph/config.tsx b/web/src/components/Graph/config.tsx index 919d29ca..2c1e24a7 100644 --- a/web/src/components/Graph/config.tsx +++ b/web/src/components/Graph/config.tsx @@ -2,21 +2,26 @@ import PropTypes from 'prop-types'; import moment from 'moment'; const now = moment(); -export const comparison = [ +export const comparisonOptions = [ { label: '1小时', + labelEn: '1 hour', value: '3600000', }, { label: '2小时', + labelEn: '2 hours', value: '7200000', }, { label: '1天', + labelEn: '1 day', value: '86400000', }, { label: '2天', + labelEn: '2 days', value: '172800000', }, { label: '7天', + labelEn: '7 days', value: '604800000', }, ]; @@ -74,6 +79,7 @@ export const graphDefaultConfig = { now: now.clone().format('x'), start: now.clone().subtract(3600000, 'ms').format('x'), end: now.clone().format('x'), + comparisonOptions, threshold: undefined, legend: false, shared: false, diff --git a/web/src/components/Graph/util/getTooltipsContent.tsx b/web/src/components/Graph/util/getTooltipsContent.tsx index 56caeed1..ad67fa4e 100644 --- a/web/src/components/Graph/util/getTooltipsContent.tsx +++ b/web/src/components/Graph/util/getTooltipsContent.tsx @@ -32,20 +32,38 @@ export default function getTooltipsContent(activeTooltipData: ActiveTooltipData) tooltipContent += getHeaderStr(activeTooltipData); _.each(sortedPoints, (point) => { - tooltipContent += singlePoint(point); + tooltipContent += singlePoint(point, activeTooltipData); }); return `
${tooltipContent}
`; } -function singlePoint(pointData = {} as PointInterface) { - const { color, filledNull, serieOptions = {} } = pointData; - const { tags } = serieOptions as any; +function singlePoint(pointData = {}, activeTooltipData) { + const { color, filledNull, serieOptions = {}, timestamp } = pointData; + const { comparison: comparisons, isComparison } = activeTooltipData; + const { tags } = serieOptions; const value = numeral(pointData.value).format('0,0[.]000'); + let name = tags; + + // 对比情况下 name 特殊处理 + if (isComparison) { + const mDate = serieOptions.comparison && typeof serieOptions.comparison === 'number' ? moment(timestamp).subtract(serieOptions.comparison, 'seconds') : moment(timestamp); + const isAllDayLevelComparison = _.every(comparisons, (o) => { + return _.isInteger(Number(o) / 86400000); + }); + + if (isAllDayLevelComparison) { + const dateStr = mDate.format('YYYY-MM-DD'); + name = `${dateStr}`; + } else { + const dateStr = mDate.format(fmt); + name = `${dateStr} ${name}`; + } + } return ( ` - ${_.escape(tags)}:${value}${filledNull ? '(空值填补,仅限看图使用)' : ''}
` + ${name}:${value}${filledNull ? '(空值填补,仅限看图使用)' : ''}
` ); } diff --git a/web/src/components/Graph/util/normalizeEndpointCounters.tsx b/web/src/components/Graph/util/normalizeEndpointCounters.tsx index d48b92e4..c56fb751 100644 --- a/web/src/components/Graph/util/normalizeEndpointCounters.tsx +++ b/web/src/components/Graph/util/normalizeEndpointCounters.tsx @@ -5,7 +5,7 @@ export function transformMsToS(ts: string) { return Number(ts.substring(0, ts.length - 3)); } -export function processComparison(comparison: number[]) { +export function processComparison(comparison: string[]) { const newComparison = [0]; _.each(comparison, (o) => { newComparison.push(transformMsToS(String(o))); @@ -14,6 +14,7 @@ export function processComparison(comparison: number[]) { } export default function normalizeEndpointCounters(graphConfig: GraphDataInterface, counterList: CounterInterface[]) { + const newComparison = processComparison(graphConfig.comparison); const firstMetric = _.get(graphConfig, 'metrics[0]', {}); const { aggrFunc, aggrGroup: groupKey, consolFunc } = firstMetric; const start = transformMsToS(_.toString(graphConfig.start)); @@ -27,6 +28,7 @@ export default function normalizeEndpointCounters(graphConfig: GraphDataInterfac aggrFunc, groupKey, consolFunc, + comparisons: newComparison, }; }); diff --git a/web/src/components/Graph/util/normalizeSeries.tsx b/web/src/components/Graph/util/normalizeSeries.tsx index 357b2e9d..62873d29 100644 --- a/web/src/components/Graph/util/normalizeSeries.tsx +++ b/web/src/components/Graph/util/normalizeSeries.tsx @@ -5,7 +5,7 @@ import { SerieInterface } from '../interface'; export default function normalizeSeries(data: any[]) { const series = [] as SerieInterface[]; _.each(_.sortBy(data, ['counter', 'endpoint']), (o, i) => { - const { endpoint } = o; + const { endpoint, comparison } = o; const color = getSerieColor(o, i); const separatorIdx = o.counter.indexOf('/'); @@ -23,6 +23,7 @@ export default function normalizeSeries(data: any[]) { lineWidth: 2, color, oldColor: color, + comparison, } as SerieInterface; series.push(serie); }); diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 92cd346b..94113cdf 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -260,6 +260,7 @@ export default { 'graph.config.aggr.max': 'max', 'graph.config.aggr.min': 'min', 'graph.config.aggr.group': 'groupBy', + 'graph.config.comparison': 'comparison', 'graph.config.series': 'series', 'graph.config.series.unit': 'pcs', 'graph.config.cate': 'cate', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 913fc4e6..1008667b 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -261,6 +261,7 @@ export default { 'graph.config.aggr.max': '最大值', 'graph.config.aggr.min': '最小值', 'graph.config.aggr.group': '聚合维度', + 'graph.config.comparison': '环比', 'graph.config.series': '曲线', 'graph.config.series.unit': '条', 'graph.config.cate': '分类',