Skip to content

Commit d6eb0b8

Browse files
craig[bot]marc
andcommitted
Merge #26890
26890: ui: encryption stats on stores report r=mberhault a=mberhault This adds basic file stats to the stores report page. Also improves the styling: - show decoded key info protobuf fields rather than raw proto (eg: creation date rather than unix timestamp) - table styling moved to core style file - full-width cells to head different sections Other changes: - use selectors for memoization - use loading component Release note (admin ui change): add encryption statistics on stores report page Co-authored-by: marc <marc@cockroachlabs.com>
2 parents 9ed0086 + 3145049 commit d6eb0b8

File tree

5 files changed

+179
-57
lines changed

5 files changed

+179
-57
lines changed
Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,117 @@
11
import React from "react";
2+
import _ from "lodash";
3+
import Long from "long";
4+
import moment from "moment";
25

36
import * as protos from "src/js/protos";
47
import { EncryptionStatusProps } from "oss/src/views/reports/containers/stores/encryption";
8+
import { Bytes } from "src/util/format";
9+
import { FixLong } from "src/util/fixLong";
510

6-
export default class EncryptionStatus extends React.Component<EncryptionStatusProps, {}> {
11+
const dateFormat = "Y-MM-DD HH:mm:ss";
12+
13+
export default class EncryptionStatus {
14+
props: EncryptionStatusProps;
15+
16+
constructor(props: EncryptionStatusProps) {
17+
this.props = props;
18+
}
19+
20+
renderHeaderRow(header: string) {
21+
return (
22+
<tr className="stores-table__row">
23+
<td colSpan={2} className="stores-table__cell stores-table__cell--header--row">{header}</td>
24+
</tr>
25+
);
26+
}
727

828
renderSimpleRow(header: string, value: string) {
929
return (
1030
<tr className="stores-table__row">
1131
<th className="stores-table__cell stores-table__cell--header">{header}</th>
12-
<td className="stores-table__cell" title={value}><pre>{value}</pre></td>
32+
<td className="stores-table__cell" title={value}>{value}</td>
1333
</tr>
1434
);
1535
}
1636

17-
render(): React.ReactElement<any> {
37+
renderStoreKey(key: protos.cockroach.ccl.storageccl.engineccl.enginepbccl.IKeyInfo) {
38+
// Get the enum name from its value (eg: "AES128_CTR" for 1).
39+
const encryptionType = protos.cockroach.ccl.storageccl.engineccl.enginepbccl.EncryptionType[key.encryption_type];
40+
const createdAt = moment.unix(FixLong(key.creation_time).toNumber()).utc().format(dateFormat);
41+
42+
return [
43+
this.renderHeaderRow("Active Store Key: user specified"),
44+
this.renderSimpleRow("Algorithm", encryptionType),
45+
this.renderSimpleRow("Key ID", key.key_id),
46+
this.renderSimpleRow("Created", createdAt),
47+
this.renderSimpleRow("Source", key.source),
48+
];
49+
}
50+
51+
renderDataKey(key: protos.cockroach.ccl.storageccl.engineccl.enginepbccl.IKeyInfo) {
52+
// Get the enum name from its value (eg: "AES128_CTR" for 1).
53+
const encryptionType = protos.cockroach.ccl.storageccl.engineccl.enginepbccl.EncryptionType[key.encryption_type];
54+
const createdAt = moment.unix(key.creation_time.toNumber()).utc().format(dateFormat);
55+
56+
return [
57+
this.renderHeaderRow("Active Data Key: automatically generated"),
58+
this.renderSimpleRow("Algorithm", encryptionType),
59+
this.renderSimpleRow("Key ID", key.key_id),
60+
this.renderSimpleRow("Created", createdAt),
61+
this.renderSimpleRow("Parent Key ID", key.parent_key_id),
62+
];
63+
}
64+
65+
calculatePercentage(active: Long, total: Long): number {
66+
if (active.eq(total)) {
67+
return 100;
68+
}
69+
return Long.fromInt(100).mul(active).toNumber() / total.toNumber();
70+
}
71+
72+
renderFileStats(stats: protos.cockroach.server.serverpb.IStoreDetails) {
73+
const totalFiles = FixLong(stats.total_files);
74+
const totalBytes = FixLong(stats.total_bytes);
75+
if (totalFiles.eq(0) && totalBytes.eq(0)) {
76+
return null;
77+
}
78+
79+
const activeFiles = FixLong(stats.active_key_files);
80+
const activeBytes = FixLong(stats.active_key_bytes);
81+
82+
let fileDetails = this.calculatePercentage(activeFiles, totalFiles).toFixed(2) + "%";
83+
fileDetails += " (" + activeFiles + "/" + totalFiles + ")";
84+
85+
let byteDetails = this.calculatePercentage(activeBytes, totalBytes).toFixed(2) + "%";
86+
byteDetails += " (" + Bytes(activeBytes.toNumber()) + "/" + Bytes(totalBytes.toNumber()) + ")";
87+
88+
return [
89+
this.renderHeaderRow("Encryption Progress: fraction encrypted using the active data key"),
90+
this.renderSimpleRow("Files", fileDetails),
91+
this.renderSimpleRow("Bytes", byteDetails),
92+
];
93+
}
94+
95+
getEncryptionRows() {
1896
const { store } = this.props;
1997
const rawStatus = store.encryption_status;
98+
if (_.isEmpty(rawStatus)) {
99+
return [ this.renderSimpleRow("Encryption status", "Not encrypted") ];
100+
}
20101

102+
let decodedStatus;
103+
104+
// Attempt to decode protobuf.
21105
try {
22-
const decodedStatus = protos.cockroach.ccl.storageccl.engineccl.enginepbccl.EncryptionStatus.decode(rawStatus);
23-
return this.renderSimpleRow("Encryption Status", JSON.stringify(decodedStatus.toJSON(), null, 2));
106+
decodedStatus = protos.cockroach.ccl.storageccl.engineccl.enginepbccl.EncryptionStatus.decode(rawStatus);
24107
} catch (e) {
25-
console.log("Error decoding protobuf: ", e);
26-
return null;
108+
return [ this.renderSimpleRow("Encryption status", "Error decoding protobuf: " + e.toString()) ];
27109
}
110+
111+
return [
112+
this.renderStoreKey(decodedStatus.active_store_key),
113+
this.renderDataKey(decodedStatus.active_data_key),
114+
this.renderFileStats(store),
115+
];
28116
}
29117
}

pkg/ui/src/views/reports/containers/stores/encryption.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ export interface EncryptionStatusProps {
66
store: protos.cockroach.server.serverpb.IStoreDetails;
77
}
88

9-
export default class EncryptionStatus extends React.Component<EncryptionStatusProps, {}> {
10-
11-
render(): React.ReactElement<any> {
9+
export default class EncryptionStatus {
10+
getEncryptionRows(): React.ReactElement<any> {
1211
return null;
1312
}
1413
}

pkg/ui/src/views/reports/containers/stores/index.tsx

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import React from "react";
33
import { Helmet } from "react-helmet";
44
import { connect } from "react-redux";
55
import { RouterState } from "react-router";
6+
import { createSelector } from "reselect";
67

78
import * as protos from "src/js/protos";
89
import { storesRequestKey, refreshStores } from "src/redux/apiReducers";
910
import { AdminUIState } from "src/redux/state";
1011
import { nodeIDAttr } from "src/util/constants";
1112
import EncryptionStatus from "src/views/reports/containers/stores/encryption";
13+
import Loading from "src/views/shared/components/loading";
1214

13-
import "./stores.styl";
15+
import spinner from "assets/spinner.gif";
1416

1517
interface StoresOwnProps {
16-
stores: protos.cockroach.server.serverpb.StoresResponse;
18+
stores: protos.cockroach.server.serverpb.IStoreDetails[];
19+
loading: boolean;
1720
lastError: Error;
1821
refreshStores: typeof refreshStores;
1922
}
@@ -58,55 +61,41 @@ class Stores extends React.Component<StoresProps, {}> {
5861
);
5962
}
6063

61-
renderStore(store: protos.cockroach.server.serverpb.IStoreDetails, key: number) {
64+
renderStore = (store: protos.cockroach.server.serverpb.IStoreDetails) => {
6265
return (
63-
<table key={key} className="stores-table">
66+
<table key={store.store_id} className="stores-table">
6467
<tbody>
6568
{ this.renderSimpleRow("Store ID", store.store_id.toString()) }
66-
<EncryptionStatus store={store} />
69+
{ new EncryptionStatus({store: store}).getEncryptionRows() }
6770
</tbody>
6871
</table>
6972
);
7073
}
7174

72-
render() {
75+
renderContent = () => {
7376
const nodeID = this.props.params[nodeIDAttr];
7477
if (!_.isNil(this.props.lastError)) {
7578
return (
76-
<div className="section">
77-
<Helmet>
78-
<title>Stores | Debug</title>
79-
</Helmet>
80-
<h1>Stores</h1>
81-
<h2>Error loading stores for node {nodeID}</h2>
82-
</div>
79+
<h2>Error loading stores for node {nodeID}</h2>
8380
);
8481
}
82+
8583
const { stores } = this.props;
8684
if (_.isEmpty(stores)) {
8785
return (
88-
<div className="section">
89-
<Helmet>
90-
<title>Stores | Debug</title>
91-
</Helmet>
92-
<h1>Stores</h1>
93-
<h2>Loading cluster status...</h2>
94-
</div>
86+
<h2>No stores were found on node {nodeID}.</h2>
9587
);
9688
}
9789

98-
if (_.isEmpty(stores.stores)) {
99-
return (
100-
<div className="section">
101-
<Helmet>
102-
<title>Stores | Debug</title>
103-
</Helmet>
104-
<h1>Stores</h1>
105-
<h2>No stores were found on node {this.props.params[nodeIDAttr]}.</h2>
106-
</div>
107-
);
108-
}
90+
return (
91+
<React.Fragment>
92+
{ _.map(this.props.stores, this.renderStore) }
93+
</React.Fragment>
94+
);
95+
}
10996

97+
render() {
98+
const nodeID = this.props.params[nodeIDAttr];
11099
let header: string = null;
111100
if (_.isNaN(parseInt(nodeID, 10))) {
112101
header = "Local Node";
@@ -121,21 +110,54 @@ class Stores extends React.Component<StoresProps, {}> {
121110
</Helmet>
122111
<h1>Stores</h1>
123112
<h2>{header} stores</h2>
124-
{
125-
_.map(stores.stores, (store, key) => (
126-
this.renderStore(store, key)
127-
))
128-
}
113+
<Loading
114+
loading={this.props.loading}
115+
className="loading-image loading-image__spinner"
116+
image={spinner}
117+
render={this.renderContent}
118+
/>
129119
</div>
130120
);
131121
}
132122
}
133123

134-
function mapStateToProps(state: AdminUIState, props: StoresProps) {
124+
function selectStoresState(state: AdminUIState, props: StoresProps) {
135125
const nodeIDKey = storesRequestKey(storesRequestFromProps(props));
126+
return state.cachedData.stores[nodeIDKey];
127+
}
128+
129+
const selectStoresLoading = createSelector(
130+
selectStoresState,
131+
(stores) => _.isEmpty(stores) || _.isEmpty(stores.data),
132+
);
133+
134+
const selectSortedStores = createSelector(
135+
selectStoresLoading,
136+
selectStoresState,
137+
(loading, stores) => {
138+
if (loading) {
139+
return null;
140+
}
141+
return _.sortBy(stores.data.stores, (store) => store.store_id);
142+
},
143+
);
144+
145+
const selectStoresLastError = createSelector(
146+
selectStoresLoading,
147+
selectStoresState,
148+
(loading, stores) => {
149+
if (loading) {
150+
return null;
151+
}
152+
return stores.lastError;
153+
},
154+
);
155+
156+
function mapStateToProps(state: AdminUIState, props: StoresProps) {
136157
return {
137-
stores: state.cachedData.stores[nodeIDKey] && state.cachedData.stores[nodeIDKey].data,
138-
lastError: state.cachedData.stores[nodeIDKey] && state.cachedData.stores[nodeIDKey].lastError,
158+
stores: selectSortedStores(state, props),
159+
loading: selectStoresLoading(state, props),
160+
lastError: selectStoresLastError(state, props),
139161
};
140162
}
141163

pkg/ui/src/views/reports/containers/stores/stores.styl

Lines changed: 0 additions & 8 deletions
This file was deleted.

pkg/ui/styl/pages/reports.styl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,27 @@ $reports-table
139139
margin 0
140140
padding 0
141141

142+
.stores-table
143+
@extend $reports-table
144+
145+
&__cell
146+
background-color white
147+
padding 6px 12px
148+
max-width none
149+
width 100%
150+
151+
&--header
152+
background-color $link-color
153+
text-align right
154+
width 150px
155+
min-width 150px
156+
157+
&--row
158+
background-color $link-color
159+
text-align center
160+
color white
161+
font-weight 900
162+
142163
.connections-table
143164
@extend $reports-table
144165
font-size 12px

0 commit comments

Comments
 (0)