From c4960a8701ca87800d09ff71c0977772cef6efec Mon Sep 17 00:00:00 2001 From: Andrii Vorobiov Date: Mon, 22 May 2023 13:07:09 +0300 Subject: [PATCH] ui: Latency page UI improvements - use connectivity endpoint as a source of connection statuses and latency; - remove popup with disconnected peers, instead it will be shown on latency matrix; Release note: None --- pkg/ui/workspaces/db-console/src/app.tsx | 5 +- .../db-console/src/redux/apiReducers.ts | 11 + .../db-console/src/redux/connectivity.ts | 4 + pkg/ui/workspaces/db-console/src/util/api.ts | 17 + .../src/views/app/components/chip/index.tsx | 12 +- .../src/views/app/components/chip/styles.styl | 7 + .../reports/containers/network/index.tsx | 418 ++++++++++-------- .../containers/network/latency/index.tsx | 301 +++++++++---- .../network/latency/latency.fixtures.ts | 317 +++++-------- .../network/latency/latency.stories.tsx | 54 ++- .../containers/network/latency/latency.styl | 14 +- .../containers/network/legend/index.tsx | 71 +-- .../reports/containers/network/sort/index.tsx | 28 +- .../db-console/styl/base/palette.styl | 1 + 14 files changed, 677 insertions(+), 583 deletions(-) create mode 100644 pkg/ui/workspaces/db-console/src/redux/connectivity.ts diff --git a/pkg/ui/workspaces/db-console/src/app.tsx b/pkg/ui/workspaces/db-console/src/app.tsx index a1827661a90f..71b7607c3440 100644 --- a/pkg/ui/workspaces/db-console/src/app.tsx +++ b/pkg/ui/workspaces/db-console/src/app.tsx @@ -406,7 +406,10 @@ export const App: React.FC = (props: AppProps) => { path={`/reports/network/:${nodeIDAttr}`} component={Network} /> - + ; events: CachedDataReducerState< @@ -592,6 +601,7 @@ export interface APIReducersState { snapshot: KeyedCachedDataReducerState; rawTrace: KeyedCachedDataReducerState; tenants: CachedDataReducerState; + connectivity: CachedDataReducerState; } export const apiReducersReducer = combineReducers({ @@ -647,6 +657,7 @@ export const apiReducersReducer = combineReducers({ [statementFingerprintInsightsReducerObj.actionNamespace]: statementFingerprintInsightsReducerObj.reducer, [tenantsListObj.actionNamespace]: tenantsListObj.reducer, + [connectivityObj.actionNamespace]: connectivityObj.reducer, }); export { CachedDataReducerState, KeyedCachedDataReducerState }; diff --git a/pkg/ui/workspaces/db-console/src/redux/connectivity.ts b/pkg/ui/workspaces/db-console/src/redux/connectivity.ts new file mode 100644 index 000000000000..81d49e54de4a --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/redux/connectivity.ts @@ -0,0 +1,4 @@ +import { AdminUIState } from "src/redux/state"; + +export const connectivitySelector = (state: AdminUIState) => + state.cachedData.connectivity; diff --git a/pkg/ui/workspaces/db-console/src/util/api.ts b/pkg/ui/workspaces/db-console/src/util/api.ts index d2761b217e83..74e6ce68ea25 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -208,6 +208,11 @@ export type ListTenantsRequestMessage = export type ListTenantsResponseMessage = protos.cockroach.server.serverpb.ListTenantsResponse; +export type NetworkConnectivityRequest = + protos.cockroach.server.serverpb.NetworkConnectivityRequest; +export type NetworkConnectivityResponse = + protos.cockroach.server.serverpb.NetworkConnectivityResponse; + // API constants export const API_PREFIX = "_admin/v1"; @@ -851,6 +856,18 @@ export function getTenants( ); } +export function getNetworkConnectivity( + req: NetworkConnectivityRequest, + timeout?: moment.Duration, +): Promise { + return timeoutFetch( + serverpb.NetworkConnectivityResponse, + `${STATUS_PREFIX}/connectivity`, + req as any, + timeout, + ); +} + export function IsValidateUriName(...args: string[]): Promise { for (const name of args) { if (name.includes("/")) { diff --git a/pkg/ui/workspaces/db-console/src/views/app/components/chip/index.tsx b/pkg/ui/workspaces/db-console/src/views/app/components/chip/index.tsx index 1fd6062b20be..47f775ae1daa 100644 --- a/pkg/ui/workspaces/db-console/src/views/app/components/chip/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/app/components/chip/index.tsx @@ -12,8 +12,16 @@ import React from "react"; import "./styles.styl"; interface IChipProps { - title: string; - type?: "green" | "lightgreen" | "grey" | "blue" | "lightblue" | "yellow"; + title: React.ReactChild; + type?: + | "green" + | "lightgreen" + | "grey" + | "blue" + | "lightblue" + | "yellow" + | "red" + | "white"; } export const Chip: React.SFC = ({ title, type }) => ( diff --git a/pkg/ui/workspaces/db-console/src/views/app/components/chip/styles.styl b/pkg/ui/workspaces/db-console/src/views/app/components/chip/styles.styl index b482448c8e85..d023acac3c82 100644 --- a/pkg/ui/workspaces/db-console/src/views/app/components/chip/styles.styl +++ b/pkg/ui/workspaces/db-console/src/views/app/components/chip/styles.styl @@ -35,3 +35,10 @@ background $chip-blue &--yellow background $chip-yellow + &--red + background $chip-red + &--white + background $colors--neutral-0 + border-color #c0c6d9 + border-style solid + border-width 1px diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/index.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/index.tsx index a921a929bd10..47668630b842 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/index.tsx @@ -16,10 +16,16 @@ import { Helmet } from "react-helmet"; import { connect } from "react-redux"; import { createSelector } from "reselect"; import { withRouter, RouteComponentProps } from "react-router-dom"; +import * as protos from "@cockroachlabs/crdb-protobuf-client"; +import NodeLivenessStatus = protos.cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus; -import { refreshLiveness, refreshNodes } from "src/redux/apiReducers"; import { - LivenessStatus, + CachedDataReducerState, + refreshConnectivity, + refreshLiveness, + refreshNodes, +} from "src/redux/apiReducers"; +import { NodesSummary, nodesSummarySelector, selectLivenessRequestStatus, @@ -27,7 +33,6 @@ import { } from "src/redux/nodes"; import { AdminUIState } from "src/redux/state"; import { util } from "@cockroachlabs/cluster-ui"; -import { FixLong } from "src/util/fixLong"; import { trackFilter, trackCollapseNodes } from "src/util/analytics"; import { getFilters, @@ -41,20 +46,27 @@ import { Legend } from "./legend"; import Sort from "./sort"; import { getMatchParamByName } from "src/util/query"; import "./network.styl"; +import { connectivitySelector } from "src/redux/connectivity"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; interface NetworkOwnProps { nodesSummary: NodesSummary; nodeSummaryErrors: Error[]; + connectivity: CachedDataReducerState; refreshNodes: typeof refreshNodes; refreshLiveness: typeof refreshLiveness; + refreshConnectivity: typeof refreshConnectivity; + showDeadNodes: boolean; } -export interface Identity { +export type Identity = { nodeID: number; address: string; locality?: string; updatedAt: moment.Moment; -} + livenessStatus: protos.cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus; + connectivity: protos.cockroach.server.serverpb.NetworkConnectivityResponse.IConnectivity; +}; export interface NoConnection { from: Identity; @@ -86,6 +98,30 @@ function contentAvailable(nodesSummary: NodesSummary) { ); } +export const isHealthyLivenessStatus = ( + status: NodeLivenessStatus, +): boolean => { + switch (status) { + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_LIVE: + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_DRAINING: + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_DECOMMISSIONING: + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_UNKNOWN: + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_UNAVAILABLE: + return true; + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_DEAD: + case cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_DECOMMISSIONED: + default: + return false; + } +}; + export function getValueFromString( key: string, params: string, @@ -113,6 +149,7 @@ export class Network extends React.Component { refresh(props = this.props) { props.refreshLiveness(); props.refreshNodes(); + props.refreshConnectivity(); } componentDidMount() { @@ -120,10 +157,8 @@ export class Network extends React.Component { this.refresh(); } - componentDidUpdate(prevProps: NetworkProps) { - if (!_.isEqual(this.props.location, prevProps.location)) { - this.refresh(this.props); - } + componentDidUpdate(_prevProps: NetworkProps) { + this.refresh(this.props); } onChangeCollapse = (collapsed: boolean) => { @@ -131,6 +166,14 @@ export class Network extends React.Component { this.setState({ collapsed }); }; + onShowDeadNodesChange = (showDeadNodes: boolean) => { + const history = this.props.history; + const search = new URLSearchParams(history.location.search); + search.set("showDeadNodes", `${showDeadNodes}`); + this.props.location.search = search.toString(); + this.props.history.replace(this.props.location); + }; + onChangeFilter = (key: string, value: string) => { const { filter } = this.state; const newFilter = filter ? filter : {}; @@ -164,6 +207,7 @@ export class Network extends React.Component { filteredDisplayIdentities = (displayIdentities: Identity[]) => { const { filter } = this.state; + const { showDeadNodes } = this.props; let data: Identity[] = []; let selectedIndex = 0; if ( @@ -173,6 +217,7 @@ export class Network extends React.Component { ) { return displayIdentities; } + displayIdentities.forEach(identities => { Object.keys(filter).forEach((key, index) => { const value = getValueFromString( @@ -203,17 +248,16 @@ export class Network extends React.Component { } }); }); + if (!showDeadNodes) { + data = data.filter(identity => + isHealthyLivenessStatus(identity.livenessStatus), + ); + } return data; }; - renderLatencyTable( - latencies: number[], - staleIDs: Set, - nodesSummary: NodesSummary, - displayIdentities: Identity[], - noConnections: NoConnection[], - ) { - const { match } = this.props; + renderLatencyTable(latencies: number[], displayIdentities: Identity[]) { + const { match, showDeadNodes } = this.props; const nodeId = getMatchParamByName(match, "node_id"); const { collapsed, filter } = this.state; const mean = d3Mean(latencies); @@ -231,11 +275,9 @@ export class Network extends React.Component { const latencyTable = ( { } // legend is just a quick table showing the standard deviation values. - return [ - , -
- + - {latencyTable} -
, - ]; +
+ + {latencyTable} +
+ + ); } getSortParams = (data: Identity[]) => { const sort: NetworkSort[] = []; const searchQuery = (params: string) => `cluster,${params}`; - data.forEach(values => { - const localities = searchQuery(values.locality).split(","); - localities.forEach((locality: string) => { - if (locality !== "") { - const value = locality.match(/^\w+/gi) - ? locality.match(/^\w+/gi)[0] - : null; - if (!sort.some(x => x.id === value)) { - const sortValue: NetworkSort = { id: value, filters: [] }; - data.forEach(item => { - const valueLocality = searchQuery(values.locality).split(","); - const itemLocality = searchQuery(item.locality); - valueLocality.forEach(val => { - const itemLocalitySplited = val.match(/^\w+/gi) - ? val.match(/^\w+/gi)[0] - : null; - if (val === "cluster" && value === "cluster") { - sortValue.filters = [ - ...sortValue.filters, - { - name: item.nodeID.toString(), - address: item.address, - }, - ]; - } else if ( - itemLocalitySplited === value && - !sortValue.filters.reduce( - (accumulator, vendor) => - accumulator || - vendor.name === getValueFromString(value, itemLocality), - false, - ) - ) { - sortValue.filters = [ - ...sortValue.filters, - { - name: getValueFromString(value, itemLocality), - address: item.address, - }, - ]; - } - }); - }); - sort.push(sortValue); + data + .filter(d => !!d.locality) // filter out dead nodes that don't have locality props + .forEach(values => { + const localities = searchQuery(values.locality).split(","); + localities.forEach((locality: string) => { + if (locality !== "") { + const value = locality.match(/^\w+/gi) + ? locality.match(/^\w+/gi)[0] + : null; + if (!sort.some(x => x.id === value)) { + const sortValue: NetworkSort = { id: value, filters: [] }; + data + .filter(d => !!d.locality) // filter out dead nodes that don't have locality props + .forEach(item => { + const valueLocality = searchQuery(values.locality).split(","); + const itemLocality = searchQuery(item.locality); + valueLocality.forEach(val => { + const address = item.address ?? ""; + const itemLocalitySplited = val.match(/^\w+/gi) + ? val.match(/^\w+/gi)[0] + : null; + if (val === "cluster" && value === "cluster") { + sortValue.filters = [ + ...sortValue.filters, + { + name: item.nodeID.toString(), + address: address, + }, + ]; + } else if ( + itemLocalitySplited === value && + !sortValue.filters.reduce( + (accumulator, vendor) => + accumulator || + vendor.name === + getValueFromString(value, itemLocality), + false, + ) + ) { + sortValue.filters = [ + ...sortValue.filters, + { + name: getValueFromString(value, itemLocality), + address: address, + }, + ]; + } + }); + }); + sort.push(sortValue); + } } - } + }); }); - }); return sort; }; - getDisplayIdentities = ( - healthyIDsContext: _.CollectionChain, - staleIDsContext: _.CollectionChain, - identityByID: Map, - ) => { + getDisplayIdentities = (identityByID: Map): Identity[] => { const { match } = this.props; const nodeId = getMatchParamByName(match, "node_id"); - const identityContent = healthyIDsContext - .union(staleIDsContext.value()) - .map(nodeID => identityByID.get(nodeID)) - .sortBy(identity => identity.nodeID); + const identityContent = _.chain(Array.from(identityByID.values())).sortBy( + identity => identity.nodeID, + ); const sort = this.getSortParams(identityContent.value()); if (sort.some(x => x.id === nodeId)) { return identityContent @@ -348,126 +394,96 @@ export class Network extends React.Component { return identityContent.value(); }; - renderContent(nodesSummary: NodesSummary, filters: NodeFilterListProps) { + renderContent( + nodesSummary: NodesSummary, + filters: NodeFilterListProps, + connections: protos.cockroach.server.serverpb.NetworkConnectivityResponse["connections"], + ) { if (!contentAvailable(nodesSummary)) { return null; } + const { showDeadNodes } = this.props; + // Following states can be observed: + // 1. live connection + // 2. partitioned connection + // 3. node is removed/decommissioned and + // a) it still has connection to another nodes; + // b) it doesn't have connections + + // Combine Node Ids known by gossip client (from `connectivity`) and from + // Nodes api to make sure we show all known nodes. + const knownNodeIds = _.union( + Object.keys(nodesSummary.livenessByNodeID), + Object.keys(connections), + ); + // List of node identities. const identityByID: Map = new Map(); - _.forEach(nodesSummary.nodeStatuses, status => { - identityByID.set(status.desc.node_id, { - nodeID: status.desc.node_id, - address: status.desc.address.address_field, - locality: localityToString(status.desc.locality), - updatedAt: util.LongToMoment(status.updated_at), + + knownNodeIds.forEach(nodeId => { + const nodeIdInt = _.parseInt(nodeId); + const status = nodesSummary.nodeStatusByID[nodeId]; + identityByID.set(nodeIdInt, { + nodeID: nodeIdInt, + address: status?.desc.address.address_field, + locality: status && localityToString(status.desc.locality), + updatedAt: status && util.LongToMoment(status.updated_at), + livenessStatus: + nodesSummary.livenessStatusByNodeID[nodeId] || + protos.cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus + .NODE_STATUS_UNKNOWN, + connectivity: connections[nodeId], }); }); - // Calculate the mean and sampled standard deviation. - let healthyIDsContext = _.chain(nodesSummary.nodeIDs) - .filter( - nodeID => - nodesSummary.livenessStatusByNodeID[nodeID] === - LivenessStatus.NODE_STATUS_LIVE, - ) - .filter(nodeID => !_.isNil(nodesSummary.nodeStatusByID[nodeID].activity)) - .map(nodeID => Number.parseInt(nodeID, 0)); - let staleIDsContext = _.chain(nodesSummary.nodeIDs) - .filter( - nodeID => - nodesSummary.livenessStatusByNodeID[nodeID] === - LivenessStatus.NODE_STATUS_UNAVAILABLE, - ) - .map(nodeID => Number.parseInt(nodeID, 0)); - if (!_.isNil(filters.nodeIDs) && filters.nodeIDs.size > 0) { - healthyIDsContext = healthyIDsContext.filter(nodeID => - filters.nodeIDs.has(nodeID), - ); - staleIDsContext = staleIDsContext.filter(nodeID => - filters.nodeIDs.has(nodeID), - ); - } - if (!_.isNil(filters.localityRegex)) { - healthyIDsContext = healthyIDsContext.filter(nodeID => - filters.localityRegex.test( - localityToString(nodesSummary.nodeStatusByID[nodeID].desc.locality), - ), - ); - staleIDsContext = staleIDsContext.filter(nodeID => - filters.localityRegex.test( - localityToString(nodesSummary.nodeStatusByID[nodeID].desc.locality), - ), - ); - } - const healthyIDs = healthyIDsContext.value(); - const staleIDs = new Set(staleIDsContext.value()); - const displayIdentities: Identity[] = this.getDisplayIdentities( - healthyIDsContext, - staleIDsContext, - identityByID, - ); - const latencies = _.flatMap(healthyIDs, nodeIDa => - _.chain(healthyIDs) - .without(nodeIDa) - .map(nodeIDb => nodesSummary.nodeStatusByID[nodeIDa].activity[nodeIDb]) - .filter(activity => !_.isNil(activity) && !_.isNil(activity.latency)) - .map(activity => util.NanoToMilli(FixLong(activity.latency).toNumber())) - .filter(ms => _.isFinite(ms) && ms > 0) - .value(), - ); + // apply filters to exclude items that don't satisfy filter conditions. + identityByID.forEach((identity, nodeId) => { + if ( + (filters.nodeIDs?.size > 0 && !filters.nodeIDs?.has(nodeId)) || + (!!filters.localityRegex && + filters.localityRegex?.test(identity.locality)) || + (!showDeadNodes && !isHealthyLivenessStatus(identity.livenessStatus)) + ) { + identityByID.delete(nodeId); + } + }); - const noConnections: NoConnection[] = _.flatMap(healthyIDs, nodeIDa => - _.chain(nodesSummary.nodeStatusByID[nodeIDa].activity) - .keys() - .map(nodeIDb => Number.parseInt(nodeIDb, 10)) - .difference(healthyIDs) - .map(nodeIDb => ({ - from: identityByID.get(nodeIDa), - to: identityByID.get(nodeIDb), - })) - .sortBy(noConnection => noConnection.to.nodeID) - .sortBy(noConnection => noConnection.to.locality) - .sortBy(noConnection => noConnection.from.nodeID) - .sortBy(noConnection => noConnection.from.locality) - .value(), - ); + const displayIdentities: Identity[] = + this.getDisplayIdentities(identityByID); - let content: JSX.Element | JSX.Element[]; - if (_.isEmpty(healthyIDs)) { - content = ( -

No healthy nodes match the filters

- ); - } else if (latencies.length < 1) { - content = ( + const latencies: number[] = _.chain(connections) + .values() + .flatMap(v => Object.values(v.peers)) + .flatMap(v => v.latency) + .filter(v => v !== undefined && v.nanos !== undefined) + .map(v => util.NanoToMilli(v.nanos)) + .value(); + + if (_.isEmpty(identityByID)) { + return

No nodes match the filters

; + } + if (knownNodeIds.length < 2) { + return (

- Cannot show latency chart without two healthy nodes. + Cannot show latency chart for cluster with less than 2 nodes.

); - } else { - content = this.renderLatencyTable( - latencies, - staleIDs, - nodesSummary, - displayIdentities, - noConnections, - ); } - return [ - content, - // staleTable(staleIdentities), - // noConnectionTable(noConnections), - ]; + return this.renderLatencyTable(latencies, displayIdentities); } render() { - const { nodesSummary, location } = this.props; + const { nodesSummary, location, connectivity } = this.props; const filters = getFilters(location); + return ( - -

Network

+ +

Network Diagnostics

{ nodeIDs={filters.nodeIDs} localityRegex={filters.localityRegex} /> - {this.renderContent(nodesSummary, filters)} + {this.renderContent( + nodesSummary, + filters, + connectivity?.data?.connections, + )} )} /> @@ -486,6 +506,13 @@ export class Network extends React.Component { } } +const selectShowDeadNodes = createSelector( + (state: AdminUIState) => state.router.location, + location => { + return location.query["showDeadNodes"] === "true"; + }, +); + const nodeSummaryErrors = createSelector( selectNodeRequestStatus, selectLivenessRequestStatus, @@ -495,11 +522,14 @@ const nodeSummaryErrors = createSelector( const mapStateToProps = (state: AdminUIState) => ({ nodesSummary: nodesSummarySelector(state), nodeSummaryErrors: nodeSummaryErrors(state), + connectivity: connectivitySelector(state), + showDeadNodes: selectShowDeadNodes(state), }); const mapDispatchToProps = { refreshNodes, refreshLiveness, + refreshConnectivity, }; export default withRouter( diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/index.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/index.tsx index 76cbcdfa8730..78de8790b779 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/index.tsx @@ -8,19 +8,25 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { Divider, Tooltip } from "antd"; +import { Badge, Divider, Icon, Tooltip } from "antd"; +import "antd/lib/icon/style"; +import "antd/lib/badge/style"; import "antd/lib/divider/style"; import "antd/lib/tooltip/style"; import classNames from "classnames"; import _ from "lodash"; import { util } from "@cockroachlabs/cluster-ui"; -import { FixLong } from "src/util/fixLong"; import { Chip } from "src/views/app/components/chip"; import React from "react"; import { Link } from "react-router-dom"; -import { getValueFromString, Identity } from ".."; +import { getValueFromString, Identity, isHealthyLivenessStatus } from ".."; import "./latency.styl"; import { Empty } from "src/components/empty"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import { livenessNomenclature } from "src/redux/nodes"; +import { BadgeProps } from "antd/lib/badge"; +import NodeLivenessStatus = cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus; +import ConnectionStatus = cockroach.server.serverpb.NetworkConnectivityResponse.ConnectionStatus; interface StdDev { stddev: number; @@ -32,10 +38,8 @@ interface StdDev { export interface ILatencyProps { displayIdentities: Identity[]; - staleIDs: Set; multipleHeader: boolean; collapsed?: boolean; - nodesSummary: any; std?: StdDev; node_id?: string; } @@ -50,22 +54,62 @@ type DetailedIdentity = Identity & { // createHeaderCell creates and decorates a header cell. function createHeaderCell( - staleIDs: Set, - id: DetailedIdentity, - key: string, + identity: DetailedIdentity, isMultiple?: boolean, collapsed?: boolean, ) { - const node = `n${id.nodeID.toString()}`; + const node = `n${identity.nodeID.toString()}`; const className = classNames( "latency-table__cell", "latency-table__cell--header", - { "latency-table__cell--header-warning": staleIDs.has(id.nodeID) }, { "latency-table__cell--start": isMultiple }, ); + const isDecommissioned = + identity.livenessStatus === NodeLivenessStatus.NODE_STATUS_DECOMMISSIONED; + + let nodeBadgeStatus: BadgeProps["status"]; + + switch (identity.livenessStatus) { + case NodeLivenessStatus.NODE_STATUS_LIVE: + nodeBadgeStatus = "success"; + break; + case NodeLivenessStatus.NODE_STATUS_DRAINING: + case NodeLivenessStatus.NODE_STATUS_UNAVAILABLE: + case NodeLivenessStatus.NODE_STATUS_DECOMMISSIONING: + nodeBadgeStatus = "warning"; + break; + case NodeLivenessStatus.NODE_STATUS_DEAD: + nodeBadgeStatus = "error"; + break; + case NodeLivenessStatus.NODE_STATUS_DECOMMISSIONED: + default: + nodeBadgeStatus = "default"; + } + // Render cell header without link to node details as it is not available + // for dead and decommissioned nodes. + if (!isHealthyLivenessStatus(identity.livenessStatus)) { + return ( + +
+ {collapsed ? "" : node} + {!collapsed && ( + + )} +
+ + ); + } return ( - - {collapsed ? "" : node} + + + {collapsed ? "" : node} + {!collapsed && ( + + )} + ); } @@ -117,32 +161,43 @@ const generateCollapsedData = ( const renderMultipleHeaders = ( displayIdentities: Identity[], collapsed: boolean, - nodesSummary: any, - staleIDs: Set, nodeId: string, multipleHeader: boolean, ) => { const data: any = []; let rowLength = 0; const filteredData = displayIdentities.map(identityA => { - const row: any[] = []; + const row: Array<{ + latency: number; + identityB: Identity; + connectivity: cockroach.server.serverpb.NetworkConnectivityResponse.ConnectionStatus; + }> = []; displayIdentities.forEach(identityB => { - const a = nodesSummary.nodeStatusByID[identityA.nodeID].activity; - const nano = FixLong(a[identityB.nodeID]?.latency || 0); + const connStatus = + identityA.connectivity?.peers[identityB.nodeID]?.status ?? + ConnectionStatus.UNKNOWN; + const latency = util.NanoToMilli( + identityA.connectivity?.peers[identityB.nodeID]?.latency?.nanos ?? 0, + ); + // When source and target nodes are the same. if (identityA.nodeID === identityB.nodeID) { - row.push({ latency: 0, identityB }); - } else if ( - staleIDs.has(identityA.nodeID) || - staleIDs.has(identityB.nodeID) || - _.isNil(a) || - _.isNil(a[identityB.nodeID]) - ) { - row.push({ latency: -2, identityB }); - } else if (nano.eq(0)) { - row.push({ latency: -1, identityB }); + row.push({ + latency: 0, + identityB, + connectivity: undefined, + }); + } else if (latency === 0) { + row.push({ + latency: -1, + identityB, + connectivity: connStatus, + }); } else { - const latency = util.NanoToMilli(nano.toNumber()); - row.push({ latency, identityB }); + row.push({ + latency, + identityB, + connectivity: connStatus, + }); } }); rowLength = row.length; @@ -187,7 +242,13 @@ const getLatencyCell = ( latency, identityB, identityA, - }: { latency: number; identityB: Identity; identityA: DetailedIdentity }, + connectivity, + }: { + latency: number; + identityB: Identity; + identityA: DetailedIdentity; + connectivity: cockroach.server.serverpb.NetworkConnectivityResponse.ConnectionStatus; + }, verticalLine: boolean, isMultiple?: boolean, std?: StdDev, @@ -199,6 +260,8 @@ const getLatencyCell = ( { "latency-table__cell--start": verticalLine }, ...names, ); + + // Case when identityA == identityB, display empty cell. if (latency === 0) { return ( ); } - if (latency === -1) { - return ( - - - - ); - } + + const isHealthyTargetNode = isHealthyLivenessStatus(identityB.livenessStatus); + + const isEstablished = connectivity === ConnectionStatus.ESTABLISHED; + const isPartitioned = + isHealthyTargetNode && connectivity === ConnectionStatus.PARTITIONED; + const isNotConnected = connectivity === ConnectionStatus.NOT_CONNECTED; + const isClosedConnection = + isHealthyLivenessStatus(identityA.livenessStatus) && + isHealthyLivenessStatus(identityB.livenessStatus) && + connectivity === ConnectionStatus.CLOSED; + const isUnknown = + isHealthyLivenessStatus(identityA.livenessStatus) && + isHealthyLivenessStatus(identityB.livenessStatus) && + connectivity === ConnectionStatus.UNKNOWN; + + const errorMessage = identityA.connectivity?.peers[identityB.nodeID]?.error; + const className = classNames({ "latency-table__cell": true, "latency-table__cell--end": isMultiple, @@ -241,7 +310,10 @@ const getLatencyCell = ( std.stddev > 0 && latency > std.stddevPlus2, }); const type: any = classNames({ - yellow: latency === -2, + _: !isHealthyTargetNode, + red: isPartitioned, + white: isNotConnected, + yellow: isClosedConnection || isUnknown, green: latency > 0 && std.stddev > 0 && latency < std.stddevMinus2, lightgreen: latency > 0 && @@ -282,26 +354,69 @@ const getLatencyCell = (
-

{`Node ${identityB.nodeID}`}

- {renderDescription(identityB.locality)} +
+ From +
+

{`Node ${identityA.nodeID}`}

+ {renderDescription(identityA.locality)}
-

{`Node ${identityA.nodeID}`}

- {renderDescription(identityA.locality)} +
+ To +
+

{`Node ${identityB.nodeID}`}

+ {renderDescription(identityB.locality)}
- {latency > 0 && ( + {latency > 0 && isEstablished && (

{`${latency.toFixed(2)}ms roundtrip`}

)} + {isPartitioned && ( +

+ Failed connection +

+ )} + {isNotConnected && ( +

+ No connection established +

+ )} + {isClosedConnection && ( +

+ Connection has been closed +

+ )} + {isUnknown && ( +

+ Unknown connection state +

+ )} + {errorMessage && ( +

+ {errorMessage} +

+ )}
} >
0 ? latency.toFixed(2) + "ms" : "--"} + title={ + isPartitioned ? ( + + ) : isNotConnected ? ( + "--" + ) : isClosedConnection || isUnknown ? ( + + ) : latency > 0 ? ( + latency.toFixed(2) + "ms" + ) : ( + "--" + ) + } type={type} />
@@ -313,18 +428,14 @@ const getLatencyCell = ( export const Latency: React.SFC = ({ displayIdentities, - staleIDs, multipleHeader, collapsed, - nodesSummary, std, node_id, }) => { const data = renderMultipleHeaders( displayIdentities, collapsed, - nodesSummary, - staleIDs, node_id, multipleHeader, ); @@ -351,7 +462,11 @@ export const Latency: React.SFC = ({ {_.map(data, (value, index) => ( - + {value[0].title} ))} @@ -361,14 +476,10 @@ export const Latency: React.SFC = ({ {multipleHeader && } - {_.map(data, value => - _.map(value, (identity, index: number) => - createHeaderCell( - staleIDs, - identity, - `0-${value.nodeID}`, - index === 0, - collapsed, + {React.Children.toArray( + _.map(data, value => + _.map(value, (identity, index: number) => + createHeaderCell(identity, index === 0, collapsed), ), ), )} @@ -376,41 +487,37 @@ export const Latency: React.SFC = ({ )} - {_.map(data, (value, index) => - _.map(data[index], (identityA, indA: number) => { - return ( - - {multipleHeader && Number(indA) === 0 && ( - - {value[0].title} - - )} - {createHeaderCell( - staleIDs, - identityA, - `${identityA.nodeID}-0`, - false, - collapsed, - )} - {_.map(identityA.row, (identity: any, indexB: number) => - getLatencyCell( - { ...identity, identityA }, - getVerticalLines(data, indexB), - false, - std, - collapsed, - ), - )} - - ); - }), + {React.Children.toArray( + _.map(data, (value, index) => + _.map(data[index], (identityA, indA: number) => { + return ( + + {multipleHeader && Number(indA) === 0 && ( + + {value[0].title} + + )} + {createHeaderCell(identityA, false, collapsed)} + {_.map(identityA.row, (identity: any, indexB: number) => + getLatencyCell( + { ...identity, identityA }, + getVerticalLines(data, indexB), + false, + std, + collapsed, + ), + )} + + ); + }), + ), )} diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.fixtures.ts b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.fixtures.ts index 5c2365d8558a..ab51c5ae7e60 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.fixtures.ts +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.fixtures.ts @@ -9,143 +9,122 @@ // licenses/APL.txt. import moment from "moment-timezone"; -import Long from "long"; import { ILatencyProps } from "."; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import NodeLivenessStatus = cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus; +import IPeer = cockroach.server.serverpb.NetworkConnectivityResponse.IPeer; +import ConnectionStatus = cockroach.server.serverpb.NetworkConnectivityResponse.ConnectionStatus; -const node1 = { - desc: { - node_id: 1, - locality: { - tiers: [ - { key: "region", value: "local" }, - { key: "zone", value: "local" }, - ], - }, - }, - activity: { - "1": { incoming: 85125, outgoing: 204928, latency: Long.fromInt(843506) }, - "2": { - incoming: 1641005, - outgoing: 196537462, - latency: Long.fromInt(12141), - }, - "3": { incoming: 27851, outgoing: 15530093 }, - "4": { - incoming: 4346803, - outgoing: 180065134, - latency: Long.fromInt(505076), - }, - }, -}; -const node2 = { - desc: { - node_id: 2, - locality: { - tiers: [ - { key: "region", value: "local" }, - { key: "zone", value: "local" }, - ], - }, - }, - activity: { - "1": { - incoming: 8408467, - outgoing: 97930352, - latency: Long.fromInt(1455817), - }, - "2": {}, - "3": { incoming: 22800, outgoing: 13925347 }, - "4": { - incoming: 1328435, - outgoing: 63271505, - latency: Long.fromInt(766402), - }, - }, -}; -const node3 = { - desc: { - node_id: 3, - locality: { - tiers: [ - { key: "region", value: "local" }, - { key: "zone", value: "local" }, - ], - }, - }, - activity: { - "1": { incoming: 49177, outgoing: 173961 }, - "2": { incoming: 98747, outgoing: 80848 }, - "3": {}, - "4": { incoming: 18239, outgoing: 12407 }, - }, -}; -const node4 = { - desc: { - node_id: 4, - locality: { - tiers: [ - { key: "region", value: "local" }, - { key: "zone", value: "local" }, - ], - }, - }, - - activity: { - "1": { - incoming: 4917367, - outgoing: 51102302, - latency: Long.fromInt(1589900), - }, - "2": { - incoming: 2657214, - outgoing: 24963000, - latency: Long.fromInt(1807269), - }, - "3": { incoming: 26480, outgoing: 251052 }, - "4": {}, - }, +const makePeer = (params?: Partial): IPeer => { + const peer: IPeer = { + latency: { nanos: Math.random() * 1000000 }, + status: ConnectionStatus.ESTABLISHED, + address: "127.0.0.1:26257", + locality: { tiers: [{ key: "az", value: "us" }] }, + ...params, + }; + return peer; }; export const latencyFixture: ILatencyProps = { displayIdentities: [ { nodeID: 1, + livenessStatus: NodeLivenessStatus.NODE_STATUS_LIVE, + connectivity: { + peers: { + "1": makePeer(), + "2": makePeer({ status: ConnectionStatus.PARTITIONED }), + "3": makePeer({ + status: ConnectionStatus.PARTITIONED, + latency: { nanos: 0 }, + }), + "4": makePeer(), + "5": makePeer(), + }, + }, address: "127.0.0.1:26257", locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:26:08.122Z"), }, { + livenessStatus: NodeLivenessStatus.NODE_STATUS_LIVE, + connectivity: { + peers: { + "1": makePeer(), + "2": makePeer(), + "3": makePeer(), + "4": makePeer({ + status: ConnectionStatus.NOT_CONNECTED, + }), + "5": makePeer({ + status: ConnectionStatus.NOT_CONNECTED, + latency: { nanos: 0 }, + }), + }, + }, nodeID: 2, address: "127.0.0.1:26259", locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:26:08.674Z"), }, { + livenessStatus: NodeLivenessStatus.NODE_STATUS_LIVE, + connectivity: { + peers: { + "1": makePeer({ status: ConnectionStatus.UNKNOWN }), + "2": makePeer({ + status: ConnectionStatus.UNKNOWN, + latency: { nanos: 0 }, + }), + "3": makePeer(), + "4": makePeer(), + "5": makePeer(), + }, + }, nodeID: 3, address: "127.0.0.1:26261", locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:25:52.640Z"), }, { + livenessStatus: NodeLivenessStatus.NODE_STATUS_LIVE, + connectivity: { + peers: { + "1": makePeer({ status: ConnectionStatus.CLOSED }), + "2": makePeer({ + status: ConnectionStatus.CLOSED, + latency: { nanos: 0 }, + }), + "3": makePeer(), + "4": makePeer(), + "5": makePeer(), + }, + }, nodeID: 4, address: "127.0.0.1:26263", locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:26:09.871Z"), }, + { + livenessStatus: NodeLivenessStatus.NODE_STATUS_LIVE, + connectivity: { + peers: { + "1": makePeer(), + "2": makePeer(), + "3": makePeer(), + "4": makePeer(), + "5": makePeer(), + }, + }, + nodeID: 5, + address: "127.0.0.1:26263", + locality: "region=local,zone=local", + updatedAt: moment("2020-05-04T16:26:09.871Z"), + }, ], - staleIDs: new Set([3]), multipleHeader: true, collapsed: false, - nodesSummary: { - nodeStatuses: [node1, node2, node3, node4], - nodeIDs: [1, 2, 3, 4], - nodeStatusByID: { - "1": node1, - "2": node2, - "3": node3, - "4": node4, - }, - }, std: { stddev: 0.3755919319616704, stddevMinus2: 0.29658363607665916, @@ -156,123 +135,67 @@ export const latencyFixture: ILatencyProps = { node_id: "region", }; -const nodeNoLocality1 = { - desc: { - node_id: 1, - }, - activity: { - "1": { incoming: 85125, outgoing: 204928, latency: Long.fromInt(843506) }, - "2": { - incoming: 1641005, - outgoing: 196537462, - latency: Long.fromInt(12141), - }, - "3": { incoming: 27851, outgoing: 15530093 }, - "4": { - incoming: 4346803, - outgoing: 180065134, - latency: Long.fromInt(505076), - }, - }, -}; -const nodeNoLocality2 = { - desc: { - node_id: 2, - }, - activity: { - "1": { - incoming: 8408467, - outgoing: 97930352, - latency: Long.fromInt(1455817), - }, - "2": {}, - "3": { incoming: 22800, outgoing: 13925347 }, - "4": { - incoming: 1328435, - outgoing: 63271505, - latency: Long.fromInt(766402), - }, - }, -}; -const nodeNoLocality3 = { - desc: { - node_id: 3, - }, - activity: { - "1": { incoming: 49177, outgoing: 173961 }, - "2": { incoming: 98747, outgoing: 80848 }, - "3": {}, - "4": { incoming: 18239, outgoing: 12407 }, - }, -}; -const nodeNoLocality4 = { - desc: { - node_id: 4, - locality: { - tiers: [ - { key: "region", value: "local" }, - { key: "zone", value: "local" }, - ], - }, - }, - - activity: { - "1": { - incoming: 4917367, - outgoing: 51102302, - latency: Long.fromInt(1589900), - }, - "2": { - incoming: 2657214, - outgoing: 24963000, - latency: Long.fromInt(1807269), - }, - "3": { incoming: 26480, outgoing: 251052 }, - "4": {}, - }, -}; - -export const latencyFixtureNoLocality: ILatencyProps = { +export const latencyFixtureWithNodeStatuses: ILatencyProps = { displayIdentities: [ { nodeID: 1, + livenessStatus: NodeLivenessStatus.NODE_STATUS_LIVE, + connectivity: { + peers: { + "1": makePeer(), + "2": makePeer({ status: ConnectionStatus.NOT_CONNECTED }), + "3": makePeer({ + status: ConnectionStatus.NOT_CONNECTED, + latency: { nanos: 0 }, + }), + "4": makePeer({ status: ConnectionStatus.NOT_CONNECTED }), + "5": makePeer(), + }, + }, address: "127.0.0.1:26257", + locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:26:08.122Z"), }, { + livenessStatus: NodeLivenessStatus.NODE_STATUS_DEAD, + connectivity: null, nodeID: 2, address: "127.0.0.1:26259", + locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:26:08.674Z"), }, { + livenessStatus: NodeLivenessStatus.NODE_STATUS_UNAVAILABLE, + connectivity: null, nodeID: 3, address: "127.0.0.1:26261", + locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:25:52.640Z"), }, { + livenessStatus: NodeLivenessStatus.NODE_STATUS_DECOMMISSIONED, + connectivity: null, nodeID: 4, address: "127.0.0.1:26263", + locality: "region=local,zone=local", + updatedAt: moment("2020-05-04T16:26:09.871Z"), + }, + { + livenessStatus: NodeLivenessStatus.NODE_STATUS_DECOMMISSIONING, + connectivity: { + peers: { + "1": makePeer(), + "5": makePeer(), + }, + }, + nodeID: 5, + address: "127.0.0.1:26263", + locality: "region=local,zone=local", updatedAt: moment("2020-05-04T16:26:09.871Z"), }, ], - staleIDs: new Set([3]), multipleHeader: true, collapsed: false, - nodesSummary: { - nodeStatuses: [ - nodeNoLocality1, - nodeNoLocality2, - nodeNoLocality3, - nodeNoLocality4, - ], - nodeIDs: [1, 2, 3, 4], - nodeStatusByID: { - "1": nodeNoLocality1, - "2": nodeNoLocality2, - "3": nodeNoLocality3, - "4": nodeNoLocality4, - }, - }, std: { stddev: 0.3755919319616704, stddevMinus2: 0.29658363607665916, diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.stories.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.stories.tsx index 15db20c63278..04ffdf326b16 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.stories.tsx +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.stories.tsx @@ -10,11 +10,59 @@ import React from "react"; import { storiesOf } from "@storybook/react"; +import { RenderFunction } from "storybook__react"; import { Latency } from "./index"; -import { latencyFixture, latencyFixtureNoLocality } from "./latency.fixtures"; +import { + latencyFixture, + latencyFixtureWithNodeStatuses, +} from "./latency.fixtures"; import { withRouterDecorator } from "src/util/decorators"; storiesOf("Latency Table", module) .addDecorator(withRouterDecorator) - .add("Default state", () => ) - .add("No localites state", () => ); + .addDecorator((storyFn: RenderFunction) => ( +
{storyFn()}
+ )) + .add("Healthy nodes with connection problems", () => ( + <> +

Network partitioned with healthy nodes

+
    +
  • + n1-n2, n1-n3 - failed connection +
  • +
  • + n2-n4, n2-n5 - not connected +
  • +
  • + n3-n1, n3-n2 - unknown status +
  • +
  • + n4-n1, n4-n2 - closed connection +
  • +
+ + + )) + .add("Unhealthy nodes", () => ( + <> +

Different node statuses

+
    +
  • + n1 - live node +
  • +
  • + n2 - dead node +
  • +
  • + n3 - unavailable node +
  • +
  • + n4 - decommissioned node +
  • +
  • + n5 - decommissioning node +
  • +
+ + + )); diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.styl b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.styl index d0c2b330753b..4ba35ae5ae95 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.styl +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/latency/latency.styl @@ -36,16 +36,16 @@ text-align center width 95px padding 9px 0 - a + a, div font-family SourceSansPro-SemiBold font-size 12px font-weight 600 line-height 1.67 letter-spacing 0.3px color $adminui-grey-2 - &:hover - color $colors--primary-blue-3 - text-decoration underline + a:hover + color $colors--primary-blue-3 + text-decoration underline &:first-child border-right 2px solid $colors--neutral-2 .Chip @@ -98,7 +98,7 @@ letter-spacing 0.1px color $table-border-color .Chip--tooltip__latency - margin-top 25px + margin-bottom 4px .Chip--tooltip__nodes display flex justify-content space-between @@ -120,6 +120,8 @@ margin 0 &--item-title line-height 1.71 + &--header + border-bottom solid #3b4a60 1px .color &--green, &--lightgreen color $colors--primary-green-2 @@ -129,3 +131,5 @@ color $colors--primary-blue-2 &--yellow color $colors--functional-yellow-3 + &--red + color $colors--functional-red-3 diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/legend/index.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/legend/index.tsx index a7d5ab663a3e..154171e16286 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/legend/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/legend/index.tsx @@ -8,14 +8,11 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { Divider, Tooltip } from "antd"; +import { Tooltip } from "antd"; import "antd/lib/divider/style"; import "antd/lib/tooltip/style"; import { Chip } from "src/views/app/components/chip"; -import Modal from "src/views/app/components/modal"; -import { getDisplayName } from "src/redux/nodes"; import React from "react"; -import { NoConnection } from ".."; import "./legend.styl"; import { Text, TextTypes } from "src/components"; @@ -25,7 +22,6 @@ interface ILegendProps { mean: number; stddevPlus1: number; stddevPlus2: number; - noConnections: NoConnection[]; } export const Legend: React.SFC = ({ @@ -34,7 +30,6 @@ export const Legend: React.SFC = ({ mean, stddevPlus1, stddevPlus2, - noConnections, }) => (
@@ -106,69 +101,5 @@ export const Legend: React.SFC = ({
- -
-
- - {`No Connections (${noConnections.length})`} - - ) - } - triggerStyle="Legend--container__head--title color--link" - triggerTitle={`No Connections (${noConnections.length})`} - > - - - - - - - - {noConnections.map(value => ( - - - - - - - ))} -
From NodeFrom LocalityTo NodeTo Locality
- - {getDisplayName(value)} - - - {value.from.address} - - - - {value.from.locality} - - - - {getDisplayName(value)} - - - {value.to.address} - - - - {value.to.locality} - -
-
-
-
- -
-
); diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/sort/index.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/sort/index.tsx index 7ef3bd6880e1..1aca11cba415 100644 --- a/pkg/ui/workspaces/db-console/src/views/reports/containers/network/sort/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/network/sort/index.tsx @@ -24,14 +24,18 @@ import "./sort.styl"; interface ISortProps { onChangeFilter: (key: string, value: string) => void; onChangeCollapse: (checked: boolean) => void; + onShowAllNodesChange: (checked: boolean) => void; deselectFilterByKey: (key: string) => void; collapsed: boolean; sort: NetworkSort[]; filter: NetworkFilter; + showDeadNodes: boolean; } class Sort extends React.Component { onChange = ({ target }: any) => this.props.onChangeCollapse(target.checked); + onShowAllNodesChange = ({ target }: any) => + this.props.onShowAllNodesChange(target.checked); pageView = () => { const { match } = this.props; @@ -42,20 +46,8 @@ class Sort extends React.Component { navigateTo = (selected: DropdownOption) => { trackNetworkSort(selected.label); this.props.onChangeCollapse(false); - this.props.history.push(`/reports/network/${selected.value}`); - }; - - componentDidMount() { - this.setDefaultSortValue("region"); - } - - setDefaultSortValue = (sortValue: string) => { - const isDefaultValuePresent = this.getSortValues(this.props.sort).find( - e => e.value === sortValue, - ); - if (isDefaultValuePresent) { - this.navigateTo(isDefaultValuePresent); - } + this.props.location.pathname = `/reports/network/${selected.value}`; + this.props.history.push(this.props.location); }; getSortValues = (sort: NetworkSort[]) => @@ -74,6 +66,7 @@ class Sort extends React.Component { deselectFilterByKey, filter, match, + showDeadNodes, } = this.props; const nodeId = getMatchParamByName(match, "node_id"); return ( @@ -100,6 +93,13 @@ class Sort extends React.Component { > Collapse Nodes + + + Show dead nodes + ); } diff --git a/pkg/ui/workspaces/db-console/styl/base/palette.styl b/pkg/ui/workspaces/db-console/styl/base/palette.styl index a727eba33ecc..0c93af3b47d3 100644 --- a/pkg/ui/workspaces/db-console/styl/base/palette.styl +++ b/pkg/ui/workspaces/db-console/styl/base/palette.styl @@ -74,3 +74,4 @@ $chip-grey = $colors--neutral-1 $chip-blue = #74bdfd80 // $colors--primary-blue-2 + 0.5 opacity $chip-lightblue = $colors--primary-blue-1 $chip-yellow = $colors--functional-orange-1 +$chip-red = $colors--functional-red-1