mirror of https://github.com/linkerd/linkerd2.git
Add per namespace pages that show all resource stats for a namespace (#893)
Add namespaces as a top level resource in the Web UI This PR does the following: - Replace the deployments table in the service mesh page with namespaces - Add a Namespaces index page that lists all namespaces and their stats - Add an individual namespace page showing all resources for that namespace - Make the incomplete mesh message more generic to any resource type - Revamp rest of service mesh page to move off ListPods
This commit is contained in:
parent
63fbbd6931
commit
1b0f269a43
|
@ -31,6 +31,36 @@
|
|||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.conduit-table.mesh-completion-table {
|
||||
& .ant-table-row {
|
||||
& td {
|
||||
& .container-bar {
|
||||
&.neutral {
|
||||
background-color: rgb(130,130,130, 0.1);;
|
||||
}
|
||||
&.poor{
|
||||
background-color: rgb(235, 87, 87, 0.1);
|
||||
}
|
||||
&.good{
|
||||
background-color: rgb(39, 174, 96, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
& .inner-bar {
|
||||
&.neutral {
|
||||
background-color: rgb(130,130,130, 0.8);;
|
||||
}
|
||||
&.poor{
|
||||
background-color: rgb(235, 87, 87, 0.8);
|
||||
}
|
||||
&.good{
|
||||
background-color: rgb(39, 174, 96, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* styles for the StatusTable */
|
||||
td .status-dot {
|
||||
float: left;
|
||||
|
|
|
@ -72,6 +72,10 @@ h2, h3, h4, h5, h6 {
|
|||
float: right;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: calc(var(--base-width) * 3);
|
||||
}
|
||||
|
||||
.subsection-header {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import React from 'react';
|
||||
import './../../css/cta.css';
|
||||
|
||||
export default class CallToAction extends React.Component {
|
||||
render() {
|
||||
let resource = this.props.resource || "resource";
|
||||
|
||||
return (
|
||||
<div className="call-to-action">
|
||||
<div className="action summary">The service mesh was successfully installed!</div>
|
||||
|
@ -20,14 +23,14 @@ export default class CallToAction extends React.Component {
|
|||
<div className="icon-container">
|
||||
<i className="fa fa-check-circle" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="message">{this.props.numDeployments || 0} deployments detected</div>
|
||||
<div className="message">{_.isNil(this.props.numResources) ? "No" : this.props.numResources} {resource}s detected</div>
|
||||
</div>
|
||||
|
||||
<div className="step-container incomplete">
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-circle-o" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="message">Connect your first deployment</div>
|
||||
<div className="message">Connect your first {resource}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import GrafanaLink from './GrafanaLink.jsx';
|
||||
import { metricToFormatter } from './util/Utils.js';
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { metricToFormatter, numericSort } from './util/Utils.js';
|
||||
|
||||
/*
|
||||
Table to display Success Rate, Requests and Latency in tabs.
|
||||
|
@ -40,7 +40,7 @@ const formatTitle = (title, tooltipText) => {
|
|||
|
||||
};
|
||||
const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, ConduitLink) => {
|
||||
return [
|
||||
let nsColumn = [
|
||||
{
|
||||
title: formatTitle("Namespace"),
|
||||
key: "namespace",
|
||||
|
@ -49,17 +49,29 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick,
|
|||
onFilterDropdownVisibleChange: onFilterClick,
|
||||
onFilter: (value, row) => row.namespace.indexOf(value) === 0,
|
||||
sorter: sortable ? (a, b) => (a.namespace || "").localeCompare(b.namespace) : false
|
||||
},
|
||||
}
|
||||
];
|
||||
let columns = [
|
||||
{
|
||||
title: formatTitle(resource),
|
||||
key: "name",
|
||||
defaultSortOrder: 'ascend',
|
||||
sorter: sortable ? (a, b) => (a.name || "").localeCompare(b.name) : false,
|
||||
render: row => row.added ? <GrafanaLink
|
||||
name={row.name}
|
||||
namespace={row.namespace}
|
||||
resource={resource}
|
||||
conduitLink={ConduitLink} /> : row.name
|
||||
render: row => {
|
||||
if (resource.toLowerCase() === "namespace") {
|
||||
return <ConduitLink to={"/namespaces/" + row.name}>{row.name}</ConduitLink>;
|
||||
} else if (!row.added) {
|
||||
return row.name;
|
||||
} else {
|
||||
return (
|
||||
<GrafanaLink
|
||||
name={row.name}
|
||||
namespace={row.namespace}
|
||||
resource={resource}
|
||||
conduitLink={ConduitLink} />
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: formatTitle("SR", "Success Rate"),
|
||||
|
@ -102,9 +114,13 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick,
|
|||
render: metricToFormatter["LATENCY"]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const numericSort = (a, b) => (_.isNil(a) ? -1 : a) - (_.isNil(b) ? -1 : b);
|
||||
if (resource.toLowerCase() === "namespace") {
|
||||
return columns;
|
||||
} else {
|
||||
return _.concat(nsColumn, columns);
|
||||
}
|
||||
};
|
||||
|
||||
export default class MetricsTable extends BaseTable {
|
||||
constructor(props) {
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import _ from 'lodash';
|
||||
import CallToAction from './CallToAction.jsx';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import { processRollupMetrics } from './util/MetricUtils.js';
|
||||
import React from 'react';
|
||||
import './../../css/list.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export default class Namespaces 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.state = this.getInitialState(this.props.params);
|
||||
}
|
||||
|
||||
getInitialState(params) {
|
||||
let ns = _.get(params, "namespace", "default");
|
||||
|
||||
return {
|
||||
ns: ns,
|
||||
pollingInterval: 2000,
|
||||
metrics: {},
|
||||
pendingRequests: false,
|
||||
loaded: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFromServer();
|
||||
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
|
||||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
// React won't unmount this component when switching resource pages so we need to clear state
|
||||
this.api.cancelCurrentRequests();
|
||||
this.setState(this.getInitialState(this.props.params));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearInterval(this.timerId);
|
||||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
if (this.state.pendingRequests) {
|
||||
return; // don't make more requests if the ones we sent haven't completed
|
||||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
this.api.setCurrentRequests(
|
||||
_.map(["deployment", "replication_controller", "pod"], resource =>
|
||||
this.api.fetchMetrics(this.api.urlsForResource[resource].url(this.state.ns).rollup))
|
||||
);
|
||||
|
||||
Promise.all(this.api.getCurrentPromises())
|
||||
.then(([deployRollup, rcRollup, podRollup]) => {
|
||||
let includeConduitStats = this.state.ns === this.props.controllerNamespace; // allow us to get stats on the conduit ns
|
||||
let deploys = processRollupMetrics(deployRollup, this.props.controllerNamespace, includeConduitStats);
|
||||
let rcs = processRollupMetrics(rcRollup, this.props.controllerNamespace, includeConduitStats);
|
||||
let pods = processRollupMetrics(podRollup, this.props.controllerNamespace, includeConduitStats);
|
||||
|
||||
this.setState({
|
||||
metrics: {
|
||||
deploy: deploys,
|
||||
rc: rcs,
|
||||
pod: pods
|
||||
},
|
||||
loaded: true,
|
||||
pendingRequests: false,
|
||||
error: ''
|
||||
});
|
||||
})
|
||||
.catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
if (e.isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: `Error getting data from server: ${e.message}`
|
||||
});
|
||||
}
|
||||
|
||||
renderResourceSection(friendlyTitle, metrics) {
|
||||
if (_.isEmpty(metrics)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="page-section">
|
||||
<h1>{friendlyTitle}s</h1>
|
||||
<MetricsTable
|
||||
resource={friendlyTitle}
|
||||
metrics={metrics}
|
||||
api={this.api} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let noMetrics = _.isEmpty(this.state.metrics.pod);
|
||||
|
||||
return (
|
||||
<div className="page-content">
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<PageHeader header={"Namespace: " + this.state.ns} api={this.api} />
|
||||
{ noMetrics ? <CallToAction /> : null}
|
||||
{this.renderResourceSection("Deployment", this.state.metrics.deploy)}
|
||||
{this.renderResourceSection("Replication Controller", this.state.metrics.rc)}
|
||||
{this.renderResourceSection("Pod", this.state.metrics.pod)}
|
||||
</div>
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -79,10 +79,12 @@ export default class ResourceList extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
renderEmptyMessage(resource) {
|
||||
return this.props.resource === "deployment" ?
|
||||
<CallToAction numDeployments={_.size(this.state.metrics)} /> :
|
||||
<div>No {resource}s found</div>;
|
||||
renderEmptyMessage() {
|
||||
let shortResource = this.props.resource === "replication_controller" ?
|
||||
"RC" : this.props.resource;
|
||||
return (<CallToAction
|
||||
resource={shortResource}
|
||||
numResources={_.size(this.state.metrics)} />);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -4,14 +4,12 @@ import ConduitSpinner from "./ConduitSpinner.jsx";
|
|||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
import { numericSort } from './util/Utils.js';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import React from 'react';
|
||||
import StatusTable from './StatusTable.jsx';
|
||||
import { Col, Row, Table } from 'antd';
|
||||
import {
|
||||
getComponentPods,
|
||||
getPodsByDeployment,
|
||||
} from './util/MetricUtils.js';
|
||||
import { Col, Row, Table, Tooltip } from 'antd';
|
||||
import './../../css/service-mesh.css';
|
||||
|
||||
const serviceMeshDetailsColumns = [
|
||||
|
@ -27,6 +25,58 @@ const serviceMeshDetailsColumns = [
|
|||
className: "numeric"
|
||||
}
|
||||
];
|
||||
|
||||
const barColor = percentMeshed => {
|
||||
if (percentMeshed <= 0) {
|
||||
return "neutral";
|
||||
} else {
|
||||
return "good";
|
||||
}
|
||||
};
|
||||
|
||||
const namespacesColumns = ConduitLink => [
|
||||
{
|
||||
title: "Namespace",
|
||||
dataIndex: "namespace",
|
||||
key: "namespace",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace),
|
||||
render: d => <ConduitLink to={"/namespaces/" + d}>{d}</ConduitLink>
|
||||
},
|
||||
{
|
||||
title: "Meshed pods",
|
||||
dataIndex: "meshedPodsStr",
|
||||
key: "meshedPodsStr",
|
||||
className: "numeric",
|
||||
sorter: (a, b) => numericSort(a.totalPods, b.totalPods),
|
||||
},
|
||||
{
|
||||
title: "Mesh completion",
|
||||
key: "meshification",
|
||||
sorter: (a, b) => numericSort(a.meshedPercent.get(), b.meshedPercent.get()),
|
||||
render: row => {
|
||||
let containerWidth = 132;
|
||||
let percent = row.meshedPercent.get();
|
||||
let barWidth = percent < 0 ? 0 : Math.round(percent * containerWidth);
|
||||
let barType = barColor(percent);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
overlayStyle={{ fontSize: "12px" }}
|
||||
title={<div>
|
||||
<div>
|
||||
{`${row.meshedPods} / ${row.totalPods} pods in mesh (${row.meshedPercent.prettyRate()})`}
|
||||
</div>
|
||||
</div>}>
|
||||
<div className={"container-bar " + barType} style={{width: containerWidth}}>
|
||||
<div className={"inner-bar " + barType} style={{width: barWidth}}> </div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const componentNames = {
|
||||
"prometheus": "Prometheus",
|
||||
"destination": "Destination",
|
||||
|
@ -55,7 +105,6 @@ export default class ServiceMesh extends React.Component {
|
|||
this.state = {
|
||||
pollingInterval: 2000,
|
||||
metrics: [],
|
||||
deploys: [],
|
||||
components: [],
|
||||
lastUpdated: 0,
|
||||
pendingRequests: false,
|
||||
|
@ -74,6 +123,45 @@ export default class ServiceMesh extends React.Component {
|
|||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
extractNsStatuses(nsData) {
|
||||
let podsByNs = _.get(nsData, ["ok", "statTables", 0, "podGroup", "rows"], []);
|
||||
let dataPlaneNamepaces = _.map(podsByNs, ns => {
|
||||
if (ns.resource.name.indexOf("kube-") === 0) {
|
||||
return;
|
||||
}
|
||||
let meshedPods = parseInt(ns.meshedPodCount, 10);
|
||||
let totalPods = parseInt(ns.runningPodCount, 10);
|
||||
|
||||
return {
|
||||
namespace: ns.resource.name,
|
||||
meshedPodsStr: ns.meshedPodCount + "/" + ns.runningPodCount,
|
||||
meshedPercent: new Percentage(meshedPods, totalPods),
|
||||
meshedPods,
|
||||
totalPods
|
||||
};
|
||||
});
|
||||
return _.compact(dataPlaneNamepaces);
|
||||
}
|
||||
|
||||
processComponents(conduitPods) {
|
||||
let pods = _.get(conduitPods, ["ok", "statTables", 0, "podGroup", "rows"], 0);
|
||||
return _.map(componentNames, (title, name) => {
|
||||
let deployName = componentDeploys[name];
|
||||
let matchingPods = _.filter(pods, p => p.resource.name.split("-")[0] === deployName);
|
||||
|
||||
return {
|
||||
name: title,
|
||||
pods: _.map(matchingPods, p => {
|
||||
return {
|
||||
name: p.resource.name,
|
||||
// we need an endpoint to return the k8s status of these pods
|
||||
value: _.size(matchingPods) > 0 ? "good" : "neutral"
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
if (this.state.pendingRequests) {
|
||||
return; // don't make more requests if the ones we sent haven't completed
|
||||
|
@ -81,17 +169,15 @@ export default class ServiceMesh extends React.Component {
|
|||
this.setState({ pendingRequests: true });
|
||||
|
||||
this.api.setCurrentRequests([
|
||||
this.api.fetchPods()
|
||||
this.api.fetchMetrics(this.api.urlsForResource["pod"].url(this.props.controllerNamespace).rollup),
|
||||
this.api.fetchMetrics(this.api.urlsForResource["namespace"].url().rollup)
|
||||
]);
|
||||
|
||||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(([pods]) => {
|
||||
let podsByDeploy = getPodsByDeployment(pods.pods);
|
||||
let controlPlanePods = this.processComponents(pods.pods);
|
||||
|
||||
.then(([conduitPods, nsStats]) => {
|
||||
this.setState({
|
||||
deploys: podsByDeploy,
|
||||
components: controlPlanePods,
|
||||
components: this.processComponents(conduitPods),
|
||||
nsStatuses: this.extractNsStatuses(nsStats),
|
||||
lastUpdated: Date.now(),
|
||||
pendingRequests: false,
|
||||
loaded: true,
|
||||
|
@ -112,26 +198,14 @@ export default class ServiceMesh extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
addedDeploymentCount() {
|
||||
return _.size(_.filter(this.state.deploys, ["added", true]));
|
||||
}
|
||||
|
||||
unaddedDeploymentCount() {
|
||||
return this.deployCount() - this.addedDeploymentCount();
|
||||
}
|
||||
|
||||
proxyCount() {
|
||||
return _.sum(_.map(this.state.deploys, d => {
|
||||
return _.size(_.filter(d.pods, ["value", "good"]));
|
||||
}));
|
||||
}
|
||||
|
||||
componentCount() {
|
||||
return _.size(this.state.components);
|
||||
}
|
||||
|
||||
deployCount() {
|
||||
return _.size(this.state.deploys);
|
||||
proxyCount() {
|
||||
return _.sumBy(this.state.nsStatuses, d => {
|
||||
return d.namespace === this.props.controllerNamespace ? 0 : d.meshedPods;
|
||||
});
|
||||
}
|
||||
|
||||
getServiceMeshDetails() {
|
||||
|
@ -139,27 +213,10 @@ export default class ServiceMesh extends React.Component {
|
|||
{ key: 1, name: "Conduit version", value: this.props.releaseVersion },
|
||||
{ key: 2, name: "Conduit namespace", value: this.props.controllerNamespace },
|
||||
{ key: 3, name: "Control plane components", value: this.componentCount() },
|
||||
{ key: 4, name: "Added deployments", value: this.addedDeploymentCount() },
|
||||
{ key: 5, name: "Unadded deployments", value: this.unaddedDeploymentCount() },
|
||||
{ key: 6, name: "Data plane proxies", value: this.proxyCount() }
|
||||
{ key: 4, name: "Data plane proxies", value: this.proxyCount() }
|
||||
];
|
||||
}
|
||||
|
||||
processComponents(pods) {
|
||||
let podIndex = _(pods)
|
||||
.filter(p => p.controlPlane)
|
||||
.groupBy(p => _.last(_.split(p.deployment, "/")))
|
||||
.value();
|
||||
|
||||
return _(componentNames)
|
||||
.map((name, id) => {
|
||||
let componentPods = _.get(podIndex, _.get(componentDeploys, id), []);
|
||||
return { name: name, pods: getComponentPods(componentPods) };
|
||||
})
|
||||
.sortBy("name")
|
||||
.value();
|
||||
}
|
||||
|
||||
renderControlPlaneDetails() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
|
@ -177,24 +234,6 @@ export default class ServiceMesh extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderDataPlaneDetails() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
<div className="clearfix header-with-metric">
|
||||
<div className="subsection-header">Data plane</div>
|
||||
<Metric title="Proxies" value={this.proxyCount()} className="metric-large" />
|
||||
<Metric title="Deployments" value={this.deployCount()} className="metric-large" />
|
||||
</div>
|
||||
|
||||
<StatusTable
|
||||
data={this.state.deploys}
|
||||
statusColumnTitle="Proxy Status"
|
||||
shouldLink={true}
|
||||
api={this.api} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderServiceMeshDetails() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
|
@ -214,61 +253,56 @@ export default class ServiceMesh extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderAddDeploymentsMessage() {
|
||||
if (this.deployCount() === 0) {
|
||||
renderAddResourcesMessage() {
|
||||
if (_.isEmpty(this.state.nsStatuses)) {
|
||||
return <div className="mesh-completion-message">No resources detected.</div>;
|
||||
}
|
||||
|
||||
let meshedCount = _.countBy(this.state.nsStatuses, pod => {
|
||||
return pod.meshedPercent.get() > 0;
|
||||
});
|
||||
let numUnadded = meshedCount["false"] || 0;
|
||||
|
||||
if (numUnadded === 0) {
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
No deployments detected. {incompleteMeshMessage()}
|
||||
All namespaces have a conduit install.
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switch (this.unaddedDeploymentCount()) {
|
||||
case 0:
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
All deployments have been added to the service mesh.
|
||||
</div>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
1 deployment has not been added to the service mesh. {incompleteMeshMessage()}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
{this.unaddedDeploymentCount()} deployments have not been added to the service mesh. {incompleteMeshMessage()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
{numUnadded} {numUnadded === 1 ? "namespace has" : "namespaces have"} no meshed resources.
|
||||
{incompleteMeshMessage()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderControlPlane() {
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>{this.renderControlPlaneDetails()}</Col>
|
||||
<Col span={8}>{this.renderServiceMeshDetails()}</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
renderNamespaceStatusTable() {
|
||||
let rowCn = row => {
|
||||
return row.meshedPercent.get() > 0.9 ? "good" : "neutral";
|
||||
};
|
||||
|
||||
renderDataPlane() {
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>{this.renderDataPlaneDetails()}</Col>
|
||||
<Col span={8}>{this.renderAddDeploymentsMessage()}</Col>
|
||||
</Row>
|
||||
<div className="mesh-section">
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Table
|
||||
className="conduit-table service-mesh-table mesh-completion-table"
|
||||
dataSource={this.state.nsStatuses}
|
||||
columns={namespacesColumns(this.api.ConduitLink)}
|
||||
rowKey="namespace"
|
||||
rowClassName={rowCn}
|
||||
pagination={false}
|
||||
size="middle" />
|
||||
</Col>
|
||||
<Col span={8}>{this.renderAddResourcesMessage()}</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOverview() {
|
||||
if (this.proxyCount() === 0) {
|
||||
return <CallToAction numDeployments={this.deployCount()} />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="page-content">
|
||||
|
@ -279,9 +313,18 @@ export default class ServiceMesh extends React.Component {
|
|||
header="Service mesh overview"
|
||||
hideButtons={this.proxyCount() === 0}
|
||||
api={this.api} />
|
||||
{this.renderOverview()}
|
||||
{this.renderControlPlane()}
|
||||
{this.renderDataPlane()}
|
||||
|
||||
{this.proxyCount() === 0 ?
|
||||
<CallToAction
|
||||
numResources={_.size(this.state.nsStatuses)}
|
||||
resource="namespace" /> : null}
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>{this.renderControlPlaneDetails()}</Col>
|
||||
<Col span={8}>{this.renderServiceMeshDetails()}</Col>
|
||||
</Row>
|
||||
|
||||
{this.renderNamespaceStatusTable()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -96,6 +96,11 @@ export default class Sidebar extends React.Component {
|
|||
<span><ConduitLink to="/servicemesh">Service mesh</ConduitLink></span>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/namespaces">
|
||||
<Icon><ConduitLink to="/namespaces"><Icon>N</Icon></ConduitLink></Icon>
|
||||
<span><ConduitLink to="/namespaces">Namespaces</ConduitLink></span>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/deployments">
|
||||
<Icon><ConduitLink to="/deployments"><Icon>D</Icon></ConduitLink></Icon>
|
||||
<span><ConduitLink to="/deployments">Deployments</ConduitLink></span>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import _ from 'lodash';
|
||||
import GrafanaLink from './GrafanaLink.jsx';
|
||||
import React from 'react';
|
||||
import { Table, Tooltip } from 'antd';
|
||||
|
||||
|
@ -40,21 +39,10 @@ const StatusDot = ({status, multilineDots, columnName}) => (
|
|||
);
|
||||
|
||||
const columns = {
|
||||
resourceName: (shouldLink, ConduitLink) => {
|
||||
return {
|
||||
title: "Deployment",
|
||||
key: "name",
|
||||
render: row => {
|
||||
let ownerInfo = row.name.split("/");
|
||||
return shouldLink && row.added ?
|
||||
<GrafanaLink
|
||||
name={ownerInfo[1]}
|
||||
namespace={ownerInfo[0]}
|
||||
displayName={row.name}
|
||||
resource="deployment"
|
||||
conduitLink={ConduitLink} /> : row.name;
|
||||
}
|
||||
};
|
||||
resourceName: {
|
||||
title: "Deployment",
|
||||
dataIndex: "name",
|
||||
key: "name"
|
||||
},
|
||||
pods: {
|
||||
title: "Pods",
|
||||
|
@ -95,7 +83,7 @@ export default class StatusTable extends React.Component {
|
|||
|
||||
render() {
|
||||
let tableCols = [
|
||||
columns.resourceName(this.props.shouldLink, this.props.api.ConduitLink),
|
||||
columns.resourceName,
|
||||
columns.pods,
|
||||
columns.status(this.props.statusColumnTitle)
|
||||
];
|
||||
|
|
|
@ -79,27 +79,27 @@ export const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
|
|||
metricsWindow = window;
|
||||
};
|
||||
|
||||
const genResourceUrl = type => {
|
||||
return namespace => {
|
||||
let baseUrl = '/api/stat?resource_type=' + type;
|
||||
return {
|
||||
rollup: !namespace ? baseUrl : baseUrl + '&namespace=' + namespace
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const urlsForResource = {
|
||||
"namespace": {
|
||||
url: genResourceUrl("namespace")
|
||||
},
|
||||
"replication_controller": {
|
||||
url: () => {
|
||||
return {
|
||||
rollup: `/api/stat?resource_type=replicationcontroller`
|
||||
};
|
||||
}
|
||||
url: genResourceUrl("replicationcontroller")
|
||||
},
|
||||
"deployment": {
|
||||
url: () => {
|
||||
return {
|
||||
rollup: `/api/stat?resource_type=deployment`
|
||||
};
|
||||
}
|
||||
url: genResourceUrl("deployment")
|
||||
},
|
||||
"pod": {
|
||||
url: () => {
|
||||
return {
|
||||
rollup: `/api/stat?resource_type=pod`
|
||||
};
|
||||
}
|
||||
url: genResourceUrl("pod")
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
/*
|
||||
* Instructions for adding deployments to service mesh
|
||||
* Instructions for adding resources to service mesh
|
||||
*/
|
||||
export const incompleteMeshMessage = name => {
|
||||
if (name) {
|
||||
return (
|
||||
<div className="action">Add {name} to the deployment.yml file<br /><br />
|
||||
Then run <code>conduit inject deployment.yml | kubectl apply -f -</code> to add it to the service mesh</div>
|
||||
<div className="action">Add {name} to the k8s.yml file<br /><br />
|
||||
Then run <code>conduit inject k8s.yml | kubectl apply -f -</code> to add it to the service mesh</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="action">Add one or more deployments to the deployment.yml file<br /><br />
|
||||
Then run <code>conduit inject deployment.yml | kubectl apply -f -</code> to add them to the service mesh</div>
|
||||
<div className="action">Add one or more resources to the k8s.yml file<br /><br />
|
||||
Then run <code>conduit inject k8s.yml | kubectl apply -f -</code> to add them to the service mesh</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -94,7 +94,7 @@ export const getComponentPods = componentPods => {
|
|||
|
||||
const kubernetesNs = "kube-system";
|
||||
const defaultControllerNs = "conduit";
|
||||
export const processRollupMetrics = (rawMetrics, controllerNamespace) => {
|
||||
export const processRollupMetrics = (rawMetrics, controllerNamespace, includeConduit) => {
|
||||
if (_.isEmpty(rawMetrics.ok) || _.isEmpty(rawMetrics.ok.statTables)) {
|
||||
return [];
|
||||
}
|
||||
|
@ -103,7 +103,10 @@ export const processRollupMetrics = (rawMetrics, controllerNamespace) => {
|
|||
}
|
||||
let metrics = _.flatMap(rawMetrics.ok.statTables, table => {
|
||||
return _.map(table.podGroup.rows, row => {
|
||||
if (row.resource.namespace === kubernetesNs || row.resource.namespace === controllerNamespace) {
|
||||
if (row.resource.namespace === kubernetesNs) {
|
||||
return null;
|
||||
}
|
||||
if (row.resource.namespace === controllerNamespace && !includeConduit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,6 @@ Percentage.prototype.prettyRate = function() {
|
|||
if (this.decimal < 0) {
|
||||
return "N/A";
|
||||
} else {
|
||||
return (100*this.decimal).toFixed(3) + "%";
|
||||
return (100*this.decimal).toFixed(1) + "%";
|
||||
}
|
||||
};
|
||||
|
|
|
@ -86,3 +86,8 @@ export const toClassName = name => {
|
|||
if (!name) return "";
|
||||
return _.lowerCase(name).replace(/[^a-zA-Z0-9]/g, "_");
|
||||
};
|
||||
|
||||
/*
|
||||
Definition of sort, for ant table sorting
|
||||
*/
|
||||
export const numericSort = (a, b) => (_.isNil(a) ? -1 : a) - (_.isNil(b) ? -1 : b);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ApiHelpers } from './components/util/ApiHelpers.jsx';
|
||||
import { Layout } from 'antd';
|
||||
import Namespace from './components/Namespace.jsx';
|
||||
import NoMatch from './components/NoMatch.jsx';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -18,6 +19,8 @@ if (proxyPathMatch) {
|
|||
pathPrefix = proxyPathMatch[0];
|
||||
}
|
||||
|
||||
let controllerNs = appData.controllerNamespace || "conduit";
|
||||
|
||||
let api = ApiHelpers(pathPrefix);
|
||||
|
||||
let applicationHtml = (
|
||||
|
@ -36,10 +39,12 @@ let applicationHtml = (
|
|||
<div className="main-content">
|
||||
<Switch>
|
||||
<Redirect exact from={`${pathPrefix}/`} to={`${pathPrefix}/servicemesh`} />
|
||||
<Route path={`${pathPrefix}/servicemesh`} render={() => <ServiceMesh api={api} releaseVersion={appData.releaseVersion} controllerNamespace={appData.controllerNamespace} />} />
|
||||
<Route path={`${pathPrefix}/deployments`} render={() => <ResourceList resource="deployment" api={api} controllerNamespace={appData.controllerNamespace} />} />
|
||||
<Route path={`${pathPrefix}/replicationcontrollers`} render={() => <ResourceList resource="replication_controller" api={api} controllerNamespace={appData.controllerNamespace} />} />
|
||||
<Route path={`${pathPrefix}/pods`} render={() => <ResourceList resource="pod" api={api} controllerNamespace={appData.controllerNamespace} />} />
|
||||
<Route path={`${pathPrefix}/servicemesh`} render={() => <ServiceMesh api={api} releaseVersion={appData.releaseVersion} controllerNamespace={controllerNs} />} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace`} render={props => <Namespace resource="namespace" api={api} controllerNamespace={controllerNs} params={props.match.params} />} />
|
||||
<Route path={`${pathPrefix}/namespaces`} render={() => <ResourceList resource="namespace" api={api} controllerNamespace={controllerNs} />} />
|
||||
<Route path={`${pathPrefix}/deployments`} render={() => <ResourceList resource="deployment" api={api} controllerNamespace={controllerNs} />} />
|
||||
<Route path={`${pathPrefix}/replicationcontrollers`} render={() => <ResourceList resource="replication_controller" api={api} controllerNamespace={controllerNs} />} />
|
||||
<Route path={`${pathPrefix}/pods`} render={() => <ResourceList resource="pod" api={api} controllerNamespace={controllerNs} />} />
|
||||
<Route component={NoMatch} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import _ from 'lodash';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import conduitPodFixtures from './fixtures/conduitPods.json';
|
||||
import Enzyme from 'enzyme';
|
||||
import { expect } from 'chai';
|
||||
import { mount } from 'enzyme';
|
||||
import podFixtures from './fixtures/pods.json';
|
||||
import nsFixtures from './fixtures/namespaces.json';
|
||||
import { routerWrap } from './testHelpers.jsx';
|
||||
import ServiceMesh from '../js/components/ServiceMesh.jsx';
|
||||
import sinon from 'sinon';
|
||||
|
@ -66,12 +67,9 @@ describe('ServiceMesh', () => {
|
|||
});
|
||||
|
||||
it("renders controller component summaries", () => {
|
||||
let addedPods = _.cloneDeep(podFixtures.pods);
|
||||
_.set(addedPods[1], "added", true);
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: addedPods})
|
||||
json: () => Promise.resolve(conduitPodFixtures)
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
|
@ -129,59 +127,71 @@ describe('ServiceMesh', () => {
|
|||
});
|
||||
|
||||
describe("renderAddDeploymentsMessage", () => {
|
||||
it("displays when no deployments are in the mesh", () => {
|
||||
it("displays when no resources are in the mesh", () => {
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: []})
|
||||
json: () => Promise.resolve({})
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).to.include("No deployments detected.");
|
||||
expect(component.html()).to.include("No resources detected");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays a message if >1 deployment has not been added to the mesh", () => {
|
||||
it("displays a message if >1 resource has not been added to the mesh", () => {
|
||||
let nsAllResourcesAdded = _.cloneDeep(nsFixtures);
|
||||
nsAllResourcesAdded.ok.statTables[0].podGroup.rows.push({
|
||||
"resource":{
|
||||
"namespace":"",
|
||||
"type":"namespaces",
|
||||
"name":"test-1"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "0",
|
||||
"runningPodCount": "5",
|
||||
"stats": null
|
||||
});
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: podFixtures.pods})
|
||||
json: () => Promise.resolve(nsAllResourcesAdded)
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).to.include("deployments have not been added to the service mesh.");
|
||||
expect(component.html()).to.include("2 namespaces have no meshed resources.");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays a message if 1 deployment has not added to servicemesh", () => {
|
||||
let addedPods = _.cloneDeep(podFixtures.pods);
|
||||
_.set(addedPods[0], "added", true);
|
||||
|
||||
it("displays a message if 1 resource has not added to servicemesh", () => {
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: addedPods})
|
||||
json: () => Promise.resolve(nsFixtures)
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).to.include("1 deployment has not been added to the service mesh.");
|
||||
expect(component.html()).to.include("1 namespace has no meshed resources.");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays a message if all deployments have been added to servicemesh", () => {
|
||||
let addedPods = _.cloneDeep(podFixtures.pods);
|
||||
_.forEach(addedPods, pod => {
|
||||
_.set(pod, "added", true);
|
||||
it("displays a message if all resource have been added to servicemesh", () => {
|
||||
let nsAllResourcesAdded = _.cloneDeep(nsFixtures);
|
||||
_.each(nsAllResourcesAdded.ok.statTables[0].podGroup.rows, row => {
|
||||
if (row.resource.name === "default") {
|
||||
row.meshedPodCount = "10";
|
||||
row.runningPodCount = "10";
|
||||
}
|
||||
});
|
||||
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ pods: addedPods})
|
||||
json: () => Promise.resolve(nsAllResourcesAdded)
|
||||
});
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.html()).to.include("All deployments have been added to the service mesh.");
|
||||
expect(component.html()).to.include("All namespaces have a conduit install.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"ok": {
|
||||
"statTables": [
|
||||
{
|
||||
"podGroup": {
|
||||
"rows": [
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "conduit",
|
||||
"type": "pods",
|
||||
"name": "web-5f97578cf6-597sr"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "4",
|
||||
"runningPodCount": "4",
|
||||
"stats": {
|
||||
"successCount": "6",
|
||||
"failureCount": "0",
|
||||
"latencyMsP50": "5",
|
||||
"latencyMsP95": "9",
|
||||
"latencyMsP99": "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "conduit",
|
||||
"type": "pods",
|
||||
"name": "controller-795dd8df6-tjdt6"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "4",
|
||||
"runningPodCount": "4",
|
||||
"stats": {
|
||||
"successCount": "24",
|
||||
"failureCount": "0",
|
||||
"latencyMsP50": "7",
|
||||
"latencyMsP95": "18",
|
||||
"latencyMsP99": "20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "conduit",
|
||||
"type": "pods",
|
||||
"name": "prometheus-595785446-r7rlq"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "4",
|
||||
"runningPodCount": "4",
|
||||
"stats": null
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "conduit",
|
||||
"type": "pods",
|
||||
"name": "grafana-78cdb656bf-v8lsj"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "4",
|
||||
"runningPodCount": "4",
|
||||
"stats": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
"successCount": "56"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"totalPodCount": "1"
|
||||
"runningPodCount": "1"
|
||||
},
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
|
@ -36,7 +36,7 @@
|
|||
"successCount": "124"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"totalPodCount": "1"
|
||||
"runningPodCount": "1"
|
||||
},
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
|
@ -53,7 +53,7 @@
|
|||
"successCount": "6"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"totalPodCount": "1"
|
||||
"runningPodCount": "1"
|
||||
},
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
|
@ -70,7 +70,7 @@
|
|||
"successCount": "113"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"totalPodCount": "1"
|
||||
"runningPodCount": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"ok": {
|
||||
"statTables": [
|
||||
{
|
||||
"podGroup": {
|
||||
"rows": [
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "",
|
||||
"type": "namespaces",
|
||||
"name": "conduit"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "4",
|
||||
"runningPodCount": "4",
|
||||
"stats": {
|
||||
"successCount": "30",
|
||||
"failureCount": "0",
|
||||
"latencyMsP50": "5",
|
||||
"latencyMsP95": "10",
|
||||
"latencyMsP99": "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "",
|
||||
"type": "namespaces",
|
||||
"name": "kube-system"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "0",
|
||||
"runningPodCount": "9",
|
||||
"stats": null
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "",
|
||||
"type": "namespaces",
|
||||
"name": "default"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "0",
|
||||
"runningPodCount": "0",
|
||||
"stats": null
|
||||
},
|
||||
{
|
||||
"resource": {
|
||||
"namespace": "",
|
||||
"type": "namespaces",
|
||||
"name": "kube-public"
|
||||
},
|
||||
"timeWindow": "1m",
|
||||
"meshedPodCount": "0",
|
||||
"runningPodCount": "0",
|
||||
"stats": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"pods": [
|
||||
{
|
||||
"name": "test/gen2-2742169793-6rkz8",
|
||||
"podIP": "10.196.1.205",
|
||||
"deployment": "test/gen2",
|
||||
"status": "Failed",
|
||||
"added": false,
|
||||
"sinceLastReport": null,
|
||||
"controllerNamespace": "",
|
||||
"controlPlane": false
|
||||
},
|
||||
{
|
||||
"name": "test/gen3-689289393-xz7xg",
|
||||
"podIP": "10.196.1.206",
|
||||
"deployment": "test/gen3",
|
||||
"status": "Running",
|
||||
"added": false,
|
||||
"sinceLastReport": null,
|
||||
"controllerNamespace": "",
|
||||
"controlPlane": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -83,6 +83,8 @@ func NewServer(addr, templateDir, staticDir, uuid, controllerNamespace, webpackD
|
|||
// webapp routes
|
||||
server.router.GET("/", handler.handleIndex)
|
||||
server.router.GET("/servicemesh", handler.handleIndex)
|
||||
server.router.GET("/namespaces", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace", handler.handleIndex)
|
||||
server.router.GET("/deployments", handler.handleIndex)
|
||||
server.router.GET("/replicationcontrollers", handler.handleIndex)
|
||||
server.router.GET("/pods", handler.handleIndex)
|
||||
|
|
Loading…
Reference in New Issue