Skip to content

Commit

Permalink
ui: refactor txn details page to use RequestState typed prop
Browse files Browse the repository at this point in the history
This commit Replaces the request state props (isDataValid, statements,
transaction, error, isLoading) in the txn details page with a single
prop containing the request state.

The logic that was previously in the selectTransaction (and being
duplicated in db-console and CC) has been moved to a new file,
`transactionDetailsUtils` with added unit testing.

Epic: none

Release note: None
  • Loading branch information
xinhaoz committed May 8, 2023
1 parent cbe078a commit 43b9763
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 169 deletions.
10 changes: 8 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/api/testUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Long from "long";

export type Stmt =
cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics;
type Txn =
export type Txn =
cockroach.server.serverpb.StatementsResponse.IExtendedCollectedTransactionStatistics;
type ILatencyInfo = cockroach.sql.ILatencyInfo;

Expand All @@ -27,6 +27,7 @@ const latencyInfo: Required<ILatencyInfo> = {

const baseStmt: Partial<Stmt> = {
id: Long.fromInt(11871906682067483964),
txn_fingerprint_ids: [Long.fromInt(1)],
key: {
key_data: {
query: "SELECT node_id FROM system.statement_statistics",
Expand Down Expand Up @@ -193,13 +194,18 @@ const assignObjectPropsIfExists = <T extends { [key: string]: unknown }>(
overrides: Partial<T>,
): T => {
const copiedObj: T = { ...baseObj };

for (const prop in baseObj) {
if (overrides[prop] === undefined) {
continue;
}

const val = copiedObj[prop];
if (typeof val === "object") {
if (
typeof val === "object" &&
!Array.isArray(val) &&
overrides[prop] != null
) {
copiedObj[prop] = assignObjectPropsIfExists(
val as Record<string, unknown>,
overrides[prop] as Record<string, unknown>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ import { baseHeadingClasses } from "../transactionsPage/transactionsPageClasses"
import { Button } from "../button";
import { tableClasses } from "../transactionsTable/transactionsTableClasses";
import { SqlBox } from "../sql";
import {
aggregateStatements,
statementFingerprintIdsToText,
} from "../transactionsPage/utils";
import { aggregateStatements } from "../transactionsPage/utils";
import { Loading } from "../loading";
import { SummaryCard, SummaryCardItem } from "../summaryCard";
import {
Expand Down Expand Up @@ -63,6 +60,8 @@ import Long from "long";
import {
createCombinedStmtsRequest,
InsightRecommendation,
RequestState,
SqlStatsResponse,
StatementsRequest,
TxnInsightsRequest,
} from "../api";
Expand All @@ -79,7 +78,6 @@ import {
timeScaleRangeToObj,
toRoundedDateRange,
} from "../timeScaleDropdown";
import moment from "moment-timezone";

import timeScaleStyles from "../timeScaleDropdown/timeScale.module.scss";
import insightTableStyles from "../insightsTable/insightsTable.module.scss";
Expand All @@ -90,6 +88,11 @@ import {
import { CockroachCloudContext } from "../contexts";
import { SqlStatsSortType } from "src/api/statementsApi";
import { FormattedTimescale } from "../timeScaleDropdown/formattedTimeScale";
import {
getStatementsForTransaction,
getTxnFromSqlStatsMemoized,
getTxnQueryString,
} from "./transactionDetailsUtils";
const { containerClass } = tableClasses;
const cx = classNames.bind(statementsStyles);
const timeScaleStylesCx = classNames.bind(timeScaleStyles);
Expand All @@ -105,18 +108,13 @@ export interface TransactionDetailsStateProps {
timeScale: TimeScale;
limit: number;
reqSortSetting: SqlStatsSortType;
error?: Error | null;
isTenant: UIConfigState["isTenant"];
hasViewActivityRedactedRole?: UIConfigState["hasViewActivityRedactedRole"];
nodeRegions: { [nodeId: string]: string };
statements?: Statement[];
transaction: Transaction;
transactionFingerprintId: string;
isLoading: boolean;
lastUpdated: moment.Moment | null;
transactionInsights: TxnInsightEvent[];
hasAdminRole?: UIConfigState["hasAdminRole"];
isDataValid: boolean;
txnStatsResp: RequestState<SqlStatsResponse>;
}

export interface TransactionDetailsDispatchProps {
Expand All @@ -135,6 +133,8 @@ interface TState {
sortSetting: SortSetting;
pagination: ISortedTablePagination;
latestTransactionText: string;
txnDetails: Transaction | null;
statements: Statement[] | null;
}

function statementsRequestFromProps(
Expand All @@ -155,6 +155,18 @@ export class TransactionDetails extends React.Component<
> {
constructor(props: TransactionDetailsProps) {
super(props);

const txnDetails = getTxnFromSqlStatsMemoized(
this.props.txnStatsResp?.data,
this.props.match,
this.props.location,
);

const stmts = getStatementsForTransaction(
txnDetails,
this.props.txnStatsResp?.data?.statements,
);

this.state = {
sortSetting: {
// Sort by statement latency as default column.
Expand All @@ -165,7 +177,9 @@ export class TransactionDetails extends React.Component<
pageSize: 10,
current: 1,
},
latestTransactionText: this.getTxnQueryString(),
latestTransactionText: getTxnQueryString(txnDetails, stmts),
txnDetails,
statements: stmts,
};

// In case the user selected a option not available on this page,
Expand All @@ -177,33 +191,33 @@ export class TransactionDetails extends React.Component<
}
}

getTxnQueryString = (): string => {
const { transaction } = this.props;

const statementFingerprintIds =
transaction?.stats_data?.statement_fingerprint_ids;
setTxnDetails = (): void => {
const txnDetails = getTxnFromSqlStatsMemoized(
this.props.txnStatsResp?.data,
this.props.match,
this.props.location,
);

return (
(statementFingerprintIds &&
statementFingerprintIdsToText(
statementFingerprintIds,
this.getStatementsForTransaction(),
)) ??
""
const statements = getStatementsForTransaction(
txnDetails,
this.props.txnStatsResp?.data?.statements,
);
};

setTxnQueryString = (): void => {
const transactionText = this.getTxnQueryString();
// Only overwrite the transaction text if it is non-null.
const transactionText =
getTxnQueryString(txnDetails, statements) ??
this.state.latestTransactionText;

// If a new, non-empty-string transaction text is available (derived from the time-frame-specific endpoint
// response), cache the text.
if (
transactionText &&
transactionText !== this.state.latestTransactionText
transactionText !== this.state.latestTransactionText ||
txnDetails !== this.state.txnDetails
) {
this.setState({
latestTransactionText: transactionText,
txnDetails,
statements,
});
}
};
Expand All @@ -222,7 +236,7 @@ export class TransactionDetails extends React.Component<
};

componentDidMount(): void {
if (!this.props.transaction || !this.props.isDataValid) {
if (!this.props.txnStatsResp?.data || !this.props.txnStatsResp?.valid) {
this.refreshData();
}
this.props.refreshUserSQLRoles();
Expand All @@ -239,9 +253,9 @@ export class TransactionDetails extends React.Component<
if (
prevProps.transactionFingerprintId !==
this.props.transactionFingerprintId ||
prevProps.statements !== this.props.statements
prevProps.txnStatsResp !== this.props.txnStatsResp
) {
this.setTxnQueryString();
this.setTxnDetails();
}

if (this.props.timeScale !== prevProps.timeScale) {
Expand All @@ -265,28 +279,11 @@ export class TransactionDetails extends React.Component<
this.props.history.push("/sql-activity?tab=Transactions&view=fingerprints");
};

getStatementsForTransaction = (): Statement[] => {
const { transaction, statements } = this.props;

const statementFingerprintIds =
transaction?.stats_data?.statement_fingerprint_ids;
if (!statementFingerprintIds) {
return [];
}

return (
statements?.filter(
s =>
s.key.key_data.transaction_fingerprint_id.toString() ===
this.props.transactionFingerprintId,
) ?? []
);
};

render(): React.ReactElement {
const { error, nodeRegions, transaction } = this.props;
const { latestTransactionText } = this.state;
const statementsForTransaction = this.getStatementsForTransaction();
const { nodeRegions } = this.props;
const transaction = this.state.txnDetails;
const error = this.props.txnStatsResp?.error;
const { latestTransactionText, statements } = this.state;
const transactionStats = transaction?.stats_data?.stats;
const period = <FormattedTimescale ts={this.props.timeScale} />;

Expand Down Expand Up @@ -324,7 +321,7 @@ export class TransactionDetails extends React.Component<
<Loading
error={error}
page={"transaction details"}
loading={this.props.isLoading}
loading={this.props.txnStatsResp?.inFlight}
render={() => {
if (!transaction) {
return (
Expand Down Expand Up @@ -358,9 +355,7 @@ export class TransactionDetails extends React.Component<
} = this.props;
const { sortSetting, pagination } = this.state;

const aggregatedStatements = aggregateStatements(
statementsForTransaction,
);
const aggregatedStatements = aggregateStatements(statements);
populateRegionNodeForStatements(aggregatedStatements, nodeRegions);
const duration = (v: number) => Duration(v * 1e9);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { createSelector } from "@reduxjs/toolkit";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { withRouter } from "react-router-dom";
import { Dispatch } from "redux";

import { AppState, uiConfigActions } from "src/store";
Expand Down Expand Up @@ -40,77 +39,26 @@ import {
selectTxnsPageReqSort,
} from "../store/utils/selectors";
import { StatementsRequest } from "src/api/statementsApi";
import {
txnFingerprintIdAttr,
getMatchParamByName,
queryByName,
appNamesAttr,
unset,
} from "../util";
import { txnFingerprintIdAttr, getMatchParamByName } from "../util";
import { TimeScale } from "../timeScaleDropdown";
import { actions as analyticsActions } from "../store/analytics";

export const selectTransaction = createSelector(
(state: AppState) => state.adminUI?.transactions,
(_state: AppState, props: RouteComponentProps) => props,
(transactionState, props) => {
const transactions = transactionState.data?.transactions;
if (!transactions) {
return {
isLoading: transactionState.inFlight,
transaction: null,
isValid: transactionState.valid,
};
}

const apps = queryByName(props.location, appNamesAttr)
?.split(",")
.map(s => s.trim());

const txnFingerprintId = getMatchParamByName(
props.match,
txnFingerprintIdAttr,
);

const transaction = transactions.find(
txn =>
txn.stats_data.transaction_fingerprint_id.toString() ===
txnFingerprintId &&
(apps?.length ? apps.includes(txn.stats_data.app ?? unset) : true),
);

return {
isLoading: transactionState.inFlight,
transaction: transaction,
lastUpdated: transactionState.lastUpdated,
isValid: transactionState.valid,
};
},
);

const mapStateToProps = (
state: AppState,
props: TransactionDetailsProps,
): TransactionDetailsStateProps => {
const { isLoading, transaction, lastUpdated, isValid } = selectTransaction(
state,
props,
);
return {
timeScale: selectTimeScale(state),
isTenant: selectIsTenant(state),
nodeRegions: nodeRegionsByIDSelector(state),
transaction,
txnStatsResp: state?.adminUI?.transactions,
transactionFingerprintId: getMatchParamByName(
props.match,
txnFingerprintIdAttr,
),
isLoading: isLoading,
lastUpdated: lastUpdated,
hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state),
transactionInsights: selectTxnInsightsByFingerprint(state, props),
hasAdminRole: selectHasAdminRole(state),
isDataValid: isValid,
limit: selectTxnsPageLimit(state),
reqSortSetting: selectTxnsPageReqSort(state),
};
Expand Down
Loading

0 comments on commit 43b9763

Please sign in to comment.