New design for hover states and overflow handling

This commit is contained in:
Roland Schilter
2017-07-11 14:05:12 +02:00
parent 9dcdc65e66
commit 5a2593b5eb
8 changed files with 69 additions and 34 deletions

View File

@@ -54,6 +54,28 @@ class NodeDetails extends React.Component {
resetDocumentTitle();
}
static collectMetrics(details) {
const metrics = details.metrics || [];
// collect by metric id (id => link)
const metricLinks = (details.metric_links || [])
.reduce((agg, link) => Object.assign(agg, {[link.id]: link}), {});
const availableMetrics = metrics.reduce(
(agg, m) => Object.assign(agg, {[m.id]: true}),
{}
);
// append links with no metrics as fake metrics
(details.metric_links || []).forEach((link) => {
if (availableMetrics[link.id] === undefined) {
metrics.push({id: link.id, label: link.label});
}
});
return { metrics, metricLinks };
}
renderTools() {
const showSwitchTopology = this.props.nodeId !== this.props.selectedNodeId;
const topologyTitle = `View ${this.props.label} in ${this.props.topologyId}`;
@@ -165,13 +187,7 @@ class NodeDetails extends React.Component {
}
};
// collect by metric id (id => link)
const metricLinks = (details.metric_links || [])
.reduce((agg, link) => Object.assign(agg, {[link.id]: link}), {});
// collect links with no corresponding metric
const unattachedLinks = Object.assign({}, metricLinks);
(details.metrics || []).forEach(metric => delete unattachedLinks[metric.id]);
const { metrics, metricLinks } = NodeDetails.collectMetrics(details);
return (
<div className="node-details">
@@ -198,13 +214,13 @@ class NodeDetails extends React.Component {
</div>}
<div className="node-details-content">
{((details.metrics || []).length + Object.keys(unattachedLinks).length > 0) && <div className="node-details-content-section">
{metrics.length > 0 && <div className="node-details-content-section">
<div className="node-details-content-section-header">Status</div>
<NodeDetailsHealth
metrics={details.metrics}
metrics={metrics}
metricLinks={metricLinks}
unattachedLinks={unattachedLinks}
topologyId={topologyId}
nodeColor={nodeColor}
/>
</div>}
{details.metadata && <div className="node-details-content-section">

View File

@@ -10,12 +10,11 @@ function NodeDetailsHealthItem(props) {
{props.samples && <div className="node-details-health-item-sparkline">
<Sparkline
data={props.samples} max={props.max} format={props.format}
first={props.first} last={props.last} />
first={props.first} last={props.last} strokeWidth={props.strokeWidth}
strokeColor={props.strokeColor} />
</div>}
{!props.samples && <div className="node-details-health-item-placeholder"><span className="fa fa-circle-thin" /></div>}
<div className="node-details-health-item-label">
<div className="node-details-health-item-label" style={{ color: props.labelColor }}>
{props.label}
{props.icon && <span className={`fa ${props.icon}`} />}
</div>
</div>
);

View File

@@ -1,15 +1,29 @@
import React from 'react';
import { trackMixpanelEvent } from '../../utils/tracking-utils';
import { getMetricColor } from '../../utils/metric-utils';
import NodeDetailsHealthItem from './node-details-health-item';
export default class NodeDetailsHealthLinkItem extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
hovered: false
};
this.handleClick = this.handleClick.bind(this);
this.buildHref = this.buildHref.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
}
onMouseOver() {
this.setState({hovered: true});
}
onMouseOut() {
this.setState({hovered: false});
}
handleClick(ev, href) {
@@ -32,18 +46,29 @@ export default class NodeDetailsHealthLinkItem extends React.Component {
}
render() {
const { links, withoutGraph, id, ...props } = this.props;
const { links, id, nodeColor, ...props } = this.props;
const href = this.buildHref(links[id] && links[id].url);
if (!href) return <NodeDetailsHealthItem {...props} />;
const hasData = (props.samples && props.samples.length > 0) || props.value !== undefined;
const labelColor = this.state.hovered && !hasData ? nodeColor : undefined;
const sparkline = {};
if (this.state.hovered) {
sparkline.strokeColor = getMetricColor(id);
sparkline.strokeWidth = '2px';
}
return (
<a
className="node-details-health-link-item"
href={href}
onClick={e => this.handleClick(e, href)}>
{!withoutGraph && <NodeDetailsHealthItem {...props} icon="fa-expand" />}
{withoutGraph && <NodeDetailsHealthItem label={props.label} icon="fa-expand" />}
onClick={e => this.handleClick(e, href)}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}>
<NodeDetailsHealthItem
{...props}
{...sparkline}
labelColor={labelColor} />
</a>
);
}

View File

@@ -6,7 +6,7 @@ function NodeDetailsHealthOverflowItem(props) {
return (
<div className="node-details-health-overflow-item">
<div className="node-details-health-overflow-item-value">
{formatMetric(props.value, props)}
{props.value !== undefined && formatMetric(props.value, props)}
</div>
<div className="node-details-health-overflow-item-label truncate">{props.label}</div>
</div>

View File

@@ -18,7 +18,7 @@ export default class NodeDetailsHealthOverflow extends React.Component {
const items = this.props.items.slice(0, 4);
return (
<div className="node-details-health-overflow" onClick={this.handleClick}>
<div className="node-details-health-overflow" onClick={this.handleClick} title="Expand metrics">
{items.map(item => <NodeDetailsHealthOverflowItem key={item.id} {...item} />)}
</div>
);

View File

@@ -25,8 +25,8 @@ export default class NodeDetailsHealth extends React.Component {
const {
metrics = makeList(),
metricLinks = makeMap(),
unattachedLinks = makeMap(),
topologyId,
nodeColor,
} = this.props;
const primeCutoff = metrics.length > 3 && !this.state.expanded ? 2 : metrics.length;
@@ -45,6 +45,7 @@ export default class NodeDetailsHealth extends React.Component {
{...item}
links={metricLinks}
topologyId={topologyId}
nodeColor={nodeColor}
/>
</CloudFeature>)}
{showOverflow && <NodeDetailsHealthOverflow
@@ -55,17 +56,6 @@ export default class NodeDetailsHealth extends React.Component {
<ShowMore
handleClick={this.handleClickMore} collection={this.props.metrics}
expanded={this.state.expanded} notShown={notShown} hideNumber />
<div className="node-details-health-wrapper">
{Object.keys(unattachedLinks).map(id => <CloudFeature alwaysShow key={id}>
<NodeDetailsHealthLinkItem
withoutGraph
{...unattachedLinks[id]}
links={unattachedLinks}
topologyId={topologyId}
/>
</CloudFeature>)}
</div>
</div>
);
}

View File

@@ -52,7 +52,9 @@ export function getMetricValue(metric) {
export function getMetricColor(metric) {
const metricId = metric && metric.get('id');
const metricId = typeof metric === 'string'
? metric
: metric && metric.get('id');
if (/mem/.test(metricId)) {
return 'steelBlue';
} else if (/cpu/.test(metricId)) {

View File

@@ -926,11 +926,14 @@
&-item {
padding: 8px 16px;
width: 33%;
display: flex;
flex-direction: column;
&-label {
color: $text-secondary-color;
text-transform: uppercase;
font-size: 80%;
margin-top: auto;
.fa {
margin-left: 0.5em;