diff --git a/web/app/js/components/MetricsTable.jsx b/web/app/js/components/MetricsTable.jsx index 72e735969..94a6c9105 100644 --- a/web/app/js/components/MetricsTable.jsx +++ b/web/app/js/components/MetricsTable.jsx @@ -1,6 +1,8 @@ import _ from 'lodash'; import BaseTable from './BaseTable.jsx'; import GrafanaLink from './GrafanaLink.jsx'; +import { processedMetricsPropType } from './util/MetricUtils.js'; +import PropTypes from 'prop-types'; import React from 'react'; import { Tooltip } from 'antd'; import { withContext } from './util/AppContext.jsx'; @@ -35,7 +37,7 @@ const formatTitle = (title, tooltipText) => { } }; -const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, linkifyNsColumn, ConduitLink) => { +const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceColumn, ConduitLink) => { let nsColumn = [ { title: formatTitle("Namespace"), @@ -44,32 +46,19 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, filters: namespaces, onFilterDropdownVisibleChange: onFilterClick, onFilter: (value, row) => row.namespace.indexOf(value) === 0, - sorter: sortable ? (a, b) => (a.namespace || "").localeCompare(b.namespace) : false, + sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace), render: ns => { - if (linkifyNsColumn) { - return {ns}; - } else { - return ns; - } + return {ns}; } } ]; - let percentMeshedColumn = [ - { - title: formatTitle("Secured", "Percent of traffic that is TLSed"), - key: "securedTraffic", - dataIndex: "meshedRequestPercent", - className: "numeric", - sorter: sortable ? (a, b) => numericSort(a.meshedRequestPercent.get(), b.meshedRequestPercent.get()) : false, - render: d => _.isNil(d) ? "---" : d.prettyRate() - } - ]; + let columns = [ { title: formatTitle(resource), key: "name", defaultSortOrder: 'ascend', - sorter: sortable ? (a, b) => (a.name || "").localeCompare(b.name) : false, + sorter: (a, b) => (a.name || "").localeCompare(b.name), render: row => { if (resource.toLowerCase() === "namespace") { return {row.name}; @@ -91,7 +80,7 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, dataIndex: "successRate", key: "successRateRollup", className: "numeric", - sorter: sortable ? (a, b) => numericSort(a.successRate, b.successRate) : false, + sorter: (a, b) => numericSort(a.successRate, b.successRate), render: d => metricToFormatter["SUCCESS_RATE"](d) }, { @@ -99,7 +88,7 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, dataIndex: "requestRate", key: "requestRateRollup", className: "numeric", - sorter: sortable ? (a, b) => numericSort(a.requestRate, b.requestRate) : false, + sorter: (a, b) => numericSort(a.requestRate, b.requestRate), render: d => withTooltip(d, "REQUEST_RATE") }, { @@ -107,7 +96,7 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, dataIndex: "P50", key: "p50LatencyRollup", className: "numeric", - sorter: sortable ? (a, b) => numericSort(a.P50, b.P50) : false, + sorter: (a, b) => numericSort(a.P50, b.P50), render: metricToFormatter["LATENCY"] }, { @@ -115,7 +104,7 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, dataIndex: "P95", key: "p95LatencyRollup", className: "numeric", - sorter: sortable ? (a, b) => numericSort(a.P95, b.P95) : false, + sorter: (a, b) => numericSort(a.P95, b.P95), render: metricToFormatter["LATENCY"] }, { @@ -123,19 +112,41 @@ const columnDefinitions = (sortable = true, resource, namespaces, onFilterClick, dataIndex: "P99", key: "p99LatencyRollup", className: "numeric", - sorter: sortable ? (a, b) => numericSort(a.P99, b.P99) : false, + sorter: (a, b) => numericSort(a.P99, b.P99), render: metricToFormatter["LATENCY"] + }, + { + title: formatTitle("Secured", "Percentage of TLS Traffic"), + key: "securedTraffic", + dataIndex: "meshedRequestPercent", + className: "numeric", + sorter: (a, b) => numericSort(a.meshedRequestPercent.get(), b.meshedRequestPercent.get()), + render: d => _.isNil(d) ? "---" : d.prettyRate() } ]; - if (resource.toLowerCase() === "namespace") { + if (resource.toLowerCase() === "namespace" || !showNamespaceColumn) { return columns; } else { - return _.concat(nsColumn, columns, percentMeshedColumn); + return _.concat(nsColumn, columns); } }; -class MetricsTable extends BaseTable { +/** @extends React.Component */ +export class MetricsTableBase extends BaseTable { + static defaultProps = { + showNamespaceColumn: true, + } + + static propTypes = { + api: PropTypes.shape({ + ConduitLink: PropTypes.func.isRequired, + }).isRequired, + metrics: PropTypes.arrayOf(processedMetricsPropType.isRequired).isRequired, + resource: PropTypes.string.isRequired, + showNamespaceColumn: PropTypes.bool, + } + constructor(props) { super(props); this.api = this.props.api; @@ -145,6 +156,17 @@ class MetricsTable extends BaseTable { }; } + shouldComponentUpdate() { + // prevent the table from updating if the filter dropdown menu is open + // this is because if the table updates, the filters will reset which + // makes it impossible to select a filter + return !this.state.preventTableUpdates; + } + + onFilterDropdownVisibleChange(dropdownVisible) { + this.setState({ preventTableUpdates: dropdownVisible}); + } + preprocessMetrics() { let tableData = _.cloneDeep(this.props.metrics); let namespaces = []; @@ -162,17 +184,6 @@ class MetricsTable extends BaseTable { }; } - shouldComponentUpdate() { - // prevent the table from updating if the filter dropdown menu is open - // this is because if the table updates, the filters will reset which - // makes it impossible to select a filter - return !this.state.preventTableUpdates; - } - - onFilterDropdownVisibleChange(dropdownVisible) { - this.setState({ preventTableUpdates: dropdownVisible}); - } - render() { let tableData = this.preprocessMetrics(); let namespaceFilterText = _.map(tableData.namespaces, ns => { @@ -180,11 +191,10 @@ class MetricsTable extends BaseTable { }); let columns = _.compact(columnDefinitions( - this.props.sortable, this.props.resource, - this.props.showNamespaceFilter ? namespaceFilterText : undefined, - this.props.showNamespaceFilter ? this.onFilterDropdownVisibleChange : undefined, - this.props.linkifyNsColumn, + namespaceFilterText, + this.onFilterDropdownVisibleChange, + this.props.showNamespaceColumn, this.api.ConduitLink )); @@ -205,8 +215,4 @@ class MetricsTable extends BaseTable { } } -MetricsTable.defaultProps = { - showNamespaceFilter: true -}; - -export default withContext(MetricsTable); +export default withContext(MetricsTableBase); diff --git a/web/app/js/components/Namespace.jsx b/web/app/js/components/Namespace.jsx index c5e871e7a..7c0223407 100644 --- a/web/app/js/components/Namespace.jsx +++ b/web/app/js/components/Namespace.jsx @@ -114,7 +114,7 @@ class Namespaces extends React.Component { + showNamespaceColumn={false} /> ); } diff --git a/web/app/js/components/ResourceList.jsx b/web/app/js/components/ResourceList.jsx index 0c5f6c042..8b63ba4e5 100644 --- a/web/app/js/components/ResourceList.jsx +++ b/web/app/js/components/ResourceList.jsx @@ -1,13 +1,12 @@ import _ from 'lodash'; import ConduitSpinner from "./ConduitSpinner.jsx"; import ErrorBanner from './ErrorBanner.jsx'; -import { metricsPropType } from './util/ApiHelpers.jsx'; import MetricsTable from './MetricsTable.jsx'; import PageHeader from './PageHeader.jsx'; -import { processSingleResourceRollup } from './util/MetricUtils.js'; import PropTypes from 'prop-types'; import React from 'react'; import withREST from './util/withREST.jsx'; +import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.js'; import './../../css/list.css'; import 'whatwg-fetch'; @@ -50,8 +49,7 @@ export class ResourceListBase extends React.Component { return ( + metrics={processedMetrics} /> ); } diff --git a/web/app/js/components/util/ApiHelpers.jsx b/web/app/js/components/util/ApiHelpers.jsx index 4629a3898..406adbd12 100644 --- a/web/app/js/components/util/ApiHelpers.jsx +++ b/web/app/js/components/util/ApiHelpers.jsx @@ -37,34 +37,6 @@ const makeCancelable = (promise, onSuccess) => { }; }; -export const metricsPropType = PropTypes.shape({ - ok: PropTypes.shape({ - statTables: PropTypes.arrayOf(PropTypes.shape({ - podGroup: PropTypes.shape({ - rows: PropTypes.arrayOf(PropTypes.shape({ - failedPodCount: PropTypes.string, - meshedPodCount: PropTypes.string.isRequired, - resource: PropTypes.shape({ - name: PropTypes.string.isRequired, - namespace: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - }).isRequired, - runningPodCount: PropTypes.string.isRequired, - stats: PropTypes.shape({ - failureCount: PropTypes.string.isRequired, - latencyMsP50: PropTypes.string.isRequired, - latencyMsP95: PropTypes.string.isRequired, - latencyMsP99: PropTypes.string.isRequired, - meshedRequestCount: PropTypes.string.isRequired, - successCount: PropTypes.string.isRequired, - }), - timeWindow: PropTypes.string.isRequired, - }).isRequired), - }), - }).isRequired).isRequired, - }), -}); - const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => { let metricsWindow = defaultMetricsWindow; const podsPath = `/api/pods`; diff --git a/web/app/js/components/util/MetricUtils.js b/web/app/js/components/util/MetricUtils.js index d4edb3a39..fd7545800 100644 --- a/web/app/js/components/util/MetricUtils.js +++ b/web/app/js/components/util/MetricUtils.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import Percentage from './Percentage'; +import PropTypes from 'prop-types'; const getPodCategorization = pod => { if (pod.added && pod.status === "Running") { @@ -151,3 +152,39 @@ export const processMultiResourceRollup = rawMetrics => { }); return metricsByResource; }; + +export const metricsPropType = PropTypes.shape({ + ok: PropTypes.shape({ + statTables: PropTypes.arrayOf(PropTypes.shape({ + podGroup: PropTypes.shape({ + rows: PropTypes.arrayOf(PropTypes.shape({ + failedPodCount: PropTypes.string, + meshedPodCount: PropTypes.string.isRequired, + resource: PropTypes.shape({ + name: PropTypes.string.isRequired, + namespace: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + }).isRequired, + runningPodCount: PropTypes.string.isRequired, + stats: PropTypes.shape({ + failureCount: PropTypes.string.isRequired, + latencyMsP50: PropTypes.string.isRequired, + latencyMsP95: PropTypes.string.isRequired, + latencyMsP99: PropTypes.string.isRequired, + meshedRequestCount: PropTypes.string.isRequired, + successCount: PropTypes.string.isRequired, + }), + timeWindow: PropTypes.string.isRequired, + }).isRequired), + }), + }).isRequired).isRequired, + }), +}); + +export const processedMetricsPropType = PropTypes.shape({ + name: PropTypes.string.isRequired, + namespace: PropTypes.string.isRequired, + totalRequests: PropTypes.number.isRequired, + requestRate: PropTypes.number, + successRate: PropTypes.number, +}); diff --git a/web/app/test/MetricsTableTest.js b/web/app/test/MetricsTableTest.js new file mode 100644 index 000000000..511ae187d --- /dev/null +++ b/web/app/test/MetricsTableTest.js @@ -0,0 +1,64 @@ +import Adapter from 'enzyme-adapter-react-16'; +import ApiHelpers from '../js/components/util/ApiHelpers.jsx'; +import BaseTable from '../js/components/BaseTable.jsx'; +import { expect } from 'chai'; +import { MetricsTableBase } from '../js/components/MetricsTable.jsx'; +import React from 'react'; +import Enzyme, { shallow } from 'enzyme'; + +Enzyme.configure({ adapter: new Adapter() }); + +describe('Tests for ', () => { + const defaultProps = { + api: ApiHelpers(''), + }; + + it('renders the table with all columns', () => { + const component = shallow( + + ); + + const table = component.find(BaseTable); + + expect(table).to.have.length(1); + expect(table.props().dataSource).to.have.length(1); + expect(table.props().columns).to.have.length(8); + }); + + it('omits the namespace column for the namespace resource', () => { + const component = shallow( + + ); + + const table = component.find(BaseTable); + + expect(table).to.have.length(1); + expect(table.props().columns).to.have.length(7); + }); + + it('omits the namespace column when showNamespaceColumn is false', () => { + const component = shallow( + + ); + + const table = component.find(BaseTable); + + expect(table).to.have.length(1); + expect(table.props().columns).to.have.length(7); + }); + +});