UI updates, graph removals (#319)

UI cleanups. Remove repetitive labels in the UI, remove unused elements, 
remove graphs until we improve their utility.

- remove “Deployment” from the headers of the Deployment Detail Page
- remove Routes in sidebar
- kill leftmost 100px of sidebear
- remove word controller from service mesh page first table
- add twitter and GitHub and slack links
- kill the graphs, replace with one large header (request rate, success rate, latency top bar)
put upstream/downstream diagram before upstream downstream tables

* Clean up DeploymentList page (#321)

- remove "Most active deployments" graphs from the Deployments List page
- remove the scatterplot sections of the page as I don't think we'll be using them for a while
This commit is contained in:
Risha Mars 2018-02-12 12:44:33 -08:00 committed by GitHub
parent 261586b862
commit 1f6aa27922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 144 additions and 176 deletions

View File

@ -1,6 +1,7 @@
@import 'styles.css';
.entity-health {
margin-top: 32px;
margin-bottom: 32px;
& .metric-title {

View File

@ -0,0 +1,19 @@
@import 'styles.css';
.metric-summary {
& .metric {
padding-left: var(--base-width);
border-left: 4px solid var(--royalblue);
&.metric-large {
& .metric-title {
font-size: 12px;
font-weight: var(--font-weight-bold);
}
& .metric-value {
font-size: 18px;
font-weight: var(--font-weight-extra-bold);
}
}
}
}

View File

@ -1,7 +1,7 @@
@import 'styles.css';
.sidebar {
padding: 0px 0px 0px 20%;
padding: 0px 10% 0px 10%;
background-color: #091B39;
color: white;
min-height: 100vh;
@ -55,6 +55,17 @@
& .ant-menu-item-active, & .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
background: #007EFF;
}
& .social-links {
padding: 0 0 0 9px;
position: fixed;
height: 40px;
bottom: calc(var(--base-width)*16);
& a {
margin-right: calc(var(--base-width)*2);
}
}
}
.call-to-action .action {

View File

@ -2,12 +2,12 @@ import _ from 'lodash';
import ConduitSpinner from "./ConduitSpinner.jsx";
import ErrorBanner from './ErrorBanner.jsx';
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
import MetricsSummary from './MetricsSummary.jsx';
import PageHeader from './PageHeader.jsx';
import React from 'react';
import ResourceHealthOverview from './ResourceHealthOverview.jsx';
import ResourceMetricsOverview from './ResourceMetricsOverview.jsx';
import UpstreamDownstream from './UpstreamDownstream.jsx';
import { getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import { getPodsByDeployment, processRollupMetrics } from './util/MetricUtils.js';
import './../../css/deployment.css';
import 'whatwg-fetch';
@ -61,7 +61,7 @@ export default class DeploymentDetail extends React.Component {
let urls = this.api.urlsForResource;
let podListFetch = this.api.fetchPods();
let deployMetricsUrl = urls["deployment"].url(this.state.deploy).ts;
let deployMetricsUrl = urls["deployment"].url(this.state.deploy).rollup;
let upstreamRollupUrl = urls["upstream_deployment"].url(this.state.deploy).rollup;
let downstreamRollupUrl = urls["downstream_deployment"].url(this.state.deploy).rollup;
@ -70,11 +70,9 @@ export default class DeploymentDetail extends React.Component {
let downstreamFetch = this.api.fetchMetrics(downstreamRollupUrl);
// expose serverPromise for testing
this.serverPromise = Promise.all([
deployFetch, upstreamFetch, downstreamFetch, podListFetch
])
this.serverPromise = Promise.all([deployFetch, upstreamFetch, downstreamFetch, podListFetch])
.then(([deployMetrics, upstreamRollup, downstreamRollup, podList]) => {
let tsByDeploy = processTimeseriesMetrics(deployMetrics.metrics, "targetDeploy");
let deployRollup = processRollupMetrics(deployMetrics.metrics, "targetDeploy");
let upstreamMetrics = processRollupMetrics(upstreamRollup.metrics, "sourceDeploy");
let downstreamMetrics = processRollupMetrics(downstreamRollup.metrics, "targetDeploy");
@ -83,7 +81,8 @@ export default class DeploymentDetail extends React.Component {
this.setState({
added: deploy.added,
pods: deploy.pods,
deployTs: _.get(tsByDeploy, this.state.deploy, {}),
deployMetrics: _.get(deployRollup, 0, {}),
deployTs: {},
upstreamMetrics: upstreamMetrics,
downstreamMetrics: downstreamMetrics,
lastUpdated: Date.now(),
@ -114,6 +113,9 @@ export default class DeploymentDetail extends React.Component {
let currentSuccessRate = _.get(_.last(srTs), "value");
return [
<MetricsSummary
key="metrics-summary"
metrics={this.state.deployMetrics} />,
<ResourceHealthOverview
key="deploy-health-pane"
resourceName={this.state.deploy}
@ -122,13 +124,6 @@ export default class DeploymentDetail extends React.Component {
upstreamMetrics={this.state.upstreamMetrics}
downstreamMetrics={this.state.downstreamMetrics}
deploymentAdded={this.state.added} />,
_.isEmpty(this.state.deployTs) ? null :
<ResourceMetricsOverview
key="stat-pane"
resourceType="deployment"
lastUpdated={this.state.lastUpdated}
timeseries={this.state.deployTs}
window={this.api.getMetricsWindow()} />,
<UpstreamDownstream
key="deploy-upstream-downstream"
resourceType="deployment"

View File

@ -1,42 +1,24 @@
import _ from 'lodash';
import CallToAction from './CallToAction.jsx';
import ConduitSpinner from "./ConduitSpinner.jsx";
import DeploymentSummary from './DeploymentSummary.jsx';
import ErrorBanner from './ErrorBanner.jsx';
import PageHeader from './PageHeader.jsx';
import React from 'react';
import ScatterPlot from './ScatterPlot.jsx';
import TabbedMetricsTable from './TabbedMetricsTable.jsx';
import { Col, Row } from 'antd';
import { emptyMetric, getPodsByDeployment, processRollupMetrics, processTimeseriesMetrics } from './util/MetricUtils.js';
import { metricToFormatter, rowGutter } from './util/Utils.js';
import { emptyMetric, getPodsByDeployment, processRollupMetrics } from './util/MetricUtils.js';
import './../../css/deployments.css';
import 'whatwg-fetch';
const maxTsToFetch = 15; // Beyond this, stop showing sparklines in table
let nodeStats = (description, node) => (
<div>
<div className="title">{description}:</div>
<div>
{node.name} ({metricToFormatter["LATENCY"](_.get(node, ["latency", "P99"]))})
</div>
</div>
);
export default class DeploymentsList extends React.Component {
constructor(props) {
super(props);
this.api = this.props.api;
this.handleApiError = this.handleApiError.bind(this);
this.loadFromServer = this.loadFromServer.bind(this);
this.loadTimeseriesFromServer = this.loadTimeseriesFromServer.bind(this);
this.state = {
pollingInterval: 10000, // TODO: poll based on metricsWindow size
metrics: [],
timeseriesByDeploy: {},
lastUpdated: 0,
limitSparklineData: false,
pendingRequests: false,
loaded: false,
error: ''
@ -79,40 +61,14 @@ export default class DeploymentsList extends React.Component {
let meshDeploys = processRollupMetrics(rollup.metrics, "targetDeploy");
let combinedMetrics = this.addDeploysWithNoMetrics(poByDeploy, meshDeploys);
this.loadTimeseriesFromServer(meshDeploys, combinedMetrics);
})
.catch(this.handleApiError);
}
loadTimeseriesFromServer(meshDeployMetrics, combinedMetrics) {
// fetch only the timeseries for the 3 deployments we display at the top of the page
let limitSparklineData = _.size(meshDeployMetrics) > maxTsToFetch;
let resourceInfo = this.api.urlsForResource["deployment"];
let mostActiveDeployments = this.getMostActiveDeployments(meshDeployMetrics);
let tsPromises = _.map(mostActiveDeployments, dep => {
let tsPathForDeploy = resourceInfo.url(dep.name).ts;
return this.api.fetchMetrics(tsPathForDeploy);
});
Promise.all(tsPromises)
.then(tsMetrics => {
let mostActiveTs = _.reduce(tsMetrics, (mem, ea) => {
mem = mem.concat(ea.metrics);
return mem;
}, []);
let tsByDeploy = processTimeseriesMetrics(mostActiveTs, resourceInfo.groupBy);
this.setState({
timeseriesByDeploy: tsByDeploy,
lastUpdated: Date.now(),
metrics: combinedMetrics,
limitSparklineData: limitSparklineData,
loaded: true,
pendingRequests: false,
error: ''
});
}).catch(this.handleApiError);
})
.catch(this.handleApiError);
}
handleApiError(e) {
@ -122,87 +78,6 @@ export default class DeploymentsList extends React.Component {
});
}
getMostActiveDeployments(deployMetrics, limit = 3) {
return _(deployMetrics)
.filter('added')
.orderBy('requestRate', 'desc')
.take(limit)
.value();
}
renderPageContents() {
let mostActiveDeployments = this.getMostActiveDeployments(this.state.metrics);
return (
<div className="clearfix">
{_.isEmpty(mostActiveDeployments) ? null :
<div className="subsection-header">Most active deployments</div>}
<Row gutter={rowGutter}>
{
_.map(mostActiveDeployments, deployment => {
return (<Col span={8} key={`col-${deployment.name}`}>
<DeploymentSummary
key={deployment.name}
lastUpdated={this.state.lastUpdated}
data={deployment}
api = {this.api}
requestTs={_.get(this.state.timeseriesByDeploy,
[deployment.name, "REQUEST_RATE"], [])} />
</Col>);
})
}
</Row>
{/* <Row gutter={rowGutter}>
{ this.renderScatterplot() }
</Row> */}
<div className="deployments-list">
<TabbedMetricsTable
resource="deployment"
metrics={this.state.metrics}
api={this.api} />
</div>
</div>
);
}
renderScatterplot() {
let scatterplotData = _.reduce(this.state.metrics, (mem, datum) => {
if (!_.isNil(datum.successRate) && !_.isNil(datum.latency)) {
mem.push(datum);
}
return mem;
}, []);
let slowestNode = _.maxBy(scatterplotData, 'latency.P99');
let fastestNode = _.minBy(scatterplotData, 'latency.P99');
if (_.isEmpty(scatterplotData)) {
return null;
}
return (<div className="deployments-scatterplot">
<div className="scatterplot-info">
<div className="subsection-header">Success rate vs p99 latency</div>
</div>
<Row gutter={rowGutter}>
<Col span={8}>
<div className="scatterplot-display">
<div className="extremal-latencies">
{ !fastestNode ? null : nodeStats("Least latency", fastestNode) }
{ !slowestNode ? null : nodeStats("Most latency", slowestNode) }
</div>
</div>
</Col>
<Col span={16}><div className="scatterplot-chart">
<ScatterPlot
data={scatterplotData}
lastUpdated={this.state.lastUpdated}
containerClassName="scatterplot-chart" />
</div></Col>
</Row>
</div>);
}
render() {
return (
<div className="page-content">
@ -212,7 +87,12 @@ export default class DeploymentsList extends React.Component {
<PageHeader header="Deployments" api={this.api} />
{ _.isEmpty(this.state.metrics) ?
<CallToAction numDeployments={_.size(this.state.metrics)} /> :
this.renderPageContents()
<div className="deployments-list">
<TabbedMetricsTable
resource="deployment"
metrics={this.state.metrics}
api={this.api} />
</div>
}
</div>
}

View File

@ -0,0 +1,38 @@
import _ from 'lodash';
import { metricToFormatter } from './util/Utils.js';
import React from 'react';
import { Col, Row } from 'antd';
import './../../css/metric-summary.css';
export default class MetricsSummary extends React.Component {
render() {
return (
<Row className="metric-summary">
<Col span={4} className="metric metric-large">
<div className="metric-title">Request rate</div>
<div className="metric-value">
{metricToFormatter["REQUEST_RATE"](this.props.metrics.requestRate)}
</div>
</Col>
<Col span={4} className="metric metric-large">
<div className="metric-title">Success rate</div>
<div className="metric-value">
{metricToFormatter["SUCCESS_RATE"](this.props.metrics.successRate)}
</div>
</Col>
{
_.map(['P50', 'P95', 'P99'], label => {
let latency = _.get(this.props.metrics, ['latency', label]);
return (
<Col span={4} className="metric metric-large" key={`latency${label}`}>
<div className="metric-title">{label} latency</div>
<div className="metric-value">{metricToFormatter["LATENCY"](latency)}</div>
</Col>
);
})
}
</Row>
);
}
}

View File

@ -1,15 +0,0 @@
import React from 'react';
export default class Routes extends React.Component {
render() {
return (
<div className="page-content">
<div className="page-header">
<h1>Routes</h1>
</div>
<div>Coming soon!</div>
</div>
);
}
}

View File

@ -28,11 +28,11 @@ const serviceMeshDetailsColumns = [
];
const componentNames = {
"prometheus": "Prometheus",
"destination": "Controller Destination",
"proxy-api": "Controller Proxy API",
"public-api": "Controller Public API",
"tap": "Controller Tap",
"telemetry": "Controller Telemetry",
"destination": "Destination",
"proxy-api": "Proxy API",
"public-api": "Public API",
"tap": "Tap",
"telemetry": "Telemetry",
"web": "Web UI"
};

View File

@ -2,6 +2,7 @@ import _ from 'lodash';
import { getPodsByDeployment } from './util/MetricUtils.js';
import logo from './../../img/reversed_logo.png';
import React from 'react';
import SocialLinks from './SocialLinks.jsx';
import Version from './Version.jsx';
import { AutoComplete, Menu } from 'antd';
import './../../css/sidebar.css';
@ -82,14 +83,13 @@ export default class Sidebar extends React.Component {
<Menu.Item className="sidebar-menu-item" key="/deployments">
<ConduitLink to="/deployments">Deployments</ConduitLink>
</Menu.Item>
<Menu.Item className="sidebar-menu-item" key="/routes">
<ConduitLink to="/routes">Routes</ConduitLink>
</Menu.Item>
<Menu.Item className="sidebar-menu-item" key="/docs">
<ConduitLink to="https://conduit.io/docs/" absolute="true">Documentation</ConduitLink>
</Menu.Item>
</Menu>
<SocialLinks />
<Version
releaseVersion={this.props.releaseVersion}
uuid={this.props.uuid} />

View File

@ -0,0 +1,45 @@
import { Link } from 'react-router-dom';
import React from 'react';
const Twitter = () => {
return (
<svg
width="32" height="32"
version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 400 400" xmlSpace="preserve">
<g><circle className="st0" cx="200" cy="200" r="200" fill="#1DA1F2" /></g>
<g>
<path className="st1" d="M163.4,305.5c88.7,0,137.2-73.5,137.2-137.2c0-2.1,0-4.2-0.1-6.2c9.4-6.8,17.6-15.3,24.1-25 c-8.6,3.8-17.9,6.4-27.7,7.6c10-6,17.6-15.4,21.2-26.7c-9.3,5.5-19.6,9.5-30.6,11.7c-8.8-9.4-21.3-15.2-35.2-15.2 c-26.6,0-48.2,21.6-48.2,48.2c0,3.8,0.4,7.5,1.3,11c-40.1-2-75.6-21.2-99.4-50.4c-4.1,7.1-6.5,15.4-6.5,24.2 c0,16.7,8.5,31.5,21.5,40.1c-7.9-0.2-15.3-2.4-21.8-6c0,0.2,0,0.4,0,0.6c0,23.4,16.6,42.8,38.7,47.3c-4,1.1-8.3,1.7-12.7,1.7 c-3.1,0-6.1-0.3-9.1-0.9c6.1,19.2,23.9,33.1,45,33.5c-16.5,12.9-37.3,20.6-59.9,20.6c-3.9,0-7.7-0.2-11.5-0.7 C110.8,297.5,136.2,305.5,163.4,305.5" />
</g>
</svg>
);
};
const Slack = () => {
return (<svg width="32" height="32" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<path d="M165.964 15.838c-3.89-11.975-16.752-18.528-28.725-14.636-11.975 3.89-18.528 16.752-14.636 28.725l58.947 181.365c4.048 11.187 16.132 17.473 27.732 14.135 12.1-3.483 19.475-16.334 15.614-28.217L165.964 15.838" fill="#DFA22F" />
<path d="M74.626 45.516C70.734 33.542 57.873 26.989 45.9 30.879 33.924 34.77 27.37 47.631 31.263 59.606l58.948 181.366c4.047 11.186 16.132 17.473 27.732 14.132 12.099-3.481 19.474-16.332 15.613-28.217L74.626 45.516" fill="#3CB187" />
<path d="M240.162 166.045c11.975-3.89 18.526-16.75 14.636-28.726-3.89-11.973-16.752-18.527-28.725-14.636L44.708 181.632c-11.187 4.046-17.473 16.13-14.135 27.73 3.483 12.099 16.334 19.475 28.217 15.614l181.372-58.93" fill="#CE1E5B" />
<path d="M82.508 217.27l43.347-14.084-14.086-43.352-43.35 14.09 14.089 43.347" fill="#392538" />
<path d="M173.847 187.591c16.388-5.323 31.62-10.273 43.348-14.084l-14.088-43.36-43.35 14.09 14.09 43.354" fill="#BB242A" />
<path d="M210.484 74.706c11.974-3.89 18.527-16.751 14.637-28.727-3.89-11.973-16.752-18.526-28.727-14.636L15.028 90.293C3.842 94.337-2.445 106.422.896 118.022c3.481 12.098 16.332 19.474 28.217 15.613l181.371-58.93" fill="#72C5CD" />
<path d="M52.822 125.933c11.805-3.836 27.025-8.782 43.354-14.086-5.323-16.39-10.273-31.622-14.084-43.352l-43.36 14.092 14.09 43.346" fill="#248C73" />
<path d="M144.16 96.256l43.356-14.088a546179.21 546179.21 0 0 0-14.089-43.36L130.07 52.9l14.09 43.356" fill="#62803A" />
</svg>);
};
const Github = () => {
return <svg className="octicon-mark-github" fill="#ffffff" height="32" version="1.1" viewBox="0 0 16 16" width="32"><path fillRule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z" /></svg>;
};
export default class SocialLinks extends React.Component {
render() {
return (
<div className="social-links">
<Link target="_blank" to="https://github.com/runconduit/conduit"><Github /></Link>
<Link target="_blank" to="https://slack.linkerd.io/"><Slack /></Link>
<Link target="_blank" to="https://twitter.com/runconduit"><Twitter /></Link>
</div>
);
}
}

View File

@ -15,9 +15,7 @@ export default class UpstreamDownstreamTables extends React.Component {
numUpstreams === 0 ? null :
<div className="upstream-downstream-list">
<div className="border-container border-neutral subsection-header">
<div className="border-container-content subsection-header">
Upstream {this.props.resourceType}s: {numUpstreams}
</div>
<div className="border-container-content subsection-header">Upstreams</div>
</div>
<TabbedMetricsTable
resource={`upstream_${this.props.resourceType}`}
@ -30,9 +28,7 @@ export default class UpstreamDownstreamTables extends React.Component {
numDownstreams === 0 ? null :
<div className="upstream-downstream-list">
<div className="border-container border-neutral subsection-header">
<div className="border-container-content subsection-header">
Downstream {this.props.resourceType}s: {numDownstreams}
</div>
<div className="border-container-content subsection-header">Downstreams</div>
</div>
<TabbedMetricsTable
resource={`downstream_${this.props.resourceType}`}

View File

@ -5,7 +5,6 @@ import { Layout } from 'antd';
import NoMatch from './components/NoMatch.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
import Routes from './components/Routes.jsx';
import ServiceMesh from './components/ServiceMesh.jsx';
import Sidebar from './components/Sidebar.jsx';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
@ -36,7 +35,6 @@ ReactDOM.render((
<Route path={`${pathPrefix}/servicemesh`} render={() => <ServiceMesh api={api} releaseVersion={appData.releaseVersion} />} />
<Route path={`${pathPrefix}/deployments`} render={() => <DeploymentsList api={api} />} />
<Route path={`${pathPrefix}/deployment`} render={props => <DeploymentDetail api={api} location={props.location} />} />
<Route path={`${pathPrefix}/routes`} render={() => <Routes />} />
<Route component={NoMatch} />
</Switch>
</div>