blob: 99191b33cc903627d8230bd578b3280b57d00541 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, Icon, Radio, Avatar, Select, Input, Popover, Tag } from 'antd';
import { ChartCard } from '../../components/Charts';
import { AppTopology } from '../../components/Topology';
import { Panel } from '../../components/Page';
import ApplicationLitePanel from '../../components/ApplicationLitePanel';
import DescriptionList from '../../components/DescriptionList';
import { redirect } from '../../utils/utils';
const { Description } = DescriptionList;
const { Option } = Select;
const colResponsiveProps = {
xs: 24,
sm: 24,
md: 24,
lg: 12,
xl: 12,
style: { marginTop: 8 },
};
const layouts = [
{
name: 'dagre',
icon: 'img/icon/dagre.png',
rankDir: 'LR',
minLen: 4,
animate: true,
},
{
name: 'concentric',
icon: 'img/icon/concentric.png',
minNodeSpacing: 10,
animate: true,
},
{
name: 'cose-bilkent',
icon: 'img/icon/cose.png',
idealEdgeLength: 200,
edgeElasticity: 0.1,
randomize: false,
},
];
const layoutButtonStyle = { height: '90%', verticalAlign: 'middle', paddingBottom: 2 };
@connect(state => ({
topology: state.topology,
duration: state.global.duration,
globalVariables: state.global.globalVariables,
}))
export default class Topology extends PureComponent {
static defaultProps = {
graphHeight: 600,
};
findValue = (id, values) => {
const v = values.find(_ => _.id === id);
if (v) {
return v.value;
}
return null;
}
handleChange = (variables) => {
const { dispatch } = this.props;
dispatch({
type: 'topology/fetchData',
payload: { variables },
});
}
handleLayoutChange = ({ target: { value } }) => {
const { dispatch } = this.props;
dispatch({
type: 'topology/saveData',
payload: { layout: value },
});
}
handleLoadMetrics = (ids, idsS, idsC) => {
const { dispatch, globalVariables: { duration } } = this.props;
dispatch({
type: 'topology/fetchMetrics',
payload: { variables: {
duration,
ids,
idsS,
idsC,
}},
});
}
handleSelectedApplication = (appInfo) => {
const { dispatch, topology: { data: { metrics: { sla, nodeCpm, nodeLatency } } } } = this.props;
if (appInfo) {
dispatch({
type: 'topology/saveData',
payload: { appInfo: { ...appInfo,
sla: this.findValue(appInfo.id, sla.values),
cpm: this.findValue(appInfo.id, nodeCpm.values),
avgResponseTime: this.findValue(appInfo.id, nodeLatency.values),
} },
});
} else {
dispatch({
type: 'topology/saveData',
payload: { appInfo: null },
});
}
}
handleChangeLatencyStyle = (e) => {
const { value } = e.target;
const vArray = value.split(',');
if (vArray.length !== 2) {
return;
}
const latencyRange = vArray.map(_ => parseInt(_.trim(), 10)).filter(_ => !isNaN(_));
if (latencyRange.length !== 2) {
return;
}
const { dispatch } = this.props;
dispatch({
type: 'topology/setLatencyStyleRange',
payload: { latencyRange },
});
}
handleFilterApplication = (aa) => {
const { dispatch } = this.props;
dispatch({
type: 'topology/filterApplication',
payload: { aa },
});
}
renderActions = () => {
const {...propsData} = this.props;
const { data: { appInfo } } = propsData.topology;
return [
<Icon type="appstore" onClick={() => redirect(propsData.history, '/monitor/service', { key: appInfo.id, label: appInfo.name })} />,
<Icon
type="exception"
onClick={() => redirect(propsData.history, '/trace',
{ values: {
serviceId: appInfo.id,
duration: { ...propsData.duration, input: propsData.globalVariables.duration },
},
labels: { applicationId: appInfo.name },
})}
/>,
appInfo.isAlarm ? <Icon type="bell" onClick={() => redirect(propsData.history, '/monitor/alarm')} /> : null,
];
}
renderNodeType = (topologData) => {
const typeMap = new Map();
topologData.nodes.forEach((_) => {
if (typeMap.has(_.type)) {
typeMap.set(_.type, typeMap.get(_.type) + 1);
} else {
typeMap.set(_.type, 1);
}
});
const result = [];
typeMap.forEach((v, k) => result.push(<Description term={k}>{v}</Description>));
return result;
}
render() {
const {...propsData} = this.props;
const { data, variables: { appRegExps, appFilters = [], latencyRange } } = propsData.topology;
const { metrics, layout = 0 } = data;
const { getGlobalTopology: topologData } = data;
const content = (
<div>
<p><Tag color="#40a9ff">Less than {latencyRange[0]} ms </Tag></p>
<p><Tag color="#d4b106">Between {latencyRange[0]} ms and {latencyRange[1]} ms</Tag></p>
<p><Tag color="#cf1322">More than {latencyRange[1]} ms</Tag></p>
</div>
);
return (
<Panel globalVariables={propsData.globalVariables} onChange={this.handleChange}>
<Row gutter={8}>
<Col {...{ ...colResponsiveProps, xl: 18, lg: 16 }}>
<ChartCard
title="Topology Map"
avatar={<Avatar icon="fork" style={{ color: '#1890ff', backgroundColor: '#ffffff' }} />}
action={(
<Radio.Group value={layout} onChange={this.handleLayoutChange} size="normal">
{layouts.map((_, i) => (
<Radio.Button value={i} key={_.name}>
<img src={_.icon} alt={_.name} style={layoutButtonStyle} />
</Radio.Button>))}
</Radio.Group>
)}
>
{topologData.nodes.length > 0 ? (
<AppTopology
height={propsData.graphHeight}
elements={topologData}
metrics={metrics}
onSelectedApplication={this.handleSelectedApplication}
onLoadMetircs={this.handleLoadMetrics}
layout={layouts[layout]}
latencyRange={latencyRange}
appRegExps={appRegExps}
/>
) : null}
</ChartCard>
</Col>
<Col {...{ ...colResponsiveProps, xl: 6, lg: 8 }}>
{data.appInfo ? (
<Card
title={data.appInfo.name}
bodyStyle={{ height: 568 }}
actions={this.renderActions()}
>
<ApplicationLitePanel appInfo={data.appInfo} />
</Card>
)
: (
<Card title="Overview" style={{ height: 672 }}>
<Select
mode="tags"
style={{ width: '100%', marginBottom: 20 }}
placeholder="Filter application"
onChange={this.handleFilterApplication}
tokenSeparators={[',']}
value={appFilters}
>
{data.getGlobalTopology.nodes.filter(_ => _.isReal)
.map(_ => <Option key={_.name}>{_.name}</Option>)}
</Select>
<Popover content={content} title="Info">
<h4>Latency coloring thresholds <Icon type="info-circle-o" /></h4>
</Popover>
<Input style={{ width: '100%', marginBottom: 20 }} onChange={this.handleChangeLatencyStyle} value={latencyRange.join(',')} />
<h4>Overview</h4>
<DescriptionList layout="vertical">
<Description term="Total">{topologData.nodes.length}</Description>
{this.renderNodeType(topologData)}
</DescriptionList>
</Card>
)}
</Col>
</Row>
</Panel>
);
}
}