diff --git a/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts index 0be5bb0029f8..bbd7be686749 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts @@ -21,40 +21,20 @@ import { BlockedContentionDetails, InsightExecEnum, InsightNameEnum, - StatementInsightEvent, - TransactionInsightEvent, - TransactionInsightEventDetails, + FlattenedStmtInsightEvent, + TxnContentionInsightEvent, + TxnContentionInsightDetails, + TxnInsightEvent, + getInsightsFromProblemsAndCauses, + getInsightFromCause, } from "src/insights"; import moment from "moment"; +import { INTERNAL_APP_NAME_PREFIX } from "src/activeExecutions/activeStatementUtils"; -// Transaction insight events. +// Transaction contention insight events. -// There are three transaction contention event insight queries: -// 1. A query that selects transaction contention events from crdb_internal.transaction_contention_events. -// 2. A query that selects statement fingerprint IDS from crdb_internal.transaction_statistics, filtering on the -// fingerprint IDs recorded in the contention events. -// 3. A query that selects statement queries from crdb_internal.statement_statistics, filtering on the fingerprint IDs -// recorded in the contention event rows. -// After we get the results from these tables, we combine them on the frontend. - -// These types describe the final transaction contention event state, as it is stored in Redux. -export type TransactionInsightEventState = Omit< - TransactionInsightEvent, - "insights" -> & { - insightName: string; -}; -export type TransactionInsightEventsResponse = TransactionInsightEventState[]; - -export type TransactionContentionEventState = Omit< - TransactionInsightEventState, - "application" | "queries" ->; - -export type TransactionContentionEventsResponse = - TransactionContentionEventState[]; - -// txnContentionQuery selects all relevant transaction contention events. +// txnContentionQuery selects all transaction contention events that are +// above the insights latency threshold. const txnContentionQuery = ` SELECT * FROM ( @@ -92,34 +72,41 @@ type TransactionContentionResponseColumns = { threshold: string; }; -function transactionContentionResultsToEventState( +type TxnContentionEvent = Omit< + TxnContentionInsightEvent, + "application" | "queries" +>; + +function formatTxnContentionResults( response: SqlExecutionResponse, -): TransactionContentionEventsResponse { +): TxnContentionEvent[] { if (sqlResultsAreEmpty(response)) { - // No transaction contention events. return []; } return response.execution.txn_results[0].rows.map(row => ({ transactionID: row.waiting_txn_id, - fingerprintID: row.waiting_txn_fingerprint_id, + transactionFingerprintID: row.waiting_txn_fingerprint_id, startTime: moment(row.collection_ts).utc(), contentionDuration: moment.duration(row.contention_duration), contentionThreshold: moment.duration(row.threshold).asMilliseconds(), insightName: InsightNameEnum.highContention, execType: InsightExecEnum.TRANSACTION, + insights: [ + getInsightFromCause( + InsightNameEnum.highContention, + InsightExecEnum.TRANSACTION, + ), + ], })); } -export type TxnStmtFingerprintEventState = Pick< - TransactionInsightEventState, - "application" | "fingerprintID" -> & { - queryIDs: string[]; +export type TxnWithStmtFingerprints = { + application: string; + transactionFingerprintID: string; + queryIDs: string[]; // Statement fingerprint IDs. }; -export type TxnStmtFingerprintEventsResponse = TxnStmtFingerprintEventState[]; - type TxnStmtFingerprintsResponseColumns = { transaction_fingerprint_id: string; query_ids: string[]; // Statement Fingerprint IDs. @@ -138,16 +125,15 @@ WHERE app_name != '${INTERNAL_SQL_API_APP}' .map(id => `'${id}'`) .join(",")} ]`; -function txnStmtFingerprintsResultsToEventState( +function formatTxnFingerprintsResults( response: SqlExecutionResponse, -): TxnStmtFingerprintEventsResponse { +): TxnWithStmtFingerprints[] { if (sqlResultsAreEmpty(response)) { - // No transaction fingerprint results. return []; } return response.execution.txn_results[0].rows.map(row => ({ - fingerprintID: row.transaction_fingerprint_id, + transactionFingerprintID: row.transaction_fingerprint_id, queryIDs: row.query_ids, application: row.app_name, })); @@ -173,7 +159,7 @@ WHERE encode(fingerprint_id, 'hex') = ANY ARRAY[ ${stmt_fingerprint_ids .map(id => `'${id}'`) .join(",")} ]`; -function fingerprintStmtsResultsToEventState( +function createStmtFingerprintToQueryMap( response: SqlExecutionResponse, ): StmtFingerprintToQueryRecord { const idToQuery: Map = new Map(); @@ -195,8 +181,14 @@ const makeInsightsSqlRequest = (queries: string[]): SqlExecutionRequest => ({ timeout: LONG_TIMEOUT, }); -// getTransactionInsightEventState is the API function that executes the queries and returns the results. -export async function getTransactionInsightEventState(): Promise { +/** + * getTxnInsightEvents is the API function that executes the queries to collect + * txn contention insights and the query strings of txns involved in the contention. + * @returns a list of txn contention insights + */ +export async function getTxnInsightEvents(): Promise< + TxnContentionInsightEvent[] +> { // Note that any errors encountered fetching these results are caught // earlier in the call stack. @@ -209,11 +201,11 @@ export async function getTransactionInsightEventState(): Promise(); - contentionResults.execution.txn_results[0].rows.forEach(row => - txnFingerprintIDs.add(row.waiting_txn_fingerprint_id), + const txnFingerprintIDs = new Set( + contentionEvents.map(e => e.transactionFingerprintID), ); const txnStmtFingerprintResults = @@ -225,11 +217,14 @@ export async function getTransactionInsightEventState(): Promise(); - txnStmtFingerprintResults.execution.txn_results[0].rows.forEach(row => { - row.query_ids.forEach(id => stmtFingerprintIDs.add(id)); + txnsWithStmtFingerprints.forEach(txn => { + txn.queryIDs.forEach(id => stmtFingerprintIDs.add(id)); }); const fingerprintStmtsRequest = makeInsightsSqlRequest([ fingerprintStmtsQuery(Array.from(stmtFingerprintIDs)), @@ -239,18 +234,18 @@ export async function getTransactionInsightEventState(): Promise ({ - fingerprintID: txnRow.fingerprintID, + fingerprintID: txnRow.transactionFingerprintID, appName: txnRow.application, queries: txnRow.queryIDs.map(stmtID => fingerprintToQuery.get(stmtID)), })); - const res = txnContentionState.map(row => { - const qa = txnsWithStmtQueries.find( - query => query.fingerprintID === row.fingerprintID, - ); - if (qa) { + const res = txnContentionState + .map(txnContention => { + const txnQueries = txnsWithStmtQueries.find( + txn => txn.fingerprintID === txnContention.transactionFingerprintID, + ); + if (!txnQueries) { + return null; + } return { - ...row, - queries: qa.queries, - application: qa.appName, + ...txnContention, + queries: txnQueries.queries, + application: txnQueries.appName, insightName: InsightNameEnum.highContention, execType: InsightExecEnum.TRANSACTION, }; - } - }); + }) + .filter(txn => txn); // remove null entries return res; } @@ -289,27 +288,14 @@ export function combineTransactionInsightEventState( // 2. Reuse the queries/types defined above to get the waiting and blocking queries. // After we get the results from these tables, we combine them on the frontend. -// These types describe the final event state, as it is stored in Redux -export type TransactionInsightEventDetailsState = Omit< - TransactionInsightEventDetails, - "insights" -> & { - insightName: string; -}; -export type TransactionInsightEventDetailsResponse = - TransactionInsightEventDetailsState; - -export type TransactionInsightEventDetailsRequest = { id: string }; +export type TxnContentionInsightDetailsRequest = { id: string }; // Query 1 types, functions. -export type TransactionContentionEventDetailsState = Omit< - TransactionInsightEventDetailsState, +export type TransactionContentionEventDetails = Omit< + TxnContentionInsightDetails, "application" | "queries" | "blockingQueries" >; -export type TransactionContentionEventDetailsResponse = - TransactionContentionEventDetailsState; - // txnContentionDetailsQuery selects information about a specific transaction contention event. const txnContentionDetailsQuery = (id: string) => ` SELECT @@ -351,9 +337,14 @@ type TxnContentionDetailsResponseColumns = { key: string; }; -function transactionContentionDetailsResultsToEventState( +type PartialTxnContentionDetails = Omit< + TxnContentionInsightDetails, + "application" | "queries" +>; + +function formatTxnContentionDetailsResponse( response: SqlExecutionResponse, -): TransactionContentionEventDetailsResponse { +): PartialTxnContentionDetails { const resultsRows = response.execution.txn_results[0].rows; if (!resultsRows) { // No data. @@ -372,7 +363,7 @@ function transactionContentionDetailsResultsToEventState( totalContentionTime += contentionTimeInMs; blockingContentionDetails[idx] = { blockingExecutionID: value.blocking_txn_id, - blockingFingerprintID: value.blocking_txn_fingerprint_id, + blockingTxnFingerprintID: value.blocking_txn_fingerprint_id, blockingQueries: null, collectionTimeStamp: moment(value.collection_ts).utc(), contentionTimeMs: contentionTimeInMs, @@ -388,23 +379,33 @@ function transactionContentionDetailsResultsToEventState( }); const row = resultsRows[0]; + const contentionThreshold = moment.duration(row.threshold).asMilliseconds(); return { - executionID: row.waiting_txn_id, - fingerprintID: row.waiting_txn_fingerprint_id, + transactionExecutionID: row.waiting_txn_id, + transactionFingerprintID: row.waiting_txn_fingerprint_id, startTime: moment(row.collection_ts).utc(), - totalContentionTime: totalContentionTime, + totalContentionTimeMs: totalContentionTime, blockingContentionDetails: blockingContentionDetails, - contentionThreshold: moment.duration(row.threshold).asMilliseconds(), + contentionThreshold, insightName: InsightNameEnum.highContention, execType: InsightExecEnum.TRANSACTION, + insights: [ + getInsightFromCause( + InsightNameEnum.highContention, + InsightExecEnum.TRANSACTION, + contentionThreshold, + totalContentionTime, + ), + ], }; } // getTransactionInsightEventState is the API function that executes the queries and returns the results. export async function getTransactionInsightEventDetailsState( - req: TransactionInsightEventDetailsRequest, -): Promise { - // Note that any errors encountered fetching these results are caught // earlier in the call stack. + req: TxnContentionInsightDetailsRequest, +): Promise { + // Note that any errors encountered fetching these results are caught + // earlier in the call stack. // // There are 3 api requests/queries in this process. // 1. Get contention insight for the requested transaction. @@ -412,44 +413,38 @@ export async function getTransactionInsightEventDetailsState( // 3. Get the query strings for ALL statements involved in the transaction. // Get contention results for requested transaction. - const txnContentionDetailsRequest = makeInsightsSqlRequest([ - txnContentionDetailsQuery(req.id), - ]); const contentionResults = await executeInternalSql( - txnContentionDetailsRequest, + makeInsightsSqlRequest([txnContentionDetailsQuery(req.id)]), ); if (sqlResultsAreEmpty(contentionResults)) { return; } + const contentionDetails = + formatTxnContentionDetailsResponse(contentionResults); // Collect all txn fingerprints involved. const txnFingerprintIDs: string[] = []; - contentionResults.execution.txn_results.forEach(txnResult => - txnResult.rows.forEach(x => - txnFingerprintIDs.push(x.blocking_txn_fingerprint_id), - ), + contentionDetails.blockingContentionDetails.forEach(x => + txnFingerprintIDs.push(x.blockingTxnFingerprintID), ); // Add the waiting txn fingerprint ID. - txnFingerprintIDs.push( - contentionResults.execution.txn_results[0].rows[0] - .waiting_txn_fingerprint_id, - ); + txnFingerprintIDs.push(contentionDetails.transactionFingerprintID); // Collect all stmt fingerprint ids involved. const getStmtFingerprintsResponse = await executeInternalSql( makeInsightsSqlRequest([txnStmtFingerprintsQuery(txnFingerprintIDs)]), ); + const txnsWithStmtFingerprints = formatTxnFingerprintsResults( + getStmtFingerprintsResponse, + ); const stmtFingerprintIDs = new Set(); - getStmtFingerprintsResponse.execution.txn_results[0].rows.forEach( - txnFingerprint => - txnFingerprint.query_ids.forEach(id => stmtFingerprintIDs.add(id)), + txnsWithStmtFingerprints.forEach(txnFingerprint => + txnFingerprint.queryIDs.forEach(id => stmtFingerprintIDs.add(id)), ); - console.log("ok"); - console.log(txnFingerprintIDs); - console.log(stmtFingerprintIDs); + const stmtQueriesResponse = await executeInternalSql( makeInsightsSqlRequest([ @@ -457,29 +452,30 @@ export async function getTransactionInsightEventDetailsState( ]), ); - return combineTransactionInsightEventDetailsState( - transactionContentionDetailsResultsToEventState(contentionResults), - txnStmtFingerprintsResultsToEventState(getStmtFingerprintsResponse), - fingerprintStmtsResultsToEventState(stmtQueriesResponse), + return buildTxnContentionInsightDetails( + contentionDetails, + txnsWithStmtFingerprints, + createStmtFingerprintToQueryMap(stmtQueriesResponse), ); } -export function combineTransactionInsightEventDetailsState( - txnContentionDetailsState: TransactionContentionEventDetailsResponse, - txnsWithStmtFingerprints: TxnStmtFingerprintEventsResponse, +function buildTxnContentionInsightDetails( + partialTxnContentionDetails: PartialTxnContentionDetails, + txnsWithStmtFingerprints: TxnWithStmtFingerprints[], stmtFingerprintToQuery: StmtFingerprintToQueryRecord, -): TransactionInsightEventDetailsState { +): TxnContentionInsightDetails { if ( - !txnContentionDetailsState && + !partialTxnContentionDetails && !txnsWithStmtFingerprints.length && !stmtFingerprintToQuery.size ) { return null; } - txnContentionDetailsState.blockingContentionDetails.forEach(blockedRow => { + partialTxnContentionDetails.blockingContentionDetails.forEach(blockedRow => { const currBlockedFingerprintStmts = txnsWithStmtFingerprints.find( - txn => txn.fingerprintID === blockedRow.blockingFingerprintID, + txn => + txn.transactionFingerprintID === blockedRow.blockingTxnFingerprintID, ); if (!currBlockedFingerprintStmts) { @@ -492,11 +488,13 @@ export function combineTransactionInsightEventDetailsState( }); const waitingTxn = txnsWithStmtFingerprints.find( - txn => txn.fingerprintID === txnContentionDetailsState.fingerprintID, + txn => + txn.transactionFingerprintID === + partialTxnContentionDetails.transactionFingerprintID, ); const res = { - ...txnContentionDetailsState, + ...partialTxnContentionDetails, application: waitingTxn.application, queries: waitingTxn.queryIDs.map(id => stmtFingerprintToQuery.get(id)), }; @@ -504,8 +502,6 @@ export function combineTransactionInsightEventDetailsState( return res; } -// Statements - type ExecutionInsightsResponseRow = { session_id: string; txn_id: string; @@ -533,47 +529,99 @@ type ExecutionInsightsResponseRow = { plan_gist: string; }; -export type StatementInsights = StatementInsightEvent[]; +export type FlattenedStmtInsights = FlattenedStmtInsightEvent[]; -function getStatementInsightsFromClusterExecutionInsightsResponse( +// This function collects and groups rows of execution insights into +// a list of transaction insights, which contain any statement insights +// that were returned in the response. +function organizeExecutionInsightsResponseIntoTxns( response: SqlExecutionResponse, -): StatementInsights { +): TxnInsightEvent[] { if (!response.execution.txn_results[0].rows) { // No data. return []; } - return response.execution.txn_results[0].rows.map(row => { + const txnByExecID = new Map(); + + response.execution.txn_results[0].rows.forEach(row => { + let txnInsight: TxnInsightEvent = txnByExecID.get(row.txn_id); + + if (!txnInsight) { + txnInsight = { + transactionExecutionID: row.txn_id, + transactionFingerprintID: row.txn_fingerprint_id, + implicitTxn: row.implicit_txn, + databaseName: row.database_name, + application: row.app_name, + username: row.user_name, + sessionID: row.session_id, + priority: row.priority, + retries: row.retries, + lastRetryReason: row.last_retry_reason, + statementInsights: [], + insights: [], + queries: [], + }; + txnByExecID.set(row.txn_id, txnInsight); + } + const start = moment.utc(row.start_time); const end = moment.utc(row.end_time); - return { - transactionID: row.txn_id, - transactionFingerprintID: row.txn_fingerprint_id, - implicitTxn: row.implicit_txn, + const stmtInsight = { query: row.query, startTime: start, endTime: end, - databaseName: row.database_name, elapsedTimeMillis: end.diff(start, "milliseconds"), - application: row.app_name, - username: row.user_name, - statementID: row.stmt_id, + statementExecutionID: row.stmt_id, statementFingerprintID: row.stmt_fingerprint_id, - sessionID: row.session_id, isFullScan: row.full_scan, rowsRead: row.rows_read, rowsWritten: row.rows_written, - priority: row.priority, - retries: row.retries, - lastRetryReason: row.last_retry_reason, timeSpentWaiting: row.contention ? moment.duration(row.contention) : null, causes: row.causes, problem: row.problem, indexRecommendations: row.index_recommendations, - insights: null, + insights: getInsightsFromProblemsAndCauses( + row.problem, + row.causes, + InsightExecEnum.STATEMENT, + ), planGist: row.plan_gist, }; + + txnInsight.queries.push(stmtInsight.query); + txnInsight.statementInsights.push(stmtInsight); + + // Bubble up stmt insights to txn level. + txnInsight.insights = txnInsight.insights.concat( + getInsightsFromProblemsAndCauses( + row.problem, + row.causes, + InsightExecEnum.TRANSACTION, + ), + ); + }); + + txnByExecID.forEach(txn => { + // De-duplicate top-level txn insights. + const insightsSeen = new Set(); + txn.insights = txn.insights.reduce((insights, i) => { + if (insightsSeen.has(i.name)) return insights; + insightsSeen.add(i.name); + insights.push(i); + return insights; + }, []); + + // Sort stmt insights for each txn by start time. + txn.statementInsights.sort((a, b) => { + if (a.startTime.isBefore(b.startTime)) return -1; + else if (a.startTime.isAfter(b.startTime)) return 1; + return 0; + }); }); + + return Array.from(txnByExecID.values()); } type InsightQuery = { @@ -582,53 +630,59 @@ type InsightQuery = { toState: (response: SqlExecutionResponse) => State; }; -const statementInsightsQuery: InsightQuery< +const workloadInsightsQuery: InsightQuery< ExecutionInsightsResponseRow, - StatementInsights + TxnInsightEvent[] > = { name: InsightNameEnum.highContention, // We only surface the most recently observed problem for a given statement. - query: `SELECT *, prettify_statement(non_prettified_query, 108, 1, 1) AS query from ( + // Note that we don't filter by problem != 'None', so that we can get all + // stmts in the problematic transaction. + query: ` +SELECT + session_id, + insights.txn_id as txn_id, + encode(txn_fingerprint_id, 'hex') AS txn_fingerprint_id, + implicit_txn, + stmt_id, + encode(stmt_fingerprint_id, 'hex') AS stmt_fingerprint_id, + prettify_statement(query, 108, 1, 1) AS query, + start_time, + end_time, + full_scan, + app_name, + database_name, + user_name, + rows_read, + rows_written, + priority, + retries, + contention, + last_retry_reason, + index_recommendations, + problem, + causes, + plan_gist +FROM + ( SELECT - session_id, - txn_id, - encode(txn_fingerprint_id, 'hex') AS txn_fingerprint_id, - implicit_txn, - stmt_id, - encode(stmt_fingerprint_id, 'hex') AS stmt_fingerprint_id, - query AS non_prettified_query, - start_time, - end_time, - full_scan, - app_name, - database_name, - user_name, - rows_read, - rows_written, - priority, - retries, - contention, - last_retry_reason, - index_recommendations, - problem, - causes, - plan_gist, - row_number() OVER ( - PARTITION BY txn_fingerprint_id - ORDER BY end_time DESC - ) AS rank + txn_id, + row_number() OVER ( PARTITION BY txn_fingerprint_id ORDER BY end_time ) as rank FROM crdb_internal.cluster_execution_insights - WHERE problem != 'None' AND app_name != '${INTERNAL_SQL_API_APP}' - ) WHERE rank = 1 - `, - toState: getStatementInsightsFromClusterExecutionInsightsResponse, + ) as latestTxns +JOIN crdb_internal.cluster_execution_insights AS insights +ON latestTxns.txn_id = insights.txn_id +WHERE latestTxns.rank = 1 AND app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%' + `, + toState: organizeExecutionInsightsResponseIntoTxns, }; -export function getStatementInsightsApi(): Promise { +export type ExecutionInsights = TxnInsightEvent[]; +export function getClusterInsightsApi(): Promise { const request: SqlExecutionRequest = { statements: [ { - sql: statementInsightsQuery.query, + sql: workloadInsightsQuery.query, }, ], execute: true, @@ -637,7 +691,7 @@ export function getStatementInsightsApi(): Promise { }; return executeInternalSql(request).then( result => { - return statementInsightsQuery.toState(result); + return workloadInsightsQuery.toState(result); }, ); } diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/types.ts b/pkg/ui/workspaces/cluster-ui/src/insights/types.ts index a1b8add48b3d..1d5b510fc3e3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/types.ts +++ b/pkg/ui/workspaces/cluster-ui/src/insights/types.ts @@ -26,9 +26,12 @@ export enum InsightExecEnum { STATEMENT = "statement", } -export type TransactionInsightEvent = { +// What we store in redux for txn contention insight events in +// the overview page. It is missing information such as the +// blocking txn information. +export type TxnContentionInsightEvent = { transactionID: string; - fingerprintID: string; + transactionFingerprintID: string; queries: string[]; insights: Insight[]; startTime: Moment; @@ -38,10 +41,11 @@ export type TransactionInsightEvent = { execType: InsightExecEnum; }; +// Information about the blocking transaction and schema. export type BlockedContentionDetails = { collectionTimeStamp: Moment; blockingExecutionID: string; - blockingFingerprintID: string; + blockingTxnFingerprintID: string; blockingQueries: string[]; contendedKey: string; schemaName: string; @@ -51,48 +55,100 @@ export type BlockedContentionDetails = { contentionTimeMs: number; }; -export type TransactionInsightEventDetails = { - executionID: string; +// TODO (xinhaoz) these fields should be placed into TxnInsightEvent +// once they are available for contention insights. MergedTxnInsightEvent, +// (which marks these fields as optional) can then be deleted. +type UnavailableForTxnContention = { + databaseName: string; + username: string; + priority: string; + retries: number; + implicitTxn: boolean; + sessionID: string; +}; + +export type TxnInsightEvent = UnavailableForTxnContention & { + transactionExecutionID: string; + transactionFingerprintID: string; + application: string; + lastRetryReason?: string; + contention?: moment.Duration; + + // The full list of statements in this txn, with their stmt + // level insights (not all stmts in the txn may have one). + // Ordered by startTime. + statementInsights: StatementInsightEvent[]; + + insights: Insight[]; // De-duplicated list of insights from statement level. + queries: string[]; // We bubble this up from statementinsights for easy access, since txn contention details dont have stmt insights. + startTime?: Moment; // TODO (xinhaoz) not currently available + elapsedTimeMillis?: number; // TODO (xinhaoz) not currently available + endTime?: Moment; // TODO (xinhaoz) not currently available +}; + +export type MergedTxnInsightEvent = Omit< + TxnInsightEvent, + keyof UnavailableForTxnContention +> & + Partial; + +export type TxnContentionInsightDetails = { + transactionExecutionID: string; queries: string[]; insights: Insight[]; startTime: Moment; - totalContentionTime: number; + totalContentionTimeMs: number; contentionThreshold: number; application: string; - fingerprintID: string; + transactionFingerprintID: string; blockingContentionDetails: BlockedContentionDetails[]; execType: InsightExecEnum; + insightName: string; }; +export type TxnInsightDetails = Omit & { + totalContentionTimeMs?: number; + contentionThreshold?: number; + blockingContentionDetails?: BlockedContentionDetails[]; + execType: InsightExecEnum; +}; + +// Does not contain transaction information. +// This is what is stored at the transaction insight level, shown +// on the txn insights overview page. export type StatementInsightEvent = { - // Some of these can be moved to a common InsightEvent type if txn query is updated. - statementID: string; - transactionID: string; + statementExecutionID: string; statementFingerprintID: string; - transactionFingerprintID: string; - implicitTxn: boolean; startTime: Moment; + isFullScan: boolean; elapsedTimeMillis: number; - sessionID: string; timeSpentWaiting?: moment.Duration; - isFullScan: boolean; endTime: Moment; - databaseName: string; - username: string; rowsRead: number; rowsWritten: number; - lastRetryReason?: string; - priority: string; - retries: number; causes: string[]; problem: string; query: string; - application: string; insights: Insight[]; indexRecommendations: string[]; planGist: string; }; +// StatementInsightEvent with their transaction level information. +// What we show in the stmt insights overview and details pages. +export type FlattenedStmtInsightEvent = StatementInsightEvent & { + transactionExecutionID: string; + transactionFingerprintID: string; + implicitTxn: boolean; + sessionID: string; + databaseName: string; + username: string; + lastRetryReason?: string; + priority: string; + retries: number; + application: string; +}; + export type Insight = { name: InsightNameEnum; label: string; @@ -100,7 +156,7 @@ export type Insight = { tooltipDescription: string; }; -export type EventExecution = { +export type ContentionEvent = { executionID: string; fingerprintID: string; queries: string[]; @@ -114,7 +170,7 @@ export type EventExecution = { }; const highContentionInsight = ( - execType: InsightExecEnum = InsightExecEnum.TRANSACTION, + execType: InsightExecEnum, latencyThreshold?: number, contentionDuration?: number, ): Insight => { @@ -213,9 +269,7 @@ const failedExecutionInsight = (execType: InsightExecEnum): Insight => { }; }; -export const InsightTypes = [highContentionInsight]; // only used by getTransactionInsights to iterate over txn insights - -export const getInsightFromProblem = ( +export const getInsightFromCause = ( cause: string, execOption: InsightExecEnum, latencyThreshold?: number, @@ -270,7 +324,7 @@ export interface InsightRecommendation { database?: string; query?: string; indexDetails?: indexDetails; - execution?: executionDetails; + execution?: ExecutionDetails; details?: insightDetails; } @@ -281,13 +335,17 @@ export interface indexDetails { lastUsed?: string; } -export interface executionDetails { - statement?: string; - summary?: string; +// These are the fields used for workload insight recommendations. +export interface ExecutionDetails { + databaseName?: string; + elapsedTimeMillis?: number; + contentionTime?: number; fingerprintID?: string; implicit?: boolean; - retries?: number; indexRecommendations?: string[]; + retries?: number; + statement?: string; + summary?: string; } export interface insightDetails { diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts b/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts index 191920f4e593..61cf42f68aa4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts +++ b/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts @@ -8,170 +8,81 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { unset } from "src/util"; +import { limitStringArray, unset } from "src/util"; +import { FlattenedStmtInsights } from "src/api/insightsApi"; import { - StatementInsights, - TransactionInsightEventDetailsResponse, - TransactionInsightEventDetailsState, - TransactionInsightEventsResponse, - TransactionInsightEventState, -} from "src/api/insightsApi"; -import { - getInsightFromProblem, + ExecutionDetails, + FlattenedStmtInsightEvent, + getInsightFromCause, Insight, InsightExecEnum, InsightNameEnum, InsightRecommendation, InsightType, - InsightTypes, + MergedTxnInsightEvent, SchemaInsightEventFilters, StatementInsightEvent, - TransactionInsightEvent, - TransactionInsightEventDetails, + TxnContentionInsightDetails, + TxnContentionInsightEvent, + TxnInsightDetails, + TxnInsightEvent, WorkloadInsightEventFilters, } from "./types"; -const getTransactionInsights = ( - eventState: TransactionInsightEventState, -): Insight[] => { - const insights: Insight[] = []; - if (eventState) { - InsightTypes.forEach(insight => { - if ( - insight(eventState.execType, eventState.contentionThreshold).name == - eventState.insightName - ) { - insights.push( - insight( - eventState.execType, - eventState.contentionThreshold, - eventState.contentionDuration.milliseconds(), - ), - ); - } - }); - } - return insights; -}; - -export const getTransactionInsightsFromDetails = ( - eventState: TransactionInsightEventDetailsState, -): Insight[] => { - if (!eventState) { - return []; - } - return InsightTypes.map(insight => - insight( - eventState.execType, - eventState.contentionThreshold, - eventState.totalContentionTime, - ), - ).filter(insight => insight.name === eventState.insightName); -}; - -export function getInsightsFromState( - insightEventsResponse: TransactionInsightEventsResponse, -): TransactionInsightEvent[] { - const insightEvents: TransactionInsightEvent[] = []; - if (!insightEventsResponse || insightEventsResponse?.length < 0) { - return insightEvents; - } - - insightEventsResponse.forEach(e => { - const insightsForEvent = getTransactionInsights(e); - if (insightsForEvent.length < 1) { - return; - } else { - insightEvents.push({ - transactionID: e.transactionID, - fingerprintID: e.fingerprintID, - queries: e.queries, - insights: insightsForEvent, - startTime: e.startTime, - contentionDuration: e.contentionDuration, - application: e.application, - execType: InsightExecEnum.TRANSACTION, - contentionThreshold: e.contentionThreshold, - }); - } - }); - - return insightEvents; -} - -// This function adds the insights field to TransactionInsightEventDetailsResponse -export function getTransactionInsightEventDetailsFromState( - insightEventDetailsResponse: TransactionInsightEventDetailsResponse, -): TransactionInsightEventDetails { - const insightsForEventDetails = getTransactionInsightsFromDetails( - insightEventDetailsResponse, - ); - if (!insightsForEventDetails?.length) { - return null; - } - const { insightName, ...resp } = insightEventDetailsResponse; - return { - ...resp, - insights: insightsForEventDetails, - }; -} - export const filterTransactionInsights = ( - transactions: TransactionInsightEvent[] | null, + transactions: MergedTxnInsightEvent[] | null, filters: WorkloadInsightEventFilters, internalAppNamePrefix: string, search?: string, -): TransactionInsightEvent[] => { +): MergedTxnInsightEvent[] => { if (transactions == null) return []; let filteredTransactions = transactions; - const isInternal = (txn: TransactionInsightEvent) => - txn.application.startsWith(internalAppNamePrefix); + const isInternal = (txn: { application?: string }) => + txn?.application?.startsWith(internalAppNamePrefix); if (filters.app) { - filteredTransactions = filteredTransactions.filter( - (txn: TransactionInsightEvent) => { - const apps = filters.app.toString().split(","); - let showInternal = false; - if (apps.includes(internalAppNamePrefix)) { - showInternal = true; - } - if (apps.includes(unset)) { - apps.push(""); - } + filteredTransactions = filteredTransactions.filter(txn => { + const apps = filters.app.toString().split(","); + let showInternal = false; + if (apps.includes(internalAppNamePrefix)) { + showInternal = true; + } + if (apps.includes(unset)) { + apps.push(""); + } - return ( - (showInternal && isInternal(txn)) || apps.includes(txn.application) - ); - }, - ); + return ( + (showInternal && isInternal(txn)) || apps.includes(txn.application) + ); + }); } else { filteredTransactions = filteredTransactions.filter(txn => !isInternal(txn)); } if (search) { search = search.toLowerCase(); + filteredTransactions = filteredTransactions.filter( txn => - !search || - txn.transactionID.toLowerCase()?.includes(search) || - txn.queries?.find(query => query.toLowerCase().includes(search)), + txn.transactionExecutionID.toLowerCase()?.includes(search) || + limitStringArray(txn.queries, 300).toLowerCase().includes(search), ); } return filteredTransactions; }; export function getAppsFromTransactionInsights( - transactions: TransactionInsightEvent[] | null, + transactions: MergedTxnInsightEvent[] | null, internalAppNamePrefix: string, ): string[] { if (transactions == null) return []; const uniqueAppNames = new Set( transactions.map(t => { - if (t.application.startsWith(internalAppNamePrefix)) { + if (t?.application.startsWith(internalAppNamePrefix)) { return internalAppNamePrefix; } - return t.application ? t.application : unset; + return t?.application ? t.application : unset; }), ); @@ -256,11 +167,11 @@ export function insightType(type: InsightType): string { } export const filterStatementInsights = ( - statements: StatementInsights | null, + statements: FlattenedStmtInsights | null, filters: WorkloadInsightEventFilters, internalAppNamePrefix: string, search?: string, -): StatementInsights => { +): FlattenedStmtInsights => { if (statements == null) return []; let filteredStatements = statements; @@ -269,7 +180,7 @@ export const filterStatementInsights = ( appName.startsWith(internalAppNamePrefix); if (filters.app) { filteredStatements = filteredStatements.filter( - (stmt: StatementInsightEvent) => { + (stmt: FlattenedStmtInsightEvent) => { const apps = filters.app.toString().split(","); let showInternal = false; if (apps.includes(internalAppNamePrefix)) { @@ -295,7 +206,7 @@ export const filterStatementInsights = ( filteredStatements = filteredStatements.filter( stmt => !search || - stmt.statementID.toLowerCase()?.includes(search) || + stmt.statementExecutionID.toLowerCase()?.includes(search) || stmt.query?.toLowerCase().includes(search), ); } @@ -303,7 +214,7 @@ export const filterStatementInsights = ( }; export function getAppsFromStatementInsights( - statements: StatementInsights | null, + statements: FlattenedStmtInsights | null, internalAppNamePrefix: string, ): string[] { if (statements == null || statements?.length === 0) return []; @@ -320,48 +231,262 @@ export function getAppsFromStatementInsights( return Array.from(uniqueAppNames).sort(); } -export function populateStatementInsightsFromProblemAndCauses( - statements: StatementInsights, -): StatementInsights { - if (!statements?.length) { - return []; - } +/** + * getInsightsFromProblemsAndCauses returns a list of insight objects with + * labels and descriptions based on the problem, causes for the problem, and + * the execution type. + * @param problem the problem with the query e.g. SlowExecution, should be a InsightNameEnum + * @param causes an array of strings detailing the causes for the problem, if known + * @param execType execution type + * @returns list of insight objects + */ +export function getInsightsFromProblemsAndCauses( + problem: string, + causes: string[] | null, + execType: InsightExecEnum, +): Insight[] { + // TODO(ericharmeling,todd): Replace these strings when using the insights protos. + const insights: Insight[] = []; - const stmtsWithInsights: StatementInsights = statements.map(statement => { - // TODO(ericharmeling,todd): Replace these strings when using the insights protos. - const insights: Insight[] = []; - switch (statement.problem) { - case "SlowExecution": - statement.causes?.forEach(cause => - insights.push( - getInsightFromProblem(cause, InsightExecEnum.STATEMENT), - ), - ); + switch (problem) { + case "SlowExecution": + causes?.forEach(cause => + insights.push(getInsightFromCause(cause, execType)), + ); - if (insights.length === 0) { - insights.push( - getInsightFromProblem( - InsightNameEnum.slowExecution, - InsightExecEnum.STATEMENT, - ), - ); - } - break; - - case "FailedExecution": + if (insights.length === 0) { insights.push( - getInsightFromProblem( - InsightNameEnum.failedExecution, - InsightExecEnum.STATEMENT, - ), + getInsightFromCause(InsightNameEnum.slowExecution, execType), ); - break; + } + break; - default: + case "FailedExecution": + insights.push( + getInsightFromCause(InsightNameEnum.failedExecution, execType), + ); + break; + + default: + } + + return insights; +} + +/** + * flattenTxnInsightsToStmts flattens the txn insights array + * into its stmt insights. Only stmts with non-empty insights + * array will be included. + * @param txnInsights array of transaction insights + * @returns An array of FlattenedStmtInsightEvent where each elem + * includes stmt and txn info. Only includes stmts with non-empty + * insights array. + */ +export function flattenTxnInsightsToStmts( + txnInsights: TxnInsightEvent[], +): FlattenedStmtInsightEvent[] { + if (!txnInsights?.length) return []; + const stmtInsights: FlattenedStmtInsightEvent[] = []; + txnInsights.forEach(txnInsight => { + const { statementInsights, ...txnInfo } = txnInsight; + statementInsights?.forEach(stmt => { + if (!stmt.insights?.length) return; + stmtInsights.push({ ...txnInfo, ...stmt, query: stmt.query }); + }); + }); + return stmtInsights; +} + +/** + * mergeTxnContentionAndStmtInsights merges a list of txn insights + * aggregated from stmt insights, and a list of txn contention insights. + * If a txn exists in both lists, its information will be merged. + * @param txnInsightsFromStmts txn insights aggregated from stmts + * @param txnContentionInsights txn contention insights + * @returns list of merged txn insights + */ +export function mergeTxnContentionAndStmtInsights( + txnInsightsFromStmts: TxnInsightEvent[], + txnContentionInsights: TxnContentionInsightEvent[], +): MergedTxnInsightEvent[] { + const eventByTxnFingerprint: Record = {}; + txnContentionInsights?.forEach(txn => { + const formattedTxn = { + transactionExecutionID: txn.transactionID, + transactionFingerprintID: txn.transactionFingerprintID, + contention: txn.contentionDuration, + statementInsights: [] as StatementInsightEvent[], + insights: txn.insights, + queries: txn.queries, + startTime: txn.startTime, + application: txn.application, + }; + eventByTxnFingerprint[txn.transactionFingerprintID] = formattedTxn; + }); + + txnInsightsFromStmts?.forEach(txn => { + const existingContentionEvent = + eventByTxnFingerprint[txn.transactionFingerprintID]; + if (existingContentionEvent) { + if ( + existingContentionEvent.transactionExecutionID !== + txn.transactionExecutionID + ) { + // Not the same execution - for now we opt to return the contention event. + // TODO (xinhaoz) return the txn that executed more recently once + // we have txn start and end in the insights table. For now let's + // take the entry from the contention registry. + return; // Continue + } + // Merge the two results. + eventByTxnFingerprint[txn.transactionFingerprintID] = { + ...txn, + contention: existingContentionEvent.contention, + startTime: existingContentionEvent.startTime, + insights: txn.insights.concat(existingContentionEvent.insights), + }; + return; // Continue } - return { ...statement, insights }; + // This is a new key. + eventByTxnFingerprint[txn.transactionFingerprintID] = txn; }); - return stmtsWithInsights; + return Object.values(eventByTxnFingerprint); +} + +export function mergeTxnInsightDetails( + txnDetailsFromStmts: TxnInsightEvent | null, + txnContentionDetails: TxnContentionInsightDetails | null, +): TxnInsightDetails { + if (!txnContentionDetails) + return txnDetailsFromStmts + ? { ...txnDetailsFromStmts, execType: InsightExecEnum.TRANSACTION } + : null; + + // Merge info from txnDetailsFromStmts, if it exists. + return { + transactionExecutionID: txnContentionDetails.transactionExecutionID, + transactionFingerprintID: txnContentionDetails.transactionFingerprintID, + application: + txnContentionDetails.application ?? txnDetailsFromStmts?.application, + lastRetryReason: txnDetailsFromStmts?.lastRetryReason, + statementInsights: txnDetailsFromStmts?.statementInsights, + insights: txnContentionDetails.insights.concat( + txnDetailsFromStmts?.insights ?? [], + ), + queries: txnContentionDetails.queries, + startTime: txnContentionDetails.startTime, + blockingContentionDetails: txnContentionDetails.blockingContentionDetails, + contentionThreshold: txnContentionDetails.contentionThreshold, + totalContentionTimeMs: txnContentionDetails.totalContentionTimeMs, + execType: InsightExecEnum.TRANSACTION, + }; +} + +export function getRecommendationForExecInsight( + insight: Insight, + execDetails: ExecutionDetails | null, +): InsightRecommendation { + switch (insight.name) { + case InsightNameEnum.highContention: + return { + type: InsightNameEnum.highContention, + execution: execDetails, + details: { + duration: execDetails.contentionTime, + description: insight.description, + }, + }; + case InsightNameEnum.failedExecution: + return { + type: InsightNameEnum.failedExecution, + execution: execDetails, + }; + case InsightNameEnum.highRetryCount: + return { + type: InsightNameEnum.highRetryCount, + execution: execDetails, + details: { + description: insight.description, + }, + }; + case InsightNameEnum.planRegression: + return { + type: InsightNameEnum.planRegression, + execution: execDetails, + details: { + description: insight.description, + }, + }; + case InsightNameEnum.suboptimalPlan: + return { + type: InsightNameEnum.suboptimalPlan, + database: execDetails.databaseName, + execution: execDetails, + details: { + description: insight.description, + }, + }; + default: + return { + type: "Unknown", + execution: execDetails, + details: { + duration: execDetails.elapsedTimeMillis, + description: insight.description, + }, + }; + } +} + +export function getStmtInsightRecommendations( + insightDetails: Partial | null, +): InsightRecommendation[] { + if (!insightDetails) return []; + + const execDetails: ExecutionDetails = { + statement: insightDetails.query, + fingerprintID: insightDetails.statementFingerprintID, + retries: insightDetails.retries, + indexRecommendations: insightDetails.indexRecommendations, + databaseName: insightDetails.databaseName, + elapsedTimeMillis: insightDetails.elapsedTimeMillis, + }; + + const recs: InsightRecommendation[] = insightDetails.insights?.map(insight => + getRecommendationForExecInsight(insight, execDetails), + ); + + return recs; +} + +export function getTxnInsightRecommendations( + insightDetails: TxnInsightDetails | null, +): InsightRecommendation[] { + if (!insightDetails) return []; + + const execDetails: ExecutionDetails = { + retries: insightDetails.retries, + databaseName: insightDetails.databaseName, + contentionTime: insightDetails.totalContentionTimeMs, + }; + const recs: InsightRecommendation[] = []; + + insightDetails.statementInsights?.forEach(stmt => + getStmtInsightRecommendations({ + ...stmt, + ...execDetails, + })?.forEach(rec => recs.push(rec)), + ); + + // This is necessary since txn contention insight currently is not + // surfaced from the stmt level for txns. + if (recs.length === 0) { + insightDetails.insights?.forEach(insight => + recs.push(getRecommendationForExecInsight(insight, execDetails)), + ); + } + + return recs; } diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/insightDetailsTables.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/insightDetailsTables.tsx index eec85d95e76e..4cda0bda5e76 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/insightDetailsTables.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/insightDetailsTables.tsx @@ -11,71 +11,71 @@ import React from "react"; import { ColumnDescriptor, SortedTable } from "src/sortedtable"; import { DATE_FORMAT, Duration } from "src/util"; -import { EventExecution, InsightExecEnum } from "../types"; +import { ContentionEvent, InsightExecEnum } from "../types"; import { insightsTableTitles, QueriesCell } from "../workloadInsights/util"; interface InsightDetailsTableProps { - data: EventExecution[]; + data: ContentionEvent[]; execType: InsightExecEnum; } export function makeInsightDetailsColumns( execType: InsightExecEnum, -): ColumnDescriptor[] { +): ColumnDescriptor[] { return [ { name: "executionID", title: insightsTableTitles.executionID(execType), - cell: (item: EventExecution) => String(item.executionID), - sort: (item: EventExecution) => item.executionID, + cell: (item: ContentionEvent) => String(item.executionID), + sort: (item: ContentionEvent) => item.executionID, }, { name: "fingerprintID", title: insightsTableTitles.fingerprintID(execType), - cell: (item: EventExecution) => String(item.fingerprintID), - sort: (item: EventExecution) => item.fingerprintID, + cell: (item: ContentionEvent) => String(item.fingerprintID), + sort: (item: ContentionEvent) => item.fingerprintID, }, { name: "query", title: insightsTableTitles.query(execType), - cell: (item: EventExecution) => QueriesCell(item.queries, 50), - sort: (item: EventExecution) => item.queries.length, + cell: (item: ContentionEvent) => QueriesCell(item.queries, 50), + sort: (item: ContentionEvent) => item.queries.length, }, { name: "contentionStartTime", title: insightsTableTitles.contentionStartTime(execType), - cell: (item: EventExecution) => item.startTime.format(DATE_FORMAT), - sort: (item: EventExecution) => item.startTime.unix(), + cell: (item: ContentionEvent) => item.startTime.format(DATE_FORMAT), + sort: (item: ContentionEvent) => item.startTime.unix(), }, { name: "contention", title: insightsTableTitles.contention(execType), - cell: (item: EventExecution) => Duration(item.contentionTimeMs * 1e6), - sort: (item: EventExecution) => item.contentionTimeMs, + cell: (item: ContentionEvent) => Duration(item.contentionTimeMs * 1e6), + sort: (item: ContentionEvent) => item.contentionTimeMs, }, { name: "schemaName", title: insightsTableTitles.schemaName(execType), - cell: (item: EventExecution) => item.schemaName, - sort: (item: EventExecution) => item.schemaName, + cell: (item: ContentionEvent) => item.schemaName, + sort: (item: ContentionEvent) => item.schemaName, }, { name: "databaseName", title: insightsTableTitles.databaseName(execType), - cell: (item: EventExecution) => item.databaseName, - sort: (item: EventExecution) => item.databaseName, + cell: (item: ContentionEvent) => item.databaseName, + sort: (item: ContentionEvent) => item.databaseName, }, { name: "tableName", title: insightsTableTitles.tableName(execType), - cell: (item: EventExecution) => item.tableName, - sort: (item: EventExecution) => item.tableName, + cell: (item: ContentionEvent) => item.tableName, + sort: (item: ContentionEvent) => item.tableName, }, { name: "indexName", title: insightsTableTitles.indexName(execType), - cell: (item: EventExecution) => item.indexName, - sort: (item: EventExecution) => item.indexName, + cell: (item: ContentionEvent) => item.indexName, + sort: (item: ContentionEvent) => item.indexName, }, ]; } diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetails.tsx index d7e490c10863..201b008389a2 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetails.tsx @@ -19,7 +19,7 @@ import { Button } from "src/button"; import { Loading } from "src/loading"; import { SqlBox, SqlBoxSize } from "src/sql"; import { getMatchParamByName, executionIdAttr } from "src/util"; -import { StatementInsightEvent } from "../types"; +import { FlattenedStmtInsightEvent } from "../types"; import { InsightsError } from "../insightsErrorComponent"; import classNames from "classnames/bind"; @@ -39,7 +39,7 @@ enum TabKeysEnum { EXPLAIN = "explain", } export interface StatementInsightDetailsStateProps { - insightEventDetails: StatementInsightEvent; + insightEventDetails: FlattenedStmtInsightEvent; insightError: Error | null; isTenant?: boolean; } diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsOverviewTab.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsOverviewTab.tsx index e38564e57b70..f2f83273759a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsOverviewTab.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsOverviewTab.tsx @@ -16,12 +16,7 @@ import { import { SummaryCard, SummaryCardItem } from "src/summaryCard"; import { capitalize, Duration } from "src/util"; import { DATE_FORMAT_24_UTC } from "src/util/format"; -import { - executionDetails, - InsightNameEnum, - InsightRecommendation, - StatementInsightEvent, -} from "../types"; +import { FlattenedStmtInsightEvent } from "../types"; import classNames from "classnames/bind"; import { CockroachCloudContext } from "../../contexts"; @@ -36,84 +31,14 @@ import { TransactionDetailsLink, } from "../workloadInsights/util"; import { TimeScale } from "../../timeScaleDropdown"; +import { getStmtInsightRecommendations } from "../utils"; const cx = classNames.bind(insightsDetailsStyles); const tableCx = classNames.bind(insightTableStyles); const summaryCardStylesCx = classNames.bind(summaryCardStyles); -const insightsTableData = ( - insightDetails: StatementInsightEvent | null, -): InsightRecommendation[] => { - if (!insightDetails) return []; - - const execDetails: executionDetails = { - statement: insightDetails.query, - fingerprintID: insightDetails.statementFingerprintID, - retries: insightDetails.retries, - }; - - const recs: InsightRecommendation[] = insightDetails.insights?.map( - insight => { - switch (insight.name) { - case InsightNameEnum.highContention: - return { - type: "HighContention", - execution: execDetails, - details: { - duration: insightDetails.elapsedTimeMillis, - description: insight.description, - }, - }; - case InsightNameEnum.failedExecution: - return { - type: "FailedExecution", - }; - case InsightNameEnum.highRetryCount: - return { - type: "HighRetryCount", - execution: execDetails, - details: { - description: insight.description, - }, - }; - break; - case InsightNameEnum.planRegression: - return { - type: "PlanRegression", - execution: execDetails, - details: { - description: insight.description, - }, - }; - case InsightNameEnum.suboptimalPlan: - return { - type: "SuboptimalPlan", - database: insightDetails.databaseName, - execution: { - ...execDetails, - indexRecommendations: insightDetails.indexRecommendations, - }, - details: { - description: insight.description, - }, - }; - default: - return { - type: "Unknown", - details: { - duration: insightDetails.elapsedTimeMillis, - description: insight.description, - }, - }; - } - }, - ); - - return recs; -}; - export interface StatementInsightDetailsOverviewTabProps { - insightEventDetails: StatementInsightEvent; + insightEventDetails: FlattenedStmtInsightEvent; setTimeScale: (ts: TimeScale) => void; } @@ -128,7 +53,7 @@ export const StatementInsightDetailsOverviewTab: React.FC< ); const insightDetails = insightEventDetails; - const tableData = insightsTableData(insightDetails); + const tableData = getStmtInsightRecommendations(insightDetails); return (
@@ -192,7 +117,7 @@ export const StatementInsightDetailsOverviewTab: React.FC< /> insight.name === InsightNameEnum.highContention) - .map(insight => { - return { - type: "HighContention", - details: { - duration: insightDetails.totalContentionTime, - description: insight.description, - }, - }; - }); -} - +import "antd/lib/tabs/style"; export interface TransactionInsightDetailsStateProps { - insightEventDetails: TransactionInsightEventDetailsResponse; + insightDetails: TxnInsightDetails; insightError: Error | null; } export interface TransactionInsightDetailsDispatchProps { refreshTransactionInsightDetails: ( - req: TransactionInsightEventDetailsRequest, + req: TxnContentionInsightDetailsRequest, ) => void; setTimeScale: (ts: TimeScale) => void; } @@ -86,21 +45,26 @@ export type TransactionInsightDetailsProps = TransactionInsightDetailsDispatchProps & RouteComponentProps; +enum TabKeysEnum { + OVERVIEW = "overview", + STATEMENTS = "statements", +} + export const TransactionInsightDetails: React.FC< TransactionInsightDetailsProps > = ({ refreshTransactionInsightDetails, setTimeScale, history, - insightEventDetails, + insightDetails, insightError, match, }) => { - const isCockroachCloud = useContext(CockroachCloudContext); const executionID = getMatchParamByName(match, executionIdAttr); - const noInsights = !insightEventDetails; + const noInsights = !insightDetails; useEffect(() => { if (noInsights) { + // Only refresh if we have no data (e.g. refresh the page) refreshTransactionInsightDetails({ id: executionID, }); @@ -109,31 +73,6 @@ export const TransactionInsightDetails: React.FC< const prevPage = (): void => history.goBack(); - const insightDetails = - getTransactionInsightEventDetailsFromState(insightEventDetails); - - const insightQueries = - insightDetails?.queries.join("") || "Insight not found."; - const insightsColumns = makeInsightsColumns(isCockroachCloud); - - const blockingExecutions: EventExecution[] = - insightDetails?.blockingContentionDetails.map(x => { - return { - executionID: x.blockingExecutionID, - fingerprintID: x.blockingFingerprintID, - queries: x.blockingQueries, - startTime: x.collectionTimeStamp, - contentionTimeMs: x.contentionTimeMs, - execType: insightDetails.execType, - schemaName: x.schemaName, - databaseName: x.databaseName, - tableName: x.tableName, - indexName: x.indexName, - }; - }); - - const tableData = insightsTableData(insightDetails); - return (
@@ -156,76 +95,32 @@ export const TransactionInsightDetails: React.FC<
InsightsError()} > -
- - - - - - {insightDetails && ( - <> - - - - - - - - - - - - - - {/* TO DO (ericharmeling): We might want this table to span the entire page when other types of insights - are added*/} - - - - - + + + + + {insightDetails?.statementInsights?.length && ( + + + )} -
- {blockingExecutions?.length && insightDetails && ( -
- - - - {WaitTimeInsightsLabels.BLOCKED_TXNS_TABLE_TITLE( - insightDetails.executionID, - insightDetails.execType, - )} - -
- -
- -
-
- )} +
diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsConnected.tsx index d14afd3a7b09..20c1047595fb 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsConnected.tsx @@ -23,7 +23,7 @@ import { import { TimeScale } from "../../timeScaleDropdown"; import { actions as sqlStatsActions } from "../../store/sqlStats"; import { Dispatch } from "redux"; -import { TransactionInsightEventDetailsRequest } from "src/api"; +import { TxnContentionInsightDetailsRequest } from "src/api"; const mapStateToProps = ( state: AppState, @@ -32,7 +32,7 @@ const mapStateToProps = ( const insightDetails = selectTransactionInsightDetails(state, props); const insightError = selectTransactionInsightDetailsError(state, props); return { - insightEventDetails: insightDetails, + insightDetails: insightDetails, insightError: insightError, }; }; @@ -41,7 +41,7 @@ const mapDispatchToProps = ( dispatch: Dispatch, ): TransactionInsightDetailsDispatchProps => ({ refreshTransactionInsightDetails: ( - req: TransactionInsightEventDetailsRequest, + req: TxnContentionInsightDetailsRequest, ) => { dispatch(actions.refresh(req)); }, diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx new file mode 100644 index 000000000000..32b86a2f3d49 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx @@ -0,0 +1,195 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import React, { useContext } from "react"; +import { Heading } from "@cockroachlabs/ui-components"; +import { Col, Row } from "antd"; +import "antd/lib/col/style"; +import "antd/lib/row/style"; +import { SqlBox, SqlBoxSize } from "src/sql"; +import { SummaryCard, SummaryCardItem } from "src/summaryCard"; +import { DATE_FORMAT_24_UTC } from "src/util/format"; +import { WaitTimeInsightsLabels } from "src/detailsPanels/waitTimeInsightsPanel"; +import { TxnContentionInsightDetailsRequest } from "src/api"; +import { + InsightsSortedTable, + makeInsightsColumns, +} from "src/insightsTable/insightsTable"; +import { WaitTimeDetailsTable } from "./insightDetailsTables"; +import { ContentionEvent, TxnInsightDetails } from "../types"; + +import classNames from "classnames/bind"; +import insightTableStyles from "src/insightsTable/insightsTable.module.scss"; +import { CockroachCloudContext } from "../../contexts"; +import { TransactionDetailsLink } from "../workloadInsights/util"; +import { TimeScale } from "../../timeScaleDropdown"; +import { getTxnInsightRecommendations } from "../utils"; + +const tableCx = classNames.bind(insightTableStyles); + +export interface TransactionInsightDetailsStateProps { + insightDetails: TxnInsightDetails; + insightError: Error | null; +} + +export interface TransactionInsightDetailsDispatchProps { + refreshTransactionInsightDetails: ( + req: TxnContentionInsightDetailsRequest, + ) => void; + setTimeScale: (ts: TimeScale) => void; +} + +type Props = { + insightDetails: TxnInsightDetails; + setTimeScale: (ts: TimeScale) => void; +}; + +export const TransactionInsightDetailsOverviewTab: React.FC = ({ + insightDetails, + setTimeScale, +}) => { + const isCockroachCloud = useContext(CockroachCloudContext); + + const insightQueries = + insightDetails?.queries?.join("") || "Insight not found."; + const insightsColumns = makeInsightsColumns(isCockroachCloud); + + const blockingExecutions: ContentionEvent[] = + insightDetails?.blockingContentionDetails?.map(x => { + return { + executionID: x.blockingExecutionID, + fingerprintID: x.blockingTxnFingerprintID, + queries: x.blockingQueries, + startTime: x.collectionTimeStamp, + contentionTimeMs: x.contentionTimeMs, + execType: insightDetails.execType, + schemaName: x.schemaName, + databaseName: x.databaseName, + tableName: x.tableName, + indexName: x.indexName, + }; + }); + + const stmtInsights = insightDetails?.statementInsights?.map(stmt => ({ + ...stmt, + retries: insightDetails.retries, + databaseName: insightDetails.databaseName, + })); + + // Build insight recommendations off of all stmt insights. + // TODO: (xinhaoz) these recs should be a bit more detailed when there + // is stmt info available + const insightRecs = getTxnInsightRecommendations(insightDetails); + + const rowsRead = + stmtInsights?.reduce((count, stmt) => (count += stmt.rowsRead), 0) ?? "N/A"; + const rowsWritten = + stmtInsights?.reduce((count, stmt) => (count += stmt.rowsWritten), 0) ?? + "N/A"; + + return ( +
+
+ + + + + + {insightDetails && ( + <> + + + + + + + + stmt.isFullScan) + ?.toString() ?? "N/A" + } + /> + + + + + + + + + + + + + + + + + + + )} +
+ {blockingExecutions?.length && insightDetails && ( +
+ + + + {WaitTimeInsightsLabels.BLOCKED_TXNS_TABLE_TITLE( + insightDetails.transactionExecutionID, + insightDetails.execType, + )} + +
+ +
+ +
+
+ )} +
+ ); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsStmtsTab.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsStmtsTab.tsx new file mode 100644 index 000000000000..6160123f2ff8 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsStmtsTab.tsx @@ -0,0 +1,88 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import React from "react"; +import { Link } from "react-router-dom"; +import { ColumnDescriptor, SortedTable } from "src/sortedtable"; +import { StatementInsightEvent, TxnInsightDetails } from "../types"; +import { InsightCell } from "../workloadInsights/util/insightCell"; +import { DATE_FORMAT, Duration, limitText } from "src/util"; + +const stmtColumns: ColumnDescriptor[] = [ + { + name: "executionID", + title: "Execution ID", + cell: (item: StatementInsightEvent) => + item.insights?.length ? ( + + {item.statementExecutionID} + + ) : ( + item.statementExecutionID + ), + sort: (item: StatementInsightEvent) => item.statementExecutionID, + alwaysShow: true, + }, + { + name: "query", + title: "Statement Execution", + cell: (item: StatementInsightEvent) => limitText(item.query, 50), + sort: (item: StatementInsightEvent) => item.query, + }, + { + name: "insights", + title: "Insights", + cell: (item: StatementInsightEvent) => + item.insights?.map(i => InsightCell(i)) ?? "None", + sort: (item: StatementInsightEvent) => + item.insights?.reduce((str, i) => (str += i.label), ""), + }, + { + name: "startTime", + title: "Start Time", + cell: (item: StatementInsightEvent) => item.startTime.format(DATE_FORMAT), + sort: (item: StatementInsightEvent) => item.startTime.unix(), + }, + { + name: "endTime", + title: "End Time", + cell: (item: StatementInsightEvent) => item.endTime.format(DATE_FORMAT), + sort: (item: StatementInsightEvent) => item.endTime.unix(), + }, + { + name: "executionTime", + title: "Execution Time", + cell: (item: StatementInsightEvent) => + Duration(item.elapsedTimeMillis * 1e6), + sort: (item: StatementInsightEvent) => item.elapsedTimeMillis, + }, + { + name: "waitTime", + title: "Time Spent Waiting", + cell: (item: StatementInsightEvent) => + Duration((item.timeSpentWaiting?.asMilliseconds() ?? 0) * 1e6), + sort: (item: StatementInsightEvent) => item.elapsedTimeMillis, + }, +]; + +type Props = { + insightDetails: TxnInsightDetails; +}; + +export const TransactionInsightsDetailsStmtsTab: React.FC = ({ + insightDetails, +}) => { + return ( + + ); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsights.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsights.fixture.ts deleted file mode 100644 index 8bd4e7f708c1..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsights.fixture.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2022 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { StatementInsightsViewProps } from "./statementInsightsView"; -import moment from "moment"; - -export const statementInsightsPropsFixture: StatementInsightsViewProps = { - onColumnsChange: x => {}, - selectedColumnNames: [], - statements: [ - { - statementID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a9", - statementFingerprintID: "abc", - transactionFingerprintID: "defg", - transactionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a5", - implicitTxn: true, - query: - "SELECT IFNULL(a, b) FROM (SELECT (SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _) AS a, (SELECT code FROM promo_codes ORDER BY code LIMIT _) AS b)", - startTime: moment.utc("2022.08.10"), - endTime: moment.utc("2022.08.10 00:00:00.25"), - elapsedTimeMillis: moment.duration("00:00:00.25").asMilliseconds(), - application: "demo", - databaseName: "test", - username: "username test", - lastRetryReason: null, - isFullScan: false, - retries: 0, - problem: "SlowExecution", - causes: ["HighContention"], - priority: "high", - sessionID: "103", - timeSpentWaiting: null, - rowsWritten: 0, - rowsRead: 100, - insights: null, - indexRecommendations: ["make this index"], - planGist: "abc", - }, - { - statementID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a9", - statementFingerprintID: "938x3", - transactionFingerprintID: "1971x3", - transactionID: "e72f37ea-b3a0-451f-80b8-dfb27d0bc2a5", - implicitTxn: true, - query: "INSERT INTO vehicles VALUES ($1, $2, __more1_10__)", - startTime: moment.utc("2022.08.10"), - endTime: moment.utc("2022.08.10 00:00:00.25"), - elapsedTimeMillis: moment.duration("00:00:00.25").asMilliseconds(), - application: "demo", - databaseName: "test", - username: "username test", - lastRetryReason: null, - isFullScan: false, - retries: 0, - problem: "SlowExecution", - causes: ["HighContention"], - priority: "high", - sessionID: "103", - timeSpentWaiting: null, - rowsWritten: 0, - rowsRead: 100, - insights: null, - indexRecommendations: ["make that index"], - planGist: "abc", - }, - { - statementID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a9", - statementFingerprintID: "hisas", - transactionFingerprintID: "3anc", - transactionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a0", - implicitTxn: true, - query: - "UPSERT INTO vehicle_location_histories VALUES ($1, $2, now(), $3, $4)", - startTime: moment.utc("2022.08.10"), - endTime: moment.utc("2022.08.10 00:00:00.25"), - elapsedTimeMillis: moment.duration("00:00:00.25").asMilliseconds(), - application: "demo", - databaseName: "test", - username: "username test", - lastRetryReason: null, - isFullScan: false, - retries: 0, - problem: "SlowExecution", - causes: ["HighContention"], - priority: "high", - sessionID: "103", - timeSpentWaiting: null, - rowsWritten: 0, - rowsRead: 100, - insights: null, - indexRecommendations: ["make these indices"], - planGist: "abc", - }, - ], - statementsError: null, - sortSetting: { - ascending: false, - columnTitle: "startTime", - }, - filters: { - app: "", - }, - refreshStatementInsights: () => {}, - onSortChange: () => {}, - onFiltersChange: () => {}, - setTimeScale: () => {}, -}; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsTable.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsTable.tsx index fb3372c9f310..14f5cd2bd869 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsTable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsTable.tsx @@ -16,13 +16,13 @@ import { SortSetting, } from "src/sortedtable"; import { Count, DATE_FORMAT, Duration, limitText } from "src/util"; -import { InsightExecEnum, StatementInsightEvent } from "src/insights"; +import { InsightExecEnum, FlattenedStmtInsightEvent } from "src/insights"; import { InsightCell, insightsTableTitles, StatementDetailsLink, } from "../util"; -import { StatementInsights } from "src/api"; +import { FlattenedStmtInsights } from "src/api"; import { Tooltip } from "@cockroachlabs/ui-components"; import { Link } from "react-router-dom"; import classNames from "classnames/bind"; @@ -32,55 +32,55 @@ import { TimeScale } from "../../../timeScaleDropdown"; const cx = classNames.bind(styles); interface StatementInsightsTable { - data: StatementInsights; + data: FlattenedStmtInsights; sortSetting: SortSetting; onChangeSortSetting: (ss: SortSetting) => void; pagination: ISortedTablePagination; renderNoResult?: React.ReactNode; - visibleColumns: ColumnDescriptor[]; + visibleColumns: ColumnDescriptor[]; } export function makeStatementInsightsColumns( setTimeScale: (ts: TimeScale) => void, -): ColumnDescriptor[] { +): ColumnDescriptor[] { const execType = InsightExecEnum.STATEMENT; return [ { name: "latestExecutionID", title: insightsTableTitles.latestExecutionID(execType), - cell: (item: StatementInsightEvent) => ( - - {String(item.statementID)} + cell: (item: FlattenedStmtInsightEvent) => ( + + {String(item.statementExecutionID)} ), - sort: (item: StatementInsightEvent) => item.statementID, + sort: (item: FlattenedStmtInsightEvent) => item.statementExecutionID, alwaysShow: true, }, { name: "statementFingerprintID", title: insightsTableTitles.fingerprintID(execType), - cell: (item: StatementInsightEvent) => + cell: (item: FlattenedStmtInsightEvent) => StatementDetailsLink(item, setTimeScale), - sort: (item: StatementInsightEvent) => item.statementFingerprintID, + sort: (item: FlattenedStmtInsightEvent) => item.statementFingerprintID, showByDefault: true, }, { name: "query", title: insightsTableTitles.query(execType), - cell: (item: StatementInsightEvent) => ( + cell: (item: FlattenedStmtInsightEvent) => ( {limitText(item.query, 50)} ), - sort: (item: StatementInsightEvent) => item.query, + sort: (item: FlattenedStmtInsightEvent) => item.query, showByDefault: true, }, { name: "insights", title: insightsTableTitles.insights(execType), - cell: (item: StatementInsightEvent) => + cell: (item: FlattenedStmtInsightEvent) => item.insights ? item.insights.map(insight => InsightCell(insight)) : "", - sort: (item: StatementInsightEvent) => + sort: (item: FlattenedStmtInsightEvent) => item.insights ? item.insights.map(insight => insight.label).toString() : "", @@ -89,78 +89,80 @@ export function makeStatementInsightsColumns( { name: "startTime", title: insightsTableTitles.startTime(execType), - cell: (item: StatementInsightEvent) => item.startTime.format(DATE_FORMAT), - sort: (item: StatementInsightEvent) => item.startTime.unix(), + cell: (item: FlattenedStmtInsightEvent) => + item.startTime.format(DATE_FORMAT), + sort: (item: FlattenedStmtInsightEvent) => item.startTime.unix(), showByDefault: true, }, { name: "elapsedTime", title: insightsTableTitles.elapsedTime(execType), - cell: (item: StatementInsightEvent) => + cell: (item: FlattenedStmtInsightEvent) => Duration(item.elapsedTimeMillis * 1e6), - sort: (item: StatementInsightEvent) => item.elapsedTimeMillis, + sort: (item: FlattenedStmtInsightEvent) => item.elapsedTimeMillis, showByDefault: true, }, { name: "userName", title: insightsTableTitles.username(execType), - cell: (item: StatementInsightEvent) => item.username, - sort: (item: StatementInsightEvent) => item.username, + cell: (item: FlattenedStmtInsightEvent) => item.username, + sort: (item: FlattenedStmtInsightEvent) => item.username, showByDefault: true, }, { name: "applicationName", title: insightsTableTitles.applicationName(execType), - cell: (item: StatementInsightEvent) => item.application, - sort: (item: StatementInsightEvent) => item.application, + cell: (item: FlattenedStmtInsightEvent) => item.application, + sort: (item: FlattenedStmtInsightEvent) => item.application, showByDefault: true, }, { name: "rowsProcessed", title: insightsTableTitles.rowsProcessed(execType), className: cx("statements-table__col-rows-read"), - cell: (item: StatementInsightEvent) => + cell: (item: FlattenedStmtInsightEvent) => `${Count(item.rowsRead)} Reads / ${Count(item.rowsRead)} Writes`, - sort: (item: StatementInsightEvent) => item.rowsRead + item.rowsWritten, + sort: (item: FlattenedStmtInsightEvent) => + item.rowsRead + item.rowsWritten, showByDefault: true, }, { name: "retries", title: insightsTableTitles.numRetries(execType), - cell: (item: StatementInsightEvent) => item.retries, - sort: (item: StatementInsightEvent) => item.retries, + cell: (item: FlattenedStmtInsightEvent) => item.retries, + sort: (item: FlattenedStmtInsightEvent) => item.retries, showByDefault: false, }, { name: "contention", title: insightsTableTitles.contention(execType), - cell: (item: StatementInsightEvent) => + cell: (item: FlattenedStmtInsightEvent) => !item.timeSpentWaiting ? "no samples" : Duration(item.timeSpentWaiting.asMilliseconds() * 1e6), - sort: (item: StatementInsightEvent) => + sort: (item: FlattenedStmtInsightEvent) => item.timeSpentWaiting?.asMilliseconds() ?? -1, showByDefault: false, }, { name: "isFullScan", title: insightsTableTitles.isFullScan(execType), - cell: (item: StatementInsightEvent) => String(item.isFullScan), - sort: (item: StatementInsightEvent) => String(item.isFullScan), + cell: (item: FlattenedStmtInsightEvent) => String(item.isFullScan), + sort: (item: FlattenedStmtInsightEvent) => String(item.isFullScan), showByDefault: false, }, { name: "transactionFingerprintID", title: insightsTableTitles.fingerprintID(InsightExecEnum.TRANSACTION), - cell: (item: StatementInsightEvent) => item.transactionFingerprintID, - sort: (item: StatementInsightEvent) => item.transactionFingerprintID, + cell: (item: FlattenedStmtInsightEvent) => item.transactionFingerprintID, + sort: (item: FlattenedStmtInsightEvent) => item.transactionFingerprintID, showByDefault: false, }, { name: "transactionID", title: insightsTableTitles.latestExecutionID(InsightExecEnum.TRANSACTION), - cell: (item: StatementInsightEvent) => item.transactionID, - sort: (item: StatementInsightEvent) => item.transactionID, + cell: (item: FlattenedStmtInsightEvent) => item.transactionExecutionID, + sort: (item: FlattenedStmtInsightEvent) => item.transactionExecutionID, showByDefault: false, }, ]; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx index 71bb0a86d591..f442d2db1c86 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx @@ -30,13 +30,12 @@ import { getTableSortFromURL } from "src/sortedtable/getTableSortFromURL"; import { TableStatistics } from "src/tableStatistics"; import { isSelectedColumn } from "src/columnsSelector/utils"; -import { StatementInsights } from "src/api/insightsApi"; +import { FlattenedStmtInsights } from "src/api/insightsApi"; import { filterStatementInsights, getAppsFromStatementInsights, makeStatementInsightsColumns, WorkloadInsightEventFilters, - populateStatementInsightsFromProblemAndCauses, } from "src/insights"; import { EmptyInsightsTablePlaceholder } from "../util"; import { StatementInsightsTable } from "./statementInsightsTable"; @@ -52,7 +51,7 @@ const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); export type StatementInsightsViewStateProps = { - statements: StatementInsights; + statements: FlattenedStmtInsights; statementsError: Error | null; filters: WorkloadInsightEventFilters; sortSetting: SortSetting; @@ -203,10 +202,6 @@ export const StatementInsightsView: React.FC = ( search, ); - // const statementInsights = - // populateStatementInsightsFromProblemAndCauses(filteredStatements); - const statementInsights = filteredStatements; - const tableColumns = defaultColumns .filter(c => !c.alwaysShow) .map( @@ -255,21 +250,21 @@ export const StatementInsightsView: React.FC = ( 0 && statementInsights?.length === 0 + search?.length > 0 && filteredStatements?.length === 0 } /> } @@ -279,7 +274,7 @@ export const StatementInsightsView: React.FC = ( diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsights.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsights.fixture.ts deleted file mode 100644 index 005fb7681615..000000000000 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsights.fixture.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2022 The Cockroach Authors. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0, included in the file -// licenses/APL.txt. - -import { TransactionInsightsViewProps } from "./transactionInsightsView"; -import moment from "moment"; -import { InsightExecEnum } from "../../types"; - -export const transactionInsightsPropsFixture: TransactionInsightsViewProps = { - transactions: [ - { - transactionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a5", - fingerprintID: "\\x76245b7acd82d39d", - queries: [ - "SELECT IFNULL(a, b) FROM (SELECT (SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _) AS a, (SELECT code FROM promo_codes ORDER BY code LIMIT _) AS b)", - ], - insightName: "HighContention", - startTime: moment.utc("2022.08.10"), - contentionDuration: moment.duration("00:00:00.25"), - application: "demo", - execType: InsightExecEnum.TRANSACTION, - contentionThreshold: moment.duration("00:00:00.1").asMilliseconds(), - }, - { - transactionID: "e72f37ea-b3a0-451f-80b8-dfb27d0bc2a5", - fingerprintID: "\\x76245b7acd82d39e", - queries: [ - "INSERT INTO vehicles VALUES ($1, $2, __more1_10__)", - "INSERT INTO vehicles VALUES ($1, $2, __more1_10__)", - ], - insightName: "HighContention", - startTime: moment.utc("2022.08.10"), - contentionDuration: moment.duration("00:00:00.25"), - application: "demo", - execType: InsightExecEnum.TRANSACTION, - contentionThreshold: moment.duration("00:00:00.1").asMilliseconds(), - }, - { - transactionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a0", - fingerprintID: "\\x76245b7acd82d39f", - queries: [ - "UPSERT INTO vehicle_location_histories VALUES ($1, $2, now(), $3, $4)", - ], - insightName: "HighContention", - startTime: moment.utc("2022.08.10"), - contentionDuration: moment.duration("00:00:00.25"), - application: "demo", - execType: InsightExecEnum.TRANSACTION, - contentionThreshold: moment.duration("00:00:00.1").asMilliseconds(), - }, - ], - transactionsError: null, - sortSetting: { - ascending: false, - columnTitle: "startTime", - }, - filters: { - app: "", - }, - refreshTransactionInsights: () => {}, - onSortChange: () => {}, - onFiltersChange: () => {}, - setTimeScale: () => {}, -}; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsTable.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsTable.tsx index 4a402c54d859..aa768c3799de 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsTable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsTable.tsx @@ -16,7 +16,7 @@ import { SortSetting, } from "src/sortedtable"; import { DATE_FORMAT, Duration } from "src/util"; -import { InsightExecEnum, TransactionInsightEvent } from "src/insights"; +import { InsightExecEnum, MergedTxnInsightEvent } from "src/insights"; import { InsightCell, insightsTableTitles, @@ -27,7 +27,7 @@ import { Link } from "react-router-dom"; import { TimeScale } from "../../../timeScaleDropdown"; interface TransactionInsightsTable { - data: TransactionInsightEvent[]; + data: MergedTxnInsightEvent[]; sortSetting: SortSetting; onChangeSortSetting: (ss: SortSetting) => void; pagination: ISortedTablePagination; @@ -37,42 +37,41 @@ interface TransactionInsightsTable { export function makeTransactionInsightsColumns( setTimeScale: (ts: TimeScale) => void, -): ColumnDescriptor[] { +): ColumnDescriptor[] { const execType = InsightExecEnum.TRANSACTION; return [ { name: "latestExecutionID", title: insightsTableTitles.latestExecutionID(execType), - cell: (item: TransactionInsightEvent) => ( - - {String(item.transactionID)} + cell: item => ( + + {String(item.transactionExecutionID)} ), - sort: (item: TransactionInsightEvent) => item.transactionID, + sort: item => item.transactionExecutionID, }, { name: "fingerprintID", title: insightsTableTitles.fingerprintID(execType), - cell: (item: TransactionInsightEvent) => + cell: item => TransactionDetailsLink( - item.fingerprintID, + item.transactionFingerprintID, item.startTime, setTimeScale, ), - sort: (item: TransactionInsightEvent) => item.fingerprintID, + sort: item => item.transactionFingerprintID, }, { name: "query", title: insightsTableTitles.query(execType), - cell: (item: TransactionInsightEvent) => QueriesCell(item.queries, 50), - sort: (item: TransactionInsightEvent) => item.queries.length, + cell: item => QueriesCell(item.queries, 50), + sort: item => (item.queries?.length ? item.queries[0] : ""), }, { name: "insights", title: insightsTableTitles.insights(execType), - cell: (item: TransactionInsightEvent) => - item.insights ? item.insights.map(insight => InsightCell(insight)) : "", - sort: (item: TransactionInsightEvent) => + cell: item => item.insights.map(insight => InsightCell(insight)), + sort: item => item.insights ? item.insights.map(insight => insight.label).toString() : "", @@ -80,23 +79,20 @@ export function makeTransactionInsightsColumns( { name: "startTime", title: insightsTableTitles.startTime(execType), - cell: (item: TransactionInsightEvent) => - item.startTime.format(DATE_FORMAT), - sort: (item: TransactionInsightEvent) => item.startTime.unix(), + cell: item => item.startTime?.format(DATE_FORMAT) ?? "N/A", + sort: item => item.startTime?.unix() || 0, }, { name: "contention", title: insightsTableTitles.contention(execType), - cell: (item: TransactionInsightEvent) => - Duration(item.contentionDuration.asMilliseconds() * 1e6), - sort: (item: TransactionInsightEvent) => - item.contentionDuration.asMilliseconds(), + cell: item => Duration((item.contention?.asMilliseconds() ?? 0) * 1e6), + sort: item => item.contention?.asMilliseconds() ?? 0, }, { name: "applicationName", title: insightsTableTitles.applicationName(execType), - cell: (item: TransactionInsightEvent) => item.application, - sort: (item: TransactionInsightEvent) => item.application, + cell: item => item.application, + sort: item => item.application, }, ]; } diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsView.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsView.tsx index d4d860e48843..4849c4a04b8a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/transactionInsights/transactionInsightsView.tsx @@ -29,12 +29,11 @@ import { queryByName, syncHistory } from "src/util/query"; import { getTableSortFromURL } from "src/sortedtable/getTableSortFromURL"; import { TableStatistics } from "src/tableStatistics"; -import { TransactionInsightEventsResponse } from "src/api/insightsApi"; import { filterTransactionInsights, getAppsFromTransactionInsights, - getInsightsFromState, WorkloadInsightEventFilters, + MergedTxnInsightEvent, } from "src/insights"; import { EmptyInsightsTablePlaceholder } from "../util"; import { TransactionInsightsTable } from "./transactionInsightsTable"; @@ -48,7 +47,7 @@ const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); export type TransactionInsightsViewStateProps = { - transactions: TransactionInsightEventsResponse; + transactions: MergedTxnInsightEvent[]; transactionsError: Error | null; filters: WorkloadInsightEventFilters; sortSetting: SortSetting; @@ -177,7 +176,7 @@ export const TransactionInsightsView: React.FC = ( app: "", }); - const transactionInsights = getInsightsFromState(transactions); + const transactionInsights = transactions; const apps = getAppsFromTransactionInsights( transactionInsights, INTERNAL_APP_NAME_PREFIX, diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/detailsLinks.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/detailsLinks.tsx index ba0050d6617b..07743c2de75e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/detailsLinks.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/detailsLinks.tsx @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { StatementInsightEvent } from "../../types"; +import { FlattenedStmtInsightEvent } from "../../types"; import React from "react"; import { HexStringToInt64String } from "../../../util"; import { Link } from "react-router-dom"; @@ -19,26 +19,28 @@ import { Moment } from "moment"; export function TransactionDetailsLink( transactionFingerprintID: string, - startTime: Moment, + startTime: Moment | null, setTimeScale: (tw: TimeScale) => void, ): React.ReactElement { const txnID = HexStringToInt64String(transactionFingerprintID); const path = `/transaction/${txnID}`; - const timeScale: TimeScale = { - windowSize: moment.duration(65, "minutes"), - fixedWindowEnd: moment(startTime).add(1, "hour"), - sampleSize: moment.duration(1, "hour"), - key: "Custom", - }; + const timeScale: TimeScale = startTime + ? { + windowSize: moment.duration(65, "minutes"), + fixedWindowEnd: moment(startTime).add(1, "hour"), + sampleSize: moment.duration(1, "hour"), + key: "Custom", + } + : null; return ( - setTimeScale(timeScale)}> + timeScale && setTimeScale(timeScale)}>
{String(transactionFingerprintID)}
); } export function StatementDetailsLink( - insightDetails: StatementInsightEvent, + insightDetails: FlattenedStmtInsightEvent, setTimeScale: (tw: TimeScale) => void, ): React.ReactElement { const linkProps = { diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/queriesCell.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/queriesCell.tsx index e4aa5778a6b4..ff15a7c8134e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/queriesCell.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/util/queriesCell.tsx @@ -21,10 +21,12 @@ export function QueriesCell( textLimit: number, ): React.ReactElement { if ( - transactionQueries.length < 2 && - transactionQueries[0].length < textLimit + !transactionQueries.length || + (transactionQueries.length === 1 && + transactionQueries[0].length < textLimit) ) { - return
{transactionQueries[0]}
; + const query = transactionQueries?.length ? transactionQueries[0] : ""; + return
{query}
; } const combinedQuery = transactionQueries.map((query, idx, arr) => ( diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/workloadInsightsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/workloadInsightsPageConnected.tsx index 03e225781afb..618ce804cbd3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/workloadInsightsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/workloadInsightsPageConnected.tsx @@ -39,14 +39,9 @@ import { selectFilters, selectSortSetting, } from "src/store/insights/transactionInsights"; -import { bindActionCreators, Dispatch } from "redux"; +import { Dispatch } from "redux"; import { TimeScale } from "../../timeScaleDropdown"; import { actions as sqlStatsActions } from "../../store/sqlStats"; -import { - StatementInsightDetails, - StatementInsightDetailsDispatchProps, - StatementInsightDetailsStateProps, -} from "../workloadInsightDetails"; const transactionMapStateToProps = ( state: AppState, diff --git a/pkg/ui/workspaces/cluster-ui/src/insightsTable/insightsTable.tsx b/pkg/ui/workspaces/cluster-ui/src/insightsTable/insightsTable.tsx index b82676998482..740d7749e619 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insightsTable/insightsTable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insightsTable/insightsTable.tsx @@ -26,6 +26,7 @@ import { Anchor } from "../anchor"; import { Link } from "react-router-dom"; import { performanceTuningRecipes } from "../util"; import { InsightRecommendation, insightType } from "../insights"; +import { limitText } from "src/util"; const cx = classNames.bind(styles); @@ -34,15 +35,17 @@ export class InsightsSortedTable extends SortedTable {} const insightColumnLabels = { insights: "Insights", details: "Details", + query: "Statement", actions: "", }; export type InsightsTableColumnKeys = keyof typeof insightColumnLabels; type InsightsTableTitleType = { - [key in InsightsTableColumnKeys]: () => JSX.Element; + [key in InsightsTableColumnKeys]: () => React.ReactElement; }; export const insightsTableTitles: InsightsTableTitleType = { + query: () => insightColumnLabels.query, insights: () => { return ( item.type, }, + { + name: "query", + title: insightsTableTitles.query(), + cell: (item: InsightRecommendation) => + limitText(item.execution?.statement, 100) ?? "N/A", + sort: (item: InsightRecommendation) => item.execution?.statement, + }, { name: "action", title: insightsTableTitles.actions(), diff --git a/pkg/ui/workspaces/cluster-ui/src/selectors/insightsCommon.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/selectors/insightsCommon.selectors.ts index 5e293d62aed3..1c37381e5d8e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/selectors/insightsCommon.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/selectors/insightsCommon.selectors.ts @@ -8,40 +8,58 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +import { FlattenedStmtInsights } from "src/api/insightsApi"; import { - StatementInsights, - TransactionInsightEventsResponse, -} from "src/api/insightsApi"; -import { - StatementInsightEvent, - populateStatementInsightsFromProblemAndCauses, + mergeTxnInsightDetails, + flattenTxnInsightsToStmts, + FlattenedStmtInsightEvent, + TxnInsightEvent, + TxnContentionInsightEvent, + MergedTxnInsightEvent, + mergeTxnContentionAndStmtInsights, + TxnContentionInsightDetails, + TxnInsightDetails, } from "src/insights"; // The functions in this file are agnostic to the different shape of each // state in db-console and cluster-ui. This file contains selector functions // and combiners that can be reused across both packages. -export const selectStatementInsightsCombiner = ( - statementInsights: StatementInsights, -): StatementInsights => { - if (!statementInsights) return []; - return populateStatementInsightsFromProblemAndCauses(statementInsights); +export const selectFlattenedStmtInsightsCombiner = ( + executionInsights: TxnInsightEvent[], +): FlattenedStmtInsights => { + return flattenTxnInsightsToStmts(executionInsights); }; export const selectStatementInsightDetailsCombiner = ( - statementInsights: StatementInsights, + statementInsights: FlattenedStmtInsights, executionID: string, -): StatementInsightEvent | null => { +): FlattenedStmtInsightEvent | null => { if (!statementInsights) { return null; } - return statementInsights.find(insight => insight.statementID === executionID); + return statementInsights.find( + insight => insight.statementExecutionID === executionID, + ); }; export const selectTxnInsightsCombiner = ( - txnInsights: TransactionInsightEventsResponse, -): TransactionInsightEventsResponse => { - if (!txnInsights) return []; - return txnInsights; + txnInsightsFromStmts: TxnInsightEvent[], + txnContentionInsights: TxnContentionInsightEvent[], +): MergedTxnInsightEvent[] => { + if (!txnInsightsFromStmts && !txnContentionInsights) return []; + + // Merge the two insights lists. + return mergeTxnContentionAndStmtInsights( + txnInsightsFromStmts, + txnContentionInsights, + ); +}; + +export const selectTxnInsightDetailsCombiner = ( + txnInsightsFromStmts: TxnInsightEvent, + txnContentionInsights: TxnContentionInsightDetails, +): TxnInsightDetails => { + return mergeTxnInsightDetails(txnInsightsFromStmts, txnContentionInsights); }; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.reducer.ts index 6d6cc411fa80..d81aad3defe3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.reducer.ts @@ -12,13 +12,11 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { DOMAIN_NAME } from "src/store/utils"; import moment, { Moment } from "moment"; import { ErrorWithKey } from "src/api/statementsApi"; -import { - TransactionInsightEventDetailsRequest, - TransactionInsightEventDetailsResponse, -} from "src/api/insightsApi"; +import { TxnContentionInsightDetailsRequest } from "src/api/insightsApi"; +import { TxnContentionInsightDetails } from "src/insights"; export type TransactionInsightDetailsState = { - data: TransactionInsightEventDetailsResponse | null; + data: TxnContentionInsightDetails | null; lastUpdated: Moment | null; lastError: Error; valid: boolean; @@ -43,11 +41,8 @@ const transactionInsightDetailsSlice = createSlice({ name: `${DOMAIN_NAME}/transactionInsightDetailsSlice`, initialState, reducers: { - received: ( - state, - action: PayloadAction, - ) => { - state.cachedData.set(action.payload.executionID, { + received: (state, action: PayloadAction) => { + state.cachedData.set(action.payload.transactionExecutionID, { data: action.payload, valid: true, lastError: null, @@ -66,11 +61,11 @@ const transactionInsightDetailsSlice = createSlice({ }, refresh: ( _, - _action: PayloadAction, + _action: PayloadAction, ) => {}, request: ( _, - _action: PayloadAction, + _action: PayloadAction, ) => {}, }, }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.sagas.ts index 689fa94899aa..b794c25a0f72 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.sagas.ts @@ -13,20 +13,20 @@ import { all, call, put, takeLatest } from "redux-saga/effects"; import { actions } from "./transactionInsightDetails.reducer"; import { getTransactionInsightEventDetailsState, - TransactionInsightEventDetailsRequest, - TransactionInsightEventDetailsResponse, + TxnContentionInsightDetailsRequest, } from "src/api/insightsApi"; +import { TxnContentionInsightDetails } from "src/insights"; import { PayloadAction } from "@reduxjs/toolkit"; import { ErrorWithKey } from "src/api"; export function* refreshTransactionInsightDetailsSaga( - action: PayloadAction, + action: PayloadAction, ) { yield put(actions.request(action.payload)); } export function* requestTransactionInsightDetailsSaga( - action: PayloadAction, + action: PayloadAction, ): any { try { const result = yield call( @@ -48,9 +48,9 @@ const CACHE_INVALIDATION_PERIOD = 5 * 60 * 1000; // 5 minutes in ms const timeoutsByExecID = new Map(); export function receivedTxnInsightsDetailsSaga( - action: PayloadAction, + action: PayloadAction, ) { - const execID = action.payload.executionID; + const execID = action.payload.transactionExecutionID; clearTimeout(timeoutsByExecID.get(execID)); const id = setTimeout(() => { actions.invalidated({ key: execID }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.selectors.ts index 6eac4468a23d..094423be00f1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.selectors.ts @@ -11,8 +11,10 @@ import { createSelector } from "reselect"; import { AppState } from "src/store/reducers"; import { selectExecutionID } from "src/selectors/common"; +import { selectTxnInsightDetailsCombiner } from "src/selectors/insightsCommon.selectors"; +import { TxnInsightEvent } from "src/insights"; -const selectTransactionInsightDetailsState = createSelector( +const selectTxnContentionInsightsDetails = createSelector( (state: AppState) => state.adminUI.transactionInsightDetails.cachedData, selectExecutionID, (cachedTxnInsightDetails, execId) => { @@ -20,12 +22,22 @@ const selectTransactionInsightDetailsState = createSelector( }, ); +const selectTxnInsightFromExecInsight = createSelector( + (state: AppState) => state.adminUI.executionInsights?.data, + selectExecutionID, + (execInsights, execID): TxnInsightEvent => { + return execInsights.find(txn => txn.transactionExecutionID === execID); + }, +); + export const selectTransactionInsightDetails = createSelector( - selectTransactionInsightDetailsState, - state => state.data, + selectTxnInsightFromExecInsight, + selectTxnContentionInsightsDetails, + (txnInsights, txnContentionInsights) => + selectTxnInsightDetailsCombiner(txnInsights, txnContentionInsights?.data), ); export const selectTransactionInsightDetailsError = createSelector( - selectTransactionInsightDetailsState, - state => state.lastError, + selectTxnContentionInsightsDetails, + state => state?.lastError, ); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.reducer.ts index ca67df47b8a2..500970a3e985 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.reducer.ts @@ -11,16 +11,16 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { DOMAIN_NAME, noopReducer } from "../../utils"; import moment, { Moment } from "moment"; -import { StatementInsights } from "src/api/insightsApi"; +import { TxnInsightEvent } from "src/insights"; -export type StatementInsightsState = { - data: StatementInsights; +export type ExecutionInsightsState = { + data: TxnInsightEvent[]; lastUpdated: Moment; lastError: Error; valid: boolean; }; -const initialState: StatementInsightsState = { +const initialState: ExecutionInsightsState = { data: null, lastUpdated: null, lastError: null, @@ -31,7 +31,7 @@ const statementInsightsSlice = createSlice({ name: `${DOMAIN_NAME}/statementInsightsSlice`, initialState, reducers: { - received: (state, action: PayloadAction) => { + received: (state, action: PayloadAction) => { state.data = action.payload; state.valid = true; state.lastError = null; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.sagas.ts index 7e0e74d2090f..5b4e581c9713 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.sagas.ts @@ -11,7 +11,7 @@ import { all, call, put, takeLatest } from "redux-saga/effects"; import { actions } from "./statementInsights.reducer"; -import { getStatementInsightsApi } from "src/api/insightsApi"; +import { getClusterInsightsApi } from "src/api/insightsApi"; export function* refreshStatementInsightsSaga() { yield put(actions.request()); @@ -19,7 +19,7 @@ export function* refreshStatementInsightsSaga() { export function* requestStatementInsightsSaga(): any { try { - const result = yield call(getStatementInsightsApi); + const result = yield call(getClusterInsightsApi); yield put(actions.received(result)); } catch (e) { yield put(actions.failed(e)); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.selectors.ts index 4178b8dd4f6a..7232a77ebedf 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsights/statementInsights.selectors.ts @@ -13,17 +13,17 @@ import { localStorageSelector } from "src/store/utils/selectors"; import { AppState } from "src/store/reducers"; import { - selectStatementInsightsCombiner, + selectFlattenedStmtInsightsCombiner, selectStatementInsightDetailsCombiner, } from "src/selectors/insightsCommon.selectors"; import { selectExecutionID } from "src/selectors/common"; export const selectStatementInsights = createSelector( - (state: AppState) => state.adminUI.statementInsights?.data, - selectStatementInsightsCombiner, + (state: AppState) => state.adminUI.executionInsights?.data, + selectFlattenedStmtInsightsCombiner, ); export const selectStatementInsightsError = (state: AppState) => - state.adminUI.statementInsights?.lastError; + state.adminUI.executionInsights?.lastError; export const selectStatementInsightDetails = createSelector( selectStatementInsights, diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.reducer.ts index c2a31ecd1cc1..d93a03aefb25 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.reducer.ts @@ -11,10 +11,10 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { DOMAIN_NAME, noopReducer } from "src/store/utils"; import moment, { Moment } from "moment"; -import { TransactionInsightEventsResponse } from "src/api/insightsApi"; +import { TxnContentionInsightEvent } from "src/insights"; export type TransactionInsightsState = { - data: TransactionInsightEventsResponse; + data: TxnContentionInsightEvent[]; lastUpdated: Moment; lastError: Error; valid: boolean; @@ -31,10 +31,7 @@ const transactionInsightsSlice = createSlice({ name: `${DOMAIN_NAME}/transactionInsightsSlice`, initialState, reducers: { - received: ( - state, - action: PayloadAction, - ) => { + received: (state, action: PayloadAction) => { state.data = action.payload; state.valid = true; state.lastError = null; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.sagas.ts index 8df7e52d6a88..e23c8881f02d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.sagas.ts @@ -11,15 +11,17 @@ import { all, call, put, takeEvery } from "redux-saga/effects"; import { actions } from "./transactionInsights.reducer"; -import { getTransactionInsightEventState } from "src/api/insightsApi"; +import { actions as stmtActions } from "../statementInsights/statementInsights.reducer"; +import { getTxnInsightEvents } from "src/api/insightsApi"; export function* refreshTransactionInsightsSaga() { yield put(actions.request()); + yield put(stmtActions.request()); } export function* requestTransactionInsightsSaga(): any { try { - const result = yield call(getTransactionInsightEventState); + const result = yield call(getTxnInsightEvents); yield put(actions.received(result)); } catch (e) { yield put(actions.failed(e)); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.selectors.ts index add64c317bb5..6e09d061831a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsights/transactionInsights.selectors.ts @@ -17,6 +17,7 @@ const selectTransactionInsightsData = (state: AppState) => state.adminUI.transactionInsights.data; export const selectTransactionInsights = createSelector( + (state: AppState) => state.adminUI.executionInsights.data, selectTransactionInsightsData, selectTxnInsightsCombiner, ); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts index 02d629b2f0ce..d90af8377218 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts @@ -8,53 +8,54 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { combineReducers, createStore } from "redux"; import { createAction, createReducer } from "@reduxjs/toolkit"; -import { LocalStorageState, reducer as localStorage } from "./localStorage"; -import { - StatementDiagnosticsState, - reducer as statementDiagnostics, -} from "./statementDiagnostics"; -import { NodesState, reducer as nodes } from "./nodes"; -import { LivenessState, reducer as liveness } from "./liveness"; -import { SessionsState, reducer as sessions } from "./sessions"; -import { - TerminateQueryState, - reducer as terminateQuery, -} from "./terminateQuery"; -import { UIConfigState, reducer as uiConfig } from "./uiConfig"; -import { DOMAIN_NAME } from "./utils"; -import { SQLStatsState, reducer as sqlStats } from "./sqlStats"; +import { combineReducers, createStore } from "redux"; +import { TxnInsightEvent } from "src/insights"; import { - SQLDetailsStatsReducerState, - reducer as sqlDetailsStats, -} from "./statementDetails"; + ClusterLocksReqState, + reducer as clusterLocks, +} from "./clusterLocks/clusterLocks.reducer"; import { IndexStatsReducerState, reducer as indexStats, } from "./indexStats/indexStats.reducer"; -import { JobsState, reducer as jobs } from "./jobs"; -import { JobState, reducer as job } from "./jobDetails"; import { - ClusterLocksReqState, - reducer as clusterLocks, -} from "./clusterLocks/clusterLocks.reducer"; + reducer as transactionInsightDetails, + TransactionInsightDetailsCachedState, +} from "./insightDetails/transactionInsightDetails"; +import { + ExecutionInsightsState, + reducer as executionInsights, +} from "./insights/statementInsights"; import { - TransactionInsightsState, reducer as transactionInsights, + TransactionInsightsState, } from "./insights/transactionInsights"; +import { JobState, reducer as job } from "./jobDetails"; +import { JobsState, reducer as jobs } from "./jobs"; +import { LivenessState, reducer as liveness } from "./liveness"; +import { LocalStorageState, reducer as localStorage } from "./localStorage"; +import { NodesState, reducer as nodes } from "./nodes"; import { - StatementInsightsState, - reducer as statementInsights, -} from "./insights/statementInsights"; -import { - SchemaInsightsState, reducer as schemaInsights, + SchemaInsightsState, } from "./schemaInsights"; +import { reducer as sessions, SessionsState } from "./sessions"; +import { reducer as sqlStats, SQLStatsState } from "./sqlStats"; import { - reducer as transactionInsightDetails, - TransactionInsightDetailsCachedState, -} from "./insightDetails/transactionInsightDetails"; + reducer as sqlDetailsStats, + SQLDetailsStatsReducerState, +} from "./statementDetails"; +import { + reducer as statementDiagnostics, + StatementDiagnosticsState, +} from "./statementDiagnostics"; +import { + reducer as terminateQuery, + TerminateQueryState, +} from "./terminateQuery"; +import { reducer as uiConfig, UIConfigState } from "./uiConfig"; +import { DOMAIN_NAME } from "./utils"; export type AdminUiState = { statementDiagnostics: StatementDiagnosticsState; @@ -72,7 +73,7 @@ export type AdminUiState = { clusterLocks: ClusterLocksReqState; transactionInsights: TransactionInsightsState; transactionInsightDetails: TransactionInsightDetailsCachedState; - statementInsights: StatementInsightsState; + executionInsights: ExecutionInsightsState; schemaInsights: SchemaInsightsState; }; @@ -88,7 +89,7 @@ export const reducers = combineReducers({ sessions, transactionInsights, transactionInsightDetails, - statementInsights, + executionInsights, terminateQuery, uiConfig, sqlStats, diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index be5c8ead1493..1135c6c0eb81 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -12,7 +12,13 @@ import _ from "lodash"; import { Action, combineReducers } from "redux"; import { ThunkAction, ThunkDispatch } from "redux-thunk"; import moment from "moment"; -import { api as clusterUiApi, util } from "@cockroachlabs/cluster-ui"; +import { + api as clusterUiApi, + util, + TxnContentionInsightDetails, + TxnContentionInsightEvent, + TxnInsightEvent, +} from "@cockroachlabs/cluster-ui"; import { CachedDataReducer, CachedDataReducerState, @@ -405,23 +411,36 @@ export const refreshLiveWorkload = (): ThunkAction => { }; const transactionInsightsReducerObj = new CachedDataReducer( - clusterUiApi.getTransactionInsightEventState, + clusterUiApi.getTxnInsightEvents, "transactionInsights", null, moment.duration(5, "m"), ); -export const refreshTransactionInsights = transactionInsightsReducerObj.refresh; +export const refreshTxnContentionInsights = + transactionInsightsReducerObj.refresh; + +export const refreshTransactionInsights = (): ThunkAction< + any, + any, + any, + Action +> => { + return (dispatch: ThunkDispatch) => { + dispatch(refreshTxnContentionInsights()); + dispatch(refreshExecutionInsights()); + }; +}; -const statementInsightsReducerObj = new CachedDataReducer( - clusterUiApi.getStatementInsightsApi, - "statementInsights", +const executionInsightsReducerObj = new CachedDataReducer( + clusterUiApi.getClusterInsightsApi, + "executionInsights", null, moment.duration(5, "m"), ); -export const refreshStatementInsights = statementInsightsReducerObj.refresh; +export const refreshExecutionInsights = executionInsightsReducerObj.refresh; export const transactionInsightRequestKey = ( - req: clusterUiApi.TransactionInsightEventDetailsRequest, + req: clusterUiApi.TxnContentionInsightDetailsRequest, ): string => `${req.id}`; const transactionInsightDetailsReducerObj = new KeyedCachedDataReducer( @@ -431,9 +450,19 @@ const transactionInsightDetailsReducerObj = new KeyedCachedDataReducer( null, moment.duration(5, "m"), ); -export const refreshTransactionInsightDetails = + +const refreshTxnContentionInsightDetails = transactionInsightDetailsReducerObj.refresh; +export const refreshTransactionInsightDetails = ( + req: clusterUiApi.TxnContentionInsightDetailsRequest, +): ThunkAction => { + return (dispatch: ThunkDispatch) => { + dispatch(refreshTxnContentionInsightDetails(req)); + dispatch(refreshExecutionInsights()); + }; +}; + const schemaInsightsReducerObj = new CachedDataReducer( clusterUiApi.getSchemaInsights, "schemaInsights", @@ -519,9 +548,9 @@ export interface APIReducersState { userSQLRoles: CachedDataReducerState; hotRanges: PaginatedCachedDataReducerState; clusterLocks: CachedDataReducerState; - transactionInsights: CachedDataReducerState; - transactionInsightDetails: KeyedCachedDataReducerState; - statementInsights: CachedDataReducerState; + transactionInsights: CachedDataReducerState; + transactionInsightDetails: KeyedCachedDataReducerState; + executionInsights: CachedDataReducerState; schemaInsights: CachedDataReducerState; schedules: KeyedCachedDataReducerState; schedule: KeyedCachedDataReducerState; @@ -572,8 +601,8 @@ export const apiReducersReducer = combineReducers({ transactionInsightsReducerObj.reducer, [transactionInsightDetailsReducerObj.actionNamespace]: transactionInsightDetailsReducerObj.reducer, - [statementInsightsReducerObj.actionNamespace]: - statementInsightsReducerObj.reducer, + [executionInsightsReducerObj.actionNamespace]: + executionInsightsReducerObj.reducer, [schemaInsightsReducerObj.actionNamespace]: schemaInsightsReducerObj.reducer, [schedulesReducerObj.actionNamespace]: schedulesReducerObj.reducer, [scheduleReducerObj.actionNamespace]: scheduleReducerObj.reducer, diff --git a/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts b/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts index 75e9fe4774fb..c19157add7f9 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts +++ b/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts @@ -12,16 +12,17 @@ import { LocalSetting } from "src/redux/localsettings"; import { AdminUIState } from "src/redux/state"; import { createSelector } from "reselect"; import { - api, defaultFilters, WorkloadInsightEventFilters, insightType, SchemaInsightEventFilters, SortSetting, - selectStatementInsightsCombiner, + selectFlattenedStmtInsightsCombiner, selectExecutionID, selectStatementInsightDetailsCombiner, selectTxnInsightsCombiner, + TxnContentionInsightDetails, + selectTxnInsightDetailsCombiner, } from "@cockroachlabs/cluster-ui"; export const filtersLocalSetting = new LocalSetting< @@ -40,16 +41,17 @@ export const sortSettingLocalSetting = new LocalSetting< }); export const selectTransactionInsights = createSelector( + (state: AdminUIState) => state.cachedData.executionInsights?.data, (state: AdminUIState) => state.cachedData.transactionInsights?.data, selectTxnInsightsCombiner, ); -export const selectTransactionInsightDetails = createSelector( +const selectTxnContentionInsightDetails = createSelector( [ (state: AdminUIState) => state.cachedData.transactionInsightDetails, selectExecutionID, ], - (insight, insightId): api.TransactionInsightEventDetailsResponse => { + (insight, insightId): TxnContentionInsightDetails => { if (!insight) { return null; } @@ -57,6 +59,20 @@ export const selectTransactionInsightDetails = createSelector( }, ); +const selectTxnInsightFromExecInsight = createSelector( + (state: AdminUIState) => state.cachedData.executionInsights?.data, + selectExecutionID, + (execInsights, execID) => { + return execInsights?.find(txn => txn.transactionExecutionID === execID); + }, +); + +export const selectTxnInsightDetails = createSelector( + selectTxnInsightFromExecInsight, + selectTxnContentionInsightDetails, + selectTxnInsightDetailsCombiner, +); + export const selectTransactionInsightDetailsError = createSelector( (state: AdminUIState) => state.cachedData.transactionInsightDetails, selectExecutionID, @@ -69,8 +85,8 @@ export const selectTransactionInsightDetailsError = createSelector( ); export const selectStatementInsights = createSelector( - (state: AdminUIState) => state.cachedData.statementInsights?.data, - selectStatementInsightsCombiner, + (state: AdminUIState) => state.cachedData.executionInsights?.data, + selectFlattenedStmtInsightsCombiner, ); export const selectStatementInsightDetails = createSelector( diff --git a/pkg/ui/workspaces/db-console/src/views/insights/statementInsightDetailsPage.tsx b/pkg/ui/workspaces/db-console/src/views/insights/statementInsightDetailsPage.tsx index e753cb689a51..cd379e378126 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/statementInsightDetailsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/insights/statementInsightDetailsPage.tsx @@ -15,7 +15,7 @@ import { import { connect } from "react-redux"; import { RouteComponentProps, withRouter } from "react-router-dom"; import { AdminUIState } from "src/redux/state"; -import { refreshStatementInsights } from "src/redux/apiReducers"; +import { refreshExecutionInsights } from "src/redux/apiReducers"; import { selectStatementInsightDetails } from "src/views/insights/insightsSelectors"; import { setGlobalTimeScaleAction } from "src/redux/statements"; @@ -24,16 +24,16 @@ const mapStateToProps = ( props: RouteComponentProps, ): StatementInsightDetailsStateProps => { const insightStatements = selectStatementInsightDetails(state, props); - const insightError = state.cachedData?.statementInsights.lastError; + const insightError = state.cachedData?.executionInsights?.lastError; return { insightEventDetails: insightStatements, - insightError: insightError, + insightError, }; }; const mapDispatchToProps: StatementInsightDetailsDispatchProps = { setTimeScale: setGlobalTimeScaleAction, - refreshStatementInsights: refreshStatementInsights, + refreshStatementInsights: refreshExecutionInsights, }; const StatementInsightDetailsPage = withRouter( diff --git a/pkg/ui/workspaces/db-console/src/views/insights/transactionInsightDetailsPage.tsx b/pkg/ui/workspaces/db-console/src/views/insights/transactionInsightDetailsPage.tsx index fdd31678f224..079dce96a5f1 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/transactionInsightDetailsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/insights/transactionInsightDetailsPage.tsx @@ -17,7 +17,7 @@ import { RouteComponentProps, withRouter } from "react-router-dom"; import { refreshTransactionInsightDetails } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; import { - selectTransactionInsightDetails, + selectTxnInsightDetails, selectTransactionInsightDetailsError, } from "src/views/insights/insightsSelectors"; import { setGlobalTimeScaleAction } from "src/redux/statements"; @@ -27,7 +27,7 @@ const mapStateToProps = ( props: RouteComponentProps, ): TransactionInsightDetailsStateProps => { return { - insightEventDetails: selectTransactionInsightDetails(state, props), + insightDetails: selectTxnInsightDetails(state, props), insightError: selectTransactionInsightDetailsError(state, props), }; }; diff --git a/pkg/ui/workspaces/db-console/src/views/insights/workloadInsightsPage.tsx b/pkg/ui/workspaces/db-console/src/views/insights/workloadInsightsPage.tsx index e545fffa1df1..955960309d93 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/workloadInsightsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/insights/workloadInsightsPage.tsx @@ -12,7 +12,7 @@ import { connect } from "react-redux"; import { RouteComponentProps, withRouter } from "react-router-dom"; import { refreshTransactionInsights, - refreshStatementInsights, + refreshExecutionInsights, } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; import { @@ -49,7 +49,7 @@ const transactionMapStateToProps = ( _props: RouteComponentProps, ): TransactionInsightsViewStateProps => ({ transactions: selectTransactionInsights(state), - transactionsError: state.cachedData?.transactionInsights.lastError, + transactionsError: state.cachedData?.transactionInsights?.lastError, filters: filtersLocalSetting.selector(state), sortSetting: sortSettingLocalSetting.selector(state), }); @@ -59,7 +59,7 @@ const statementMapStateToProps = ( _props: RouteComponentProps, ): StatementInsightsViewStateProps => ({ statements: selectStatementInsights(state), - statementsError: state.cachedData?.statementInsights.lastError, + statementsError: state.cachedData?.executionInsights?.lastError, filters: filtersLocalSetting.selector(state), sortSetting: sortSettingLocalSetting.selector(state), selectedColumnNames: @@ -78,7 +78,7 @@ const StatementDispatchProps: StatementInsightsViewDispatchProps = { onFiltersChange: (filters: WorkloadInsightEventFilters) => filtersLocalSetting.set(filters), onSortChange: (ss: SortSetting) => sortSettingLocalSetting.set(ss), - refreshStatementInsights: refreshStatementInsights, + refreshStatementInsights: refreshExecutionInsights, onColumnsChange: (value: string[]) => insightStatementColumnsLocalSetting.set(value.join(",")), setTimeScale: setGlobalTimeScaleAction,