Skip to content

Commit

Permalink
ui: add data distribution screen (aka replica matrix)
Browse files Browse the repository at this point in the history
with collapsible tree matrix widget

Release note (admin ui change): Add "data distribution" debug page,
showing how table data is distributed across nodes, as well as the zone
configs which are affecting that distribution.
  • Loading branch information
Pete Vilter committed Jun 14, 2018
1 parent 2a2f765 commit 721de4c
Show file tree
Hide file tree
Showing 11 changed files with 1,261 additions and 1 deletion.
4 changes: 4 additions & 0 deletions pkg/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import Layout from "src/views/app/containers/layout";
import { DatabaseTablesList, DatabaseGrantsList } from "src/views/databases/containers/databases";
import TableDetails from "src/views/databases/containers/tableDetails";
import { EventPage } from "src/views/cluster/containers/events";
import DataDistributionPage from "src/views/cluster/containers/dataDistribution";
import Raft from "src/views/devtools/containers/raft";
import RaftRanges from "src/views/devtools/containers/raftRanges";
import RaftMessages from "src/views/devtools/containers/raftMessages";
Expand Down Expand Up @@ -122,6 +123,9 @@ ReactDOM.render(
</Route>
</Route>

{ /* data distribution */ }
<Route path="data-distribution" component={ DataDistributionPage } />

{ /* debug pages */ }
<Route path="debug">
<IndexRoute component={Debug} />
Expand Down
9 changes: 9 additions & 0 deletions pkg/ui/src/redux/apiReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ const queriesReducerObj = new CachedDataReducer(
);
export const refreshQueries = queriesReducerObj.refresh;

const dataDistributionReducerObj = new CachedDataReducer(
api.getDataDistribution,
"dataDistribution",
moment.duration(1, "m"),
);
export const refreshDataDistribution = dataDistributionReducerObj.refresh;

export interface APIReducersState {
cluster: CachedDataReducerState<api.ClusterResponseMessage>;
events: CachedDataReducerState<api.EventsResponseMessage>;
Expand All @@ -274,6 +281,7 @@ export interface APIReducersState {
settings: CachedDataReducerState<api.SettingsResponseMessage>;
stores: KeyedCachedDataReducerState<api.StoresResponseMessage>;
queries: CachedDataReducerState<api.QueriesResponseMessage>;
dataDistribution: CachedDataReducerState<api.DataDistributionResponseMessage>;
}

export const apiReducersReducer = combineReducers<APIReducersState>({
Expand Down Expand Up @@ -302,6 +310,7 @@ export const apiReducersReducer = combineReducers<APIReducersState>({
[settingsReducerObj.actionNamespace]: settingsReducerObj.reducer,
[storesReducerObj.actionNamespace]: storesReducerObj.reducer,
[queriesReducerObj.actionNamespace]: queriesReducerObj.reducer,
[dataDistributionReducerObj.actionNamespace]: dataDistributionReducerObj.reducer,
});

export { CachedDataReducerState, KeyedCachedDataReducerState };
7 changes: 7 additions & 0 deletions pkg/ui/src/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export type UserLogoutResponseMessage = protos.cockroach.server.serverpb.UserLog

export type QueriesResponseMessage = protos.cockroach.server.serverpb.QueriesResponse;

export type DataDistributionResponseMessage = protos.cockroach.server.serverpb.DataDistributionResponse;

// API constants

export const API_PREFIX = "_admin/v1";
Expand Down Expand Up @@ -337,3 +339,8 @@ export function getStores(req: StoresRequestMessage, timeout?: moment.Duration):
export function getQueries(timeout?: moment.Duration): Promise<QueriesResponseMessage> {
return timeoutFetch(serverpb.QueriesResponse, `${API_PREFIX}/queries`, null, timeout);
}

// getDataDistribution returns information about how replicas are distributed across nodes.
export function getDataDistribution(timeout?: moment.Duration): Promise<DataDistributionResponseMessage> {
return timeoutFetch(serverpb.DataDistributionResponse, `${API_PREFIX}/data_distribution`, null, timeout);
}
1 change: 1 addition & 0 deletions pkg/ui/src/util/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const startFlags = docsURL("start-a-node.html#flags");
export const pauseJob = docsURL("pause-job.html");
export const cancelJob = docsURL("cancel-job.html");
export const enableNodeMap = docsURL("enable-node-map.html");
export const configureReplicationZones = docsURL("configure-replication-zones.html");

// Note that these explicitly don't use the current version, since we want to
// link to the most up-to-date documentation available.
Expand Down
11 changes: 11 additions & 0 deletions pkg/ui/src/views/cluster/containers/dataDistribution/index.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.data-distribution
display: flex

&__zone-config-sidebar
padding-right 20px

.zone-config
padding-top 10px

&__raw-yaml
padding-top 5px
216 changes: 216 additions & 0 deletions pkg/ui/src/views/cluster/containers/dataDistribution/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import _ from "lodash";
import React from "react";
import { createSelector } from "reselect";
import { connect } from "react-redux";
import Helmet from "react-helmet";

import Loading from "src/views/shared/components/loading";
import spinner from "assets/spinner.gif";
import { ToolTipWrapper } from "src/views/shared/components/toolTip";
import * as docsURL from "src/util/docs";
import { FixLong } from "src/util/fixLong";
import { cockroach } from "src/js/protos";
import { AdminUIState } from "src/redux/state";
import { refreshDataDistribution, refreshNodes, refreshLiveness } from "src/redux/apiReducers";
import { LocalityTree, selectLocalityTree } from "src/redux/localities";
import ReplicaMatrix, { SchemaObject } from "./replicaMatrix";
import { TreeNode, TreePath } from "./tree";
import "./index.styl";

type DataDistributionResponse = cockroach.server.serverpb.DataDistributionResponse;
type NodeDescriptor = cockroach.roachpb.NodeDescriptor$Properties;
type ZoneConfig$Properties = cockroach.server.serverpb.DataDistributionResponse.ZoneConfig$Properties;

const ZONE_CONFIG_TEXT = (
<span>
Zone configurations
(<a href={docsURL.configureReplicationZones} target="_blank">see documentation</a>)
control how CockroachDB distributes data across nodes.
</span>
);

interface DataDistributionProps {
dataDistribution: DataDistributionResponse;
localityTree: LocalityTree;
sortedZoneConfigs: ZoneConfig$Properties[];
}

class DataDistribution extends React.Component<DataDistributionProps> {

renderZoneConfigs() {
return (
<div className="zone-config-list">
<ul>
{this.props.sortedZoneConfigs.map((zoneConfig) => (
<li key={zoneConfig.cli_specifier} className="zone-config">
<h3>{zoneConfig.cli_specifier}</h3>
<pre className="zone-config__raw-yaml">
{zoneConfig.config_yaml}
</pre>
</li>
))}
</ul>
</div>
);
}

getCellValue = (dbPath: TreePath, nodePath: TreePath): number => {
const [dbName, tableName] = dbPath;
const nodeID = nodePath[nodePath.length - 1];
const databaseInfo = this.props.dataDistribution.database_info;

const res = databaseInfo[dbName].table_info[tableName].replica_count_by_node_id[nodeID];
if (!res) {
return 0;
}
return FixLong(res).toInt();
}

render() {
const nodeTree = nodeTreeFromLocalityTree("Cluster", this.props.localityTree);

const databaseInfo = this.props.dataDistribution.database_info;
const dbTree: TreeNode<SchemaObject> = {
name: "Cluster",
data: { dbName: null, tableName: null },
children: _.map(databaseInfo, (dbInfo, dbName) => ({
name: dbName,
data: { dbName },
children: _.map(dbInfo.table_info, (_tableInfo, tableName) => ({
name: tableName,
data: { dbName, tableName },
})),
})),
};

return (
<div className="data-distribution">
<div className="data-distribution__zone-config-sidebar">
<h2>
Zone Configs{" "}
<div className="section-heading__tooltip">
<ToolTipWrapper text={ZONE_CONFIG_TEXT}>
<div className="section-heading__tooltip-hover-area">
<div className="section-heading__info-icon">i</div>
</div>
</ToolTipWrapper>
</div>
</h2>
{this.renderZoneConfigs()}
</div>
<div>
<ReplicaMatrix
cols={nodeTree}
rows={dbTree}
getValue={this.getCellValue}
/>
</div>
</div>
);
}
}

interface DataDistributionPageProps {
dataDistribution: DataDistributionResponse;
localityTree: LocalityTree;
sortedZoneConfigs: ZoneConfig$Properties[];
refreshDataDistribution: typeof refreshDataDistribution;
refreshNodes: typeof refreshNodes;
refreshLiveness: typeof refreshLiveness;
}

class DataDistributionPage extends React.Component<DataDistributionPageProps> {

componentDidMount() {
this.props.refreshDataDistribution();
this.props.refreshNodes();
this.props.refreshLiveness();
}

componentWillReceiveProps() {
this.props.refreshDataDistribution();
this.props.refreshNodes();
this.props.refreshLiveness();
}

render() {
return (
<div>
<Helmet>
<title>Data Distribution</title>
</Helmet>
<section className="section">
<h1>Data Distribution</h1>
</section>
<section className="section">
<Loading
className="loading-image loading-image__spinner-left"
loading={!this.props.dataDistribution || !this.props.localityTree}
image={spinner}
>
<DataDistribution
localityTree={this.props.localityTree}
dataDistribution={this.props.dataDistribution}
sortedZoneConfigs={this.props.sortedZoneConfigs}
/>
</Loading>
</section>
</div>
);
}
}

const sortedZoneConfigs = createSelector(
(state: AdminUIState) => state.cachedData.dataDistribution,
(dataDistributionState) => {
if (!dataDistributionState.data) {
return null;
}
return _.sortBy(dataDistributionState.data.zone_configs, (zc) => zc.cli_specifier);
},
);

// tslint:disable-next-line:variable-name
const DataDistributionPageConnected = connect(
(state: AdminUIState) => ({
dataDistribution: state.cachedData.dataDistribution.data,
sortedZoneConfigs: sortedZoneConfigs(state),
localityTree: selectLocalityTree(state),
}),
{
refreshDataDistribution,
refreshNodes,
refreshLiveness,
},
)(DataDistributionPage);

export default DataDistributionPageConnected;

// Helpers

function nodeTreeFromLocalityTree(
rootName: string,
localityTree: LocalityTree,
): TreeNode<NodeDescriptor> {
const children: TreeNode<any>[] = [];

// Add child localities.
_.forEach(localityTree.localities, (valuesForKey, key) => {
_.forEach(valuesForKey, (subLocalityTree, value) => {
children.push(nodeTreeFromLocalityTree(`${key}=${value}`, subLocalityTree));
});
});

// Add child nodes.
_.forEach(localityTree.nodes, (node) => {
children.push({
name: node.desc.node_id.toString(),
data: node.desc,
});
});

return {
name: rootName,
children: children,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.matrix
background-color white
border-collapse collapse
border-bottom 1px solid lightgrey
border-top 1px solid lightgrey

thead
border-bottom 1px solid black

th, td
padding 5px
border-left 1px solid lightgrey
border-right 1px solid lightgrey

td.value
text-align center

&__metric-label
font-style italic

&__row--internal-node
cursor pointer

&__row--internal-node:hover
background-color rgb(240, 240, 240)

&__row-header
text-align left

&__row-header--internal-node
font-weight bold

&__column-header
font-weight bold

&__column-header--internal-node
cursor pointer

&__column-header--internal-node:hover
background-color rgb(240, 240, 240)

&__cell-value
text-align right
Loading

0 comments on commit 721de4c

Please sign in to comment.