diff --git a/pkg/ui/src/redux/nodes.spec.ts b/pkg/ui/src/redux/nodes.spec.ts index db3a1389311b..f73a2046ffdc 100644 --- a/pkg/ui/src/redux/nodes.spec.ts +++ b/pkg/ui/src/redux/nodes.spec.ts @@ -6,6 +6,7 @@ import * as protos from "src/js/protos"; import { nodeDisplayNameByIDSelector, selectCommissionedNodeStatuses, + selectStoreIDsByNodeID, LivenessStatus, sumNodeStats, } from "./nodes"; @@ -110,6 +111,43 @@ describe("node data selectors", function() { assert.deepEqual(nodeDisplayNameByIDSelector(store.getState()), {}); }); }); + + describe("store IDs by node ID", function() { + it("correctly creates storeID map", function() { + const data = [ + { + desc: { node_id: 1 }, + store_statuses: [ + { desc: { store_id: 1 }}, + { desc: { store_id: 2 }}, + { desc: { store_id: 3 }}, + ], + }, + { + desc: { node_id: 2 }, + store_statuses: [ + { desc: { store_id: 4 }}, + ], + }, + { + desc: { node_id: 3 }, + store_statuses: [ + { desc: { store_id: 5 }}, + { desc: { store_id: 6 }}, + ], + }, + ]; + const store = createAdminUIStore(); + store.dispatch(nodesReducerObj.receiveData(data)); + const state = store.getState(); + + assert.deepEqual(selectStoreIDsByNodeID(state), { + 1: ["1", "2", "3"], + 2: ["4"], + 3: ["5", "6"], + }); + }); + }); }); describe("selectCommissionedNodeStatuses", function() { diff --git a/pkg/ui/src/redux/nodes.ts b/pkg/ui/src/redux/nodes.ts index d47eb2494f73..e11892a70589 100644 --- a/pkg/ui/src/redux/nodes.ts +++ b/pkg/ui/src/redux/nodes.ts @@ -236,6 +236,19 @@ export const nodeDisplayNameByIDSelector = createSelector( }, ); +// selectStoreIDsByNodeID returns a map from node ID to a list of store IDs for +// that node. Like nodeIDsSelector, the store ids are converted to strings. +export const selectStoreIDsByNodeID = createSelector( + nodeStatusesSelector, + (nodeStatuses) => { + const result: {[key: string]: string[]} = {}; + _.each(nodeStatuses, ns => + result[ns.desc.node_id] = _.map(ns.store_statuses, ss => ss.desc.store_id.toString()), + ); + return result; + }, +); + /** * nodesSummarySelector returns a directory object containing a variety of * computed information based on the current nodes. This object is easy to @@ -249,7 +262,8 @@ export const nodesSummarySelector = createSelector( nodeDisplayNameByIDSelector, livenessStatusByNodeIDSelector, livenessByNodeIDSelector, - (nodeStatuses, nodeIDs, nodeStatusByID, nodeSums, nodeDisplayNameByID, livenessStatusByNodeID, livenessByNodeID) => { + selectStoreIDsByNodeID, + (nodeStatuses, nodeIDs, nodeStatusByID, nodeSums, nodeDisplayNameByID, livenessStatusByNodeID, livenessByNodeID, storeIDsByNodeID) => { return { nodeStatuses, nodeIDs, @@ -258,6 +272,7 @@ export const nodesSummarySelector = createSelector( nodeDisplayNameByID, livenessStatusByNodeID, livenessByNodeID, + storeIDsByNodeID, }; }, ); diff --git a/pkg/ui/src/views/cluster/containers/nodeGraphs/dashboards/dashboardUtils.ts b/pkg/ui/src/views/cluster/containers/nodeGraphs/dashboards/dashboardUtils.ts index c7432edaa142..17e4bcfef84b 100644 --- a/pkg/ui/src/views/cluster/containers/nodeGraphs/dashboards/dashboardUtils.ts +++ b/pkg/ui/src/views/cluster/containers/nodeGraphs/dashboards/dashboardUtils.ts @@ -1,5 +1,3 @@ -import _ from "lodash"; - import { NodesSummary } from "src/redux/nodes"; /** @@ -45,9 +43,5 @@ export function nodeDisplayName(nodesSummary: NodesSummary, nid: string) { } export function storeIDsForNode(nodesSummary: NodesSummary, nid: string): string[] { - const ns = nodesSummary.nodeStatusByID[nid]; - if (!ns) { - return []; - } - return _.map(ns.store_statuses, (ss) => ss.desc.store_id.toString()); + return nodesSummary.storeIDsByNodeID[nid] || []; } diff --git a/pkg/ui/src/views/reports/containers/customgraph/customMetric.tsx b/pkg/ui/src/views/reports/containers/customgraph/customMetric.tsx index dd94ae279544..9f309c63b296 100644 --- a/pkg/ui/src/views/reports/containers/customgraph/customMetric.tsx +++ b/pkg/ui/src/views/reports/containers/customgraph/customMetric.tsx @@ -8,13 +8,15 @@ import { DropdownOption } from "src/views/shared/components/dropdown"; import TimeSeriesQueryAggregator = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator; import TimeSeriesQueryDerivative = protos.cockroach.ts.tspb.TimeSeriesQueryDerivative; -const aggregatorOptions: DropdownOption[] = [ +const downsamplerOptions: DropdownOption[] = [ TimeSeriesQueryAggregator.AVG, TimeSeriesQueryAggregator.MAX, TimeSeriesQueryAggregator.MIN, TimeSeriesQueryAggregator.SUM, ].map(agg => ({ label: TimeSeriesQueryAggregator[agg], value: agg.toString() })); +const aggregatorOptions = downsamplerOptions; + const derivativeOptions: DropdownOption[] = [ { label: "Normal", value: TimeSeriesQueryDerivative.NONE.toString() }, { label: "Rate", value: TimeSeriesQueryDerivative.DERIVATIVE.toString() }, @@ -26,6 +28,7 @@ export class CustomMetricState { downsampler = TimeSeriesQueryAggregator.AVG; aggregator = TimeSeriesQueryAggregator.SUM; derivative = TimeSeriesQueryDerivative.NONE; + perNode = false; source = ""; } @@ -73,6 +76,12 @@ export class CustomMetricRow extends React.Component { }); } + changePerNode = (selection: React.FormEvent) => { + this.changeState({ + perNode: selection.currentTarget.checked, + }); + } + deleteOption = () => { this.props.onDelete(this.props.index); } @@ -81,7 +90,7 @@ export class CustomMetricRow extends React.Component { const { metricOptions, nodeOptions, - rowState: { metric, downsampler, aggregator, derivative, source }, + rowState: { metric, downsampler, aggregator, derivative, source, perNode }, } = this.props; return ( @@ -107,7 +116,7 @@ export class CustomMetricRow extends React.Component { clearable={false} searchable={false} value={downsampler.toString()} - options={aggregatorOptions} + options={downsamplerOptions} onChange={this.changeDownsampler} /> @@ -148,6 +157,9 @@ export class CustomMetricRow extends React.Component { /> + + + diff --git a/pkg/ui/src/views/reports/containers/customgraph/customgraph.styl b/pkg/ui/src/views/reports/containers/customgraph/customgraph.styl index c44378ca26b0..49f0147da3ff 100644 --- a/pkg/ui/src/views/reports/containers/customgraph/customgraph.styl +++ b/pkg/ui/src/views/reports/containers/customgraph/customgraph.styl @@ -47,3 +47,6 @@ background inherit padding 0 + &__cell + text-align center + diff --git a/pkg/ui/src/views/reports/containers/customgraph/index.tsx b/pkg/ui/src/views/reports/containers/customgraph/index.tsx index 0af8e2a920d7..529f0a732dc5 100644 --- a/pkg/ui/src/views/reports/containers/customgraph/index.tsx +++ b/pkg/ui/src/views/reports/containers/customgraph/index.tsx @@ -17,6 +17,8 @@ import { PageConfig, PageConfigItem } from "src/views/shared/components/pageconf import { CustomMetricState, CustomMetricRow } from "./customMetric"; import "./customgraph.styl"; +import { NodeStatus$Properties } from "../../../../util/proto"; + const axisUnitsOptions: DropdownOption[] = [ AxisUnits.Count, AxisUnits.Bytes, @@ -66,7 +68,7 @@ class CustomGraph extends React.Component { return _.keys(nodeStatuses[0].metrics).map(k => { const fullMetricName = - _.has(nodeStatuses[0].store_statuses[0].metrics, k) + isStoreMetric(nodeStatuses[0], k) ? "cr.store." + k : "cr.node." + k; @@ -148,6 +150,7 @@ class CustomGraph extends React.Component { renderGraph() { const metrics = this.currentMetrics(); const units = this.currentAxisUnits(); + const { nodesSummary } = this.props; if (_.isEmpty(metrics)) { return (
@@ -164,17 +167,35 @@ class CustomGraph extends React.Component { { metrics.map((m, i) => { if (m.metric !== "") { - return ( - - ); + if (m.perNode) { + return _.map(nodesSummary.nodeIDs, (nodeID) => ( + n.toString()) + : [nodeID] + } + /> + )); + } else { + return ( + + ); + } } return ""; }) @@ -202,6 +223,7 @@ class CustomGraph extends React.Component { Aggregator Rate Source + Per Node @@ -266,3 +288,7 @@ const mapDispatchToProps = { }; export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CustomGraph)); + +function isStoreMetric(nodeStatus: NodeStatus$Properties, metricName: string) { + return _.has(nodeStatus.store_statuses[0].metrics, metricName); +}