From 12ea6145c2b9d859f30197cff64ca994e199c786 Mon Sep 17 00:00:00 2001 From: Eric Harmeling Date: Mon, 21 Nov 2022 10:55:37 -0500 Subject: [PATCH] ui: add time picker to insights pages This commit adds a time picker to the workload insights overview pages. The time picker store is shared across SQL activity and insights components, enabling us to better correlate the insight events with fingerprints on the SQL activity pages by time interval. The start and end values of the time picker (stored in the timeScale/SQLActivity local setting) form the request to the insights "backend", where we use these start and end values to filter queries to the internal insights and contention tables. The "backend" queries select events across the interval, then partition and filter on rank, ordered by descending end time. Part of #83780. Release note (ui change): Added a time picker to the Workload Insights Overview pages in the DB Console. --- .../cluster-ui/src/api/insightsApi.ts | 123 +++++++++++++----- .../detailsPanels/waitTimeInsightsPanel.tsx | 4 +- .../cluster-ui/src/insights/utils.ts | 28 +++- .../statementInsightDetails.tsx | 22 ++-- .../statementInsightDetailsConnected.tsx | 11 +- .../statementInsightDetailsOverviewTab.tsx | 2 +- .../transactionInsightDetails.tsx | 10 +- .../transactionInsightDetailsConnected.tsx | 2 + .../transactionInsightDetailsOverviewTab.tsx | 18 ++- .../statementInsightsTable.tsx | 3 +- .../statementInsightsView.tsx | 56 ++++++-- .../transactionInsightsView.tsx | 48 +++++-- .../workloadInsightsPageConnected.tsx | 29 +++-- .../src/selectors/insightsCommon.selectors.ts | 2 +- .../statementDetails.selectors.ts | 2 +- .../statementDetailsConnected.ts | 2 +- .../statementsPage.selectors.ts | 5 - .../statementsPageConnected.tsx | 2 +- .../transactionInsightDetails.reducer.ts | 33 +++-- .../transactionInsightDetails.sagas.ts | 28 ++-- .../transactionInsightDetails.selectors.ts | 4 +- .../statementInsights.reducer.ts | 25 ++-- .../statementInsights.sagas.ts | 46 ++++++- .../statementInsights.selectors.ts | 11 +- .../transactionInsights.reducer.ts | 23 +++- .../transactionInsights.sagas.ts | 55 ++++++-- .../transactionInsights.selectors.ts | 8 +- .../src/store/sqlStats/sqlStats.sagas.ts | 4 + .../cluster-ui/src/store/utils/selectors.ts | 5 + .../transactionDetailsConnected.tsx | 2 +- .../transactionsPageConnected.tsx | 6 +- .../cluster-ui/src/util/constants.ts | 2 + .../db-console/src/redux/apiReducers.ts | 19 +-- .../src/redux/statements/statementsSagas.ts | 4 + .../src/views/insights/insightsSelectors.ts | 33 +++-- .../insights/statementInsightDetailsPage.tsx | 10 +- .../transactionInsightDetailsPage.tsx | 4 +- .../views/insights/workloadInsightsPage.tsx | 11 +- 38 files changed, 500 insertions(+), 202 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts index d9f4ece19457..93837e8de768 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts @@ -33,12 +33,33 @@ import moment from "moment"; import { INTERNAL_APP_NAME_PREFIX } from "src/recentExecutions/recentStatementUtils"; import { FixFingerprintHexValue } from "../util"; +function getTxnContentionWhereClause( + clause: string, + filters?: QueryFilterFields, +): string { + let whereClause = clause; + if (filters?.start) { + whereClause = + whereClause + ` AND collection_ts >= '${filters.start.toISOString()}'`; + } + if (filters?.end) { + whereClause = + whereClause + + ` AND (collection_ts + contention_duration) <= '${filters.end.toISOString()}'`; + } + return whereClause; +} + // Transaction contention insight events. // txnContentionQuery selects all transaction contention events that are // above the insights latency threshold. -const txnContentionQuery = ` -SELECT * FROM +function txnContentionQuery(filters?: QueryFilterFields) { + const whereClause = getTxnContentionWhereClause( + ` WHERE encode(waiting_txn_fingerprint_id, 'hex') != '0000000000000000'`, + filters, + ); + return `SELECT * FROM ( SELECT waiting_txn_id, @@ -59,12 +80,13 @@ SELECT * FROM max(collection_ts) AS collection_ts, sum(contention_duration) AS total_contention_duration FROM crdb_internal.transaction_contention_events - WHERE encode(waiting_txn_fingerprint_id, 'hex') != '0000000000000000' + ${whereClause} GROUP BY waiting_txn_id, waiting_txn_fingerprint_id ) WHERE total_contention_duration > threshold ) WHERE rank = 1`; +} type TransactionContentionResponseColumns = { waiting_txn_id: string; @@ -195,9 +217,9 @@ const makeInsightsSqlRequest = (queries: string[]): SqlExecutionRequest => ({ * 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[] -> { +export async function getTxnInsightEvents( + req?: ExecutionInsightsRequest, +): Promise { // Note that any errors encountered fetching these results are caught // earlier in the call stack. @@ -205,7 +227,7 @@ export async function getTxnInsightEvents(): Promise< // latency threshold. const contentionResults = await executeInternalSql( - makeInsightsSqlRequest([txnContentionQuery]), + makeInsightsSqlRequest([txnContentionQuery(req)]), ); if (sqlResultsAreEmpty(contentionResults)) { return []; @@ -297,7 +319,7 @@ function buildTxnContentionInsightEvents( // 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. -export type TxnContentionInsightDetailsRequest = { id: string }; +export type TxnContentionInsightDetailsRequest = QueryFilterFields; // Query 1 types, functions. export type TransactionContentionEventDetails = Omit< @@ -306,8 +328,12 @@ export type TransactionContentionEventDetails = Omit< >; // txnContentionDetailsQuery selects information about a specific transaction contention event. -const txnContentionDetailsQuery = (id: string) => ` -SELECT +function txnContentionDetailsQuery(filters: QueryFilterFields) { + const whereClause = getTxnContentionWhereClause( + ` WHERE waiting_txn_id = '${filters.id}'`, + filters, + ); + return `SELECT collection_ts, blocking_txn_id, encode( blocking_txn_fingerprint_id, 'hex' ) AS blocking_txn_fingerprint_id, @@ -326,10 +352,11 @@ FROM FROM [SHOW CLUSTER SETTING sql.insights.latency_threshold] ), crdb_internal.transaction_contention_events AS tce - LEFT OUTER JOIN crdb_internal.ranges AS ranges - ON tce.contending_key BETWEEN ranges.start_key AND ranges.end_key - WHERE waiting_txn_id = '${id}' + LEFT OUTER JOIN crdb_internal.ranges AS ranges + ON tce.contending_key BETWEEN ranges.start_key AND ranges.end_key + ${whereClause} `; +} type TxnContentionDetailsResponseColumns = { waiting_txn_id: string; @@ -428,7 +455,7 @@ export async function getTransactionInsightEventDetailsState( // Get contention results for requested transaction. const contentionResults = await executeInternalSql( - makeInsightsSqlRequest([txnContentionDetailsQuery(req.id)]), + makeInsightsSqlRequest([txnContentionDetailsQuery(req)]), ); if (sqlResultsAreEmpty(contentionResults)) { return; @@ -651,23 +678,40 @@ function organizeExecutionInsightsResponseIntoTxns( } type InsightQuery = { - name: InsightNameEnum; query: string; toState: (response: SqlExecutionResponse) => State; }; -const workloadInsightsQuery: InsightQuery< - ExecutionInsightsResponseRow, - TxnInsightEvent[] -> = { - name: InsightNameEnum.highContention, - // We only surface the most recently observed problem for a given statement. - // Note that we don't filter by problem != 'None', so that we can get all - // stmts in the problematic transaction. - query: ` +export type QueryFilterFields = { + id?: string; + start?: moment.Moment; + end?: moment.Moment; +}; + +function workloadInsightsQuery( + filters?: QueryFilterFields, +): InsightQuery { + let whereClause = ` WHERE app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%'`; + if (filters?.start) { + whereClause = + whereClause + ` AND start_time >= '${filters.start.toISOString()}'`; + } + if (filters?.end) { + whereClause = + whereClause + ` AND end_time <= '${filters.end.toISOString()}'`; + } + return { + // We only surface the most recently observed problem for a given statement. + // Note that we don't filter by problem != 'None', so that we can get all + // stmts in the problematic transaction. + query: ` WITH insightsTable as ( - SELECT * FROM crdb_internal.cluster_execution_insights -) + SELECT + * + FROM + crdb_internal.cluster_execution_insights + ${whereClause} +) SELECT session_id, insights.txn_id as txn_id, @@ -696,23 +740,30 @@ SELECT FROM ( SELECT - txn_id, - 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 DESC ) as rank FROM insightsTable ) as latestTxns -JOIN insightsTable AS insights -ON latestTxns.txn_id = insights.txn_id -WHERE latestTxns.rank = 1 AND app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%' + JOIN insightsTable AS insights + ON latestTxns.txn_id = insights.txn_id +WHERE latestTxns.rank = 1 `, - toState: organizeExecutionInsightsResponseIntoTxns, -}; + toState: organizeExecutionInsightsResponseIntoTxns, + }; +} export type ExecutionInsights = TxnInsightEvent[]; -export function getClusterInsightsApi(): Promise { + +export type ExecutionInsightsRequest = Pick; + +export function getClusterInsightsApi( + req?: ExecutionInsightsRequest, +): Promise { + const insightsQuery = workloadInsightsQuery(req); const request: SqlExecutionRequest = { statements: [ { - sql: workloadInsightsQuery.query, + sql: insightsQuery.query, }, ], execute: true, @@ -721,7 +772,7 @@ export function getClusterInsightsApi(): Promise { }; return executeInternalSql(request).then( result => { - return workloadInsightsQuery.toState(result); + return insightsQuery.toState(result); }, ); } diff --git a/pkg/ui/workspaces/cluster-ui/src/detailsPanels/waitTimeInsightsPanel.tsx b/pkg/ui/workspaces/cluster-ui/src/detailsPanels/waitTimeInsightsPanel.tsx index 7cde11e042a2..0fb1d2ceb5cb 100644 --- a/pkg/ui/workspaces/cluster-ui/src/detailsPanels/waitTimeInsightsPanel.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/detailsPanels/waitTimeInsightsPanel.tsx @@ -16,7 +16,7 @@ import React from "react"; import { SummaryCard, SummaryCardItem } from "src/summaryCard"; import { ContendedExecution, ExecutionType } from "src/recentExecutions"; -import { capitalize, Duration } from "../util"; +import { capitalize, Duration, NO_SAMPLES_FOUND } from "../util"; import { Heading } from "@cockroachlabs/ui-components"; import { ExecutionContentionTable } from "../recentExecutions/recentTransactionsTable/execContentionTable"; @@ -89,7 +89,7 @@ export const WaitTimeInsightsPanel: React.FC = ({ value={ waitTime ? Duration(waitTime.asMilliseconds() * 1e6) - : "no samples" + : NO_SAMPLES_FOUND } /> {schemaName && ( diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts b/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts index bcc730064e2a..52627d177516 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts +++ b/pkg/ui/workspaces/cluster-ui/src/insights/utils.ts @@ -9,7 +9,10 @@ // licenses/APL.txt. import { limitStringArray, unset } from "src/util"; -import { FlattenedStmtInsights } from "src/api/insightsApi"; +import { + ExecutionInsightsRequest, + FlattenedStmtInsights, +} from "src/api/insightsApi"; import { ExecutionDetails, FlattenedStmtInsightEvent, @@ -28,6 +31,7 @@ import { TxnInsightEvent, WorkloadInsightEventFilters, } from "./types"; +import { TimeScale, toDateRange } from "../timeScaleDropdown"; export const filterTransactionInsights = ( transactions: MergedTxnInsightEvent[] | null, @@ -275,7 +279,7 @@ export function getInsightsFromProblemsAndCauses( /** * flattenTxnInsightsToStmts flattens the txn insights array - * into its stmt insights, including the txn level ifnormation. + * into its stmt insights, including the txn level information. * Only stmts with non-empty insights array will be included. * @param txnInsights array of transaction insights * @returns An array of FlattenedStmtInsightEvent where each elem @@ -287,11 +291,18 @@ export function flattenTxnInsightsToStmts( ): FlattenedStmtInsightEvent[] { if (!txnInsights?.length) return []; const stmtInsights: FlattenedStmtInsightEvent[] = []; + const seenExecutions = new Set(); txnInsights.forEach(txnInsight => { const { statementInsights, ...txnInfo } = txnInsight; statementInsights?.forEach(stmt => { - if (!stmt.insights?.length) return; + if ( + !stmt.insights?.length || + seenExecutions.has(stmt.statementExecutionID) + ) { + return; + } stmtInsights.push({ ...txnInfo, ...stmt, query: stmt.query }); + seenExecutions.add(stmt.statementExecutionID); }); }); return stmtInsights; @@ -510,3 +521,14 @@ export function dedupInsights(insights: Insight[]): Insight[] { return deduped; }, []); } + +export function executionInsightsRequestFromTimeScale( + ts: TimeScale, +): ExecutionInsightsRequest { + if (ts === null) return {}; + const [startTime, endTime] = toDateRange(ts); + return { + start: startTime, + end: endTime, + }; +} 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 b632df9d3382..ab6471c0044e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetails.tsx @@ -21,14 +21,15 @@ import { SqlBox, SqlBoxSize } from "src/sql"; import { getMatchParamByName, idAttr } from "src/util"; import { FlattenedStmtInsightEvent } from "../types"; import { InsightsError } from "../insightsErrorComponent"; -import classNames from "classnames/bind"; - -import { commonStyles } from "src/common"; import { getExplainPlanFromGist } from "src/api/decodePlanGistApi"; import { StatementInsightDetailsOverviewTab } from "./statementInsightDetailsOverviewTab"; +import { ExecutionInsightsRequest } from "../../api"; +import { executionInsightsRequestFromTimeScale } from "../utils"; import { TimeScale } from "../../timeScaleDropdown"; // Styles +import classNames from "classnames/bind"; +import { commonStyles } from "src/common"; import insightsDetailsStyles from "src/insights/workloadInsightDetails/insightsDetails.module.scss"; import LoadingError from "../../sqlActivity/errorComponent"; @@ -42,11 +43,12 @@ export interface StatementInsightDetailsStateProps { insightEventDetails: FlattenedStmtInsightEvent; insightError: Error | null; isTenant?: boolean; + timeScale?: TimeScale; } export interface StatementInsightDetailsDispatchProps { + refreshStatementInsights: (req: ExecutionInsightsRequest) => void; setTimeScale: (ts: TimeScale) => void; - refreshStatementInsights: () => void; } export type StatementInsightDetailsProps = StatementInsightDetailsStateProps & @@ -67,6 +69,7 @@ export const StatementInsightDetails: React.FC< insightError, match, isTenant, + timeScale, setTimeScale, refreshStatementInsights, }) => { @@ -101,10 +104,11 @@ export const StatementInsightDetails: React.FC< const executionID = getMatchParamByName(match, idAttr); useEffect(() => { - if (insightEventDetails == null) { - refreshStatementInsights(); + if (!insightEventDetails || insightEventDetails === null) { + const req = executionInsightsRequestFromTimeScale(timeScale); + refreshStatementInsights(req); } - }, [insightEventDetails, refreshStatementInsights]); + }, [insightEventDetails, timeScale, refreshStatementInsights]); return (
@@ -124,8 +128,8 @@ export const StatementInsightDetails: React.FC<
InsightsError()} > diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsConnected.tsx index 4a4268da7953..f7ba0cc3ad40 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsConnected.tsx @@ -19,30 +19,33 @@ import { AppState } from "src/store"; import { actions as statementInsights, selectStatementInsightDetails, - selectStatementInsightsError, + selectExecutionInsightsError, } from "src/store/insights/statementInsights"; import { selectIsTenant } from "src/store/uiConfig"; import { TimeScale } from "../../timeScaleDropdown"; import { actions as sqlStatsActions } from "../../store/sqlStats"; +import { selectTimeScale } from "../../store/utils/selectors"; +import { ExecutionInsightsRequest } from "../../api"; const mapStateToProps = ( state: AppState, props: RouteComponentProps, ): StatementInsightDetailsStateProps => { const insightStatements = selectStatementInsightDetails(state, props); - const insightError = selectStatementInsightsError(state); + const insightError = selectExecutionInsightsError(state); return { insightEventDetails: insightStatements, insightError: insightError, isTenant: selectIsTenant(state), + timeScale: selectTimeScale(state), }; }; const mapDispatchToProps = ( dispatch: Dispatch, ): StatementInsightDetailsDispatchProps => ({ - refreshStatementInsights: () => { - dispatch(statementInsights.refresh()); + refreshStatementInsights: (req: ExecutionInsightsRequest) => { + dispatch(statementInsights.refresh(req)); }, setTimeScale: (ts: TimeScale) => { dispatch( 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 3ba3f27af643..ad8a062dd23c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsOverviewTab.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsOverviewTab.tsx @@ -67,7 +67,7 @@ export const StatementInsightDetailsOverviewTab: React.FC< columnTitle: "duration", }); let contentionTable: JSX.Element = null; - if (insightDetails.contentionEvents != null) { + if (insightDetails?.contentionEvents !== null) { contentionTable = ( diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx index 53abeeefea49..96de511cd06a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetails.tsx @@ -28,9 +28,11 @@ import { TransactionInsightDetailsOverviewTab } from "./transactionInsightDetail import { TransactionInsightsDetailsStmtsTab } from "./transactionInsightDetailsStmtsTab"; import "antd/lib/tabs/style"; +import { executionInsightsRequestFromTimeScale } from "../utils"; export interface TransactionInsightDetailsStateProps { insightDetails: TxnInsightDetails; insightError: Error | null; + timeScale?: TimeScale; } export interface TransactionInsightDetailsDispatchProps { @@ -58,18 +60,22 @@ export const TransactionInsightDetails: React.FC< history, insightDetails, insightError, + timeScale, match, }) => { const executionID = getMatchParamByName(match, idAttr); const noInsights = !insightDetails; useEffect(() => { + const execReq = executionInsightsRequestFromTimeScale(timeScale); if (noInsights) { // Only refresh if we have no data (e.g. refresh the page) refreshTransactionInsightDetails({ id: executionID, + start: execReq.start, + end: execReq.end, }); } - }, [executionID, refreshTransactionInsightDetails, noInsights]); + }, [executionID, refreshTransactionInsightDetails, noInsights, timeScale]); const prevPage = (): void => history.goBack(); @@ -95,7 +101,7 @@ export const TransactionInsightDetails: React.FC<
InsightsError()} 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 20c1047595fb..02e3b1f2e5b8 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsConnected.tsx @@ -24,6 +24,7 @@ import { TimeScale } from "../../timeScaleDropdown"; import { actions as sqlStatsActions } from "../../store/sqlStats"; import { Dispatch } from "redux"; import { TxnContentionInsightDetailsRequest } from "src/api"; +import { selectTimeScale } from "../../store/utils/selectors"; const mapStateToProps = ( state: AppState, @@ -34,6 +35,7 @@ const mapStateToProps = ( return { insightDetails: insightDetails, insightError: insightError, + timeScale: selectTimeScale(state), }; }; diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx index 6754c14f0c4e..2fd42ab483fa 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/transactionInsightDetailsOverviewTab.tsx @@ -18,6 +18,7 @@ import { SummaryCard, SummaryCardItem } from "src/summaryCard"; import { DATE_WITH_SECONDS_AND_MILLISECONDS_FORMAT_24_UTC } from "src/util/format"; import { WaitTimeInsightsLabels } from "src/detailsPanels/waitTimeInsightsPanel"; import { TxnContentionInsightDetailsRequest } from "src/api"; +import { NO_SAMPLES_FOUND } from "src/util"; import { InsightsSortedTable, makeInsightsColumns, @@ -44,9 +45,6 @@ export interface TransactionInsightDetailsStateProps { } export interface TransactionInsightDetailsDispatchProps { - refreshTransactionInsightDetails: ( - req: TxnContentionInsightDetailsRequest, - ) => void; setTimeScale: (ts: TimeScale) => void; } @@ -102,10 +100,10 @@ export const TransactionInsightDetailsOverviewTab: React.FC = ({ const rowsRead = stmtInsights?.reduce((count, stmt) => (count += stmt.rowsRead), 0) ?? - "no samples"; + NO_SAMPLES_FOUND; const rowsWritten = stmtInsights?.reduce((count, stmt) => (count += stmt.rowsWritten), 0) ?? - "no samples"; + NO_SAMPLES_FOUND; return (
@@ -125,21 +123,21 @@ export const TransactionInsightDetailsOverviewTab: React.FC = ({ value={ insightDetails.startTime?.format( DATE_WITH_SECONDS_AND_MILLISECONDS_FORMAT_24_UTC, - ) ?? "no samples" + ) ?? NO_SAMPLES_FOUND } /> stmt.isFullScan) - ?.toString() ?? "no samples" + ?.toString() ?? NO_SAMPLES_FOUND } /> @@ -148,7 +146,7 @@ export const TransactionInsightDetailsOverviewTab: React.FC = ({ {insightDetails.lastRetryReason && ( = ({ )} !item.totalContentionTime - ? "no samples" + ? NO_SAMPLES_FOUND : Duration(item.totalContentionTime.asMilliseconds() * 1e6), sort: (item: FlattenedStmtInsightEvent) => item.totalContentionTime?.asMilliseconds() ?? -1, 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 f442d2db1c86..c444e2f4736a 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,8 +30,12 @@ import { getTableSortFromURL } from "src/sortedtable/getTableSortFromURL"; import { TableStatistics } from "src/tableStatistics"; import { isSelectedColumn } from "src/columnsSelector/utils"; -import { FlattenedStmtInsights } from "src/api/insightsApi"; import { + ExecutionInsightsRequest, + FlattenedStmtInsights, +} from "src/api/insightsApi"; +import { + executionInsightsRequestFromTimeScale, filterStatementInsights, getAppsFromStatementInsights, makeStatementInsightsColumns, @@ -40,12 +44,17 @@ import { import { EmptyInsightsTablePlaceholder } from "../util"; import { StatementInsightsTable } from "./statementInsightsTable"; import { InsightsError } from "../../insightsErrorComponent"; +import ColumnsSelector from "../../../columnsSelector/columnsSelector"; +import { SelectOption } from "../../../multiSelectCheckbox/multiSelectCheckbox"; +import { + defaultTimeScaleOptions, + TimeScale, + TimeScaleDropdown, +} from "../../../timeScaleDropdown"; import styles from "src/statementsPage/statementsPage.module.scss"; import sortableTableStyles from "src/sortedtable/sortedtable.module.scss"; -import ColumnsSelector from "../../../columnsSelector/columnsSelector"; -import { SelectOption } from "../../../multiSelectCheckbox/multiSelectCheckbox"; -import { TimeScale } from "../../../timeScaleDropdown"; +import { commonStyles } from "../../../common"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -56,13 +65,15 @@ export type StatementInsightsViewStateProps = { filters: WorkloadInsightEventFilters; sortSetting: SortSetting; selectedColumnNames: string[]; + isLoading?: boolean; dropDownSelect?: React.ReactElement; + timeScale?: TimeScale; }; export type StatementInsightsViewDispatchProps = { onFiltersChange: (filters: WorkloadInsightEventFilters) => void; onSortChange: (ss: SortSetting) => void; - refreshStatementInsights: () => void; + refreshStatementInsights: (req: ExecutionInsightsRequest) => void; onColumnsChange: (selectedColumns: string[]) => void; setTimeScale: (ts: TimeScale) => void; }; @@ -81,6 +92,8 @@ export const StatementInsightsView: React.FC = ( statements, statementsError, filters, + timeScale, + isLoading, refreshStatementInsights, onFiltersChange, onSortChange, @@ -100,13 +113,23 @@ export const StatementInsightsView: React.FC = ( ); useEffect(() => { - // Refresh every 10 seconds. - refreshStatementInsights(); - const interval = setInterval(refreshStatementInsights, 10 * 1000); - return () => { - clearInterval(interval); - }; - }, [refreshStatementInsights]); + if (timeScale.key !== "Custom") { + const req = executionInsightsRequestFromTimeScale(timeScale); + refreshStatementInsights(req); + // Refresh every 10 seconds except when on custom timeScale. + const interval = setInterval(refreshStatementInsights, 10 * 1000, req); + return () => { + clearInterval(interval); + }; + } + }, [timeScale, refreshStatementInsights]); + + useEffect(() => { + if (statements === null || statements.length < 1) { + const req = executionInsightsRequestFromTimeScale(timeScale); + refreshStatementInsights(req); + } + }, [statements, timeScale, refreshStatementInsights]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -232,10 +255,17 @@ export const StatementInsightsView: React.FC = ( filters={filters} /> + + +
InsightsError()} 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 4849c4a04b8a..14b047277c44 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 @@ -34,14 +34,21 @@ import { getAppsFromTransactionInsights, WorkloadInsightEventFilters, MergedTxnInsightEvent, + executionInsightsRequestFromTimeScale, } from "src/insights"; import { EmptyInsightsTablePlaceholder } from "../util"; import { TransactionInsightsTable } from "./transactionInsightsTable"; import { InsightsError } from "../../insightsErrorComponent"; +import { + TimeScale, + defaultTimeScaleOptions, + TimeScaleDropdown, +} from "../../../timeScaleDropdown"; +import { ExecutionInsightsRequest } from "src/api"; import styles from "src/statementsPage/statementsPage.module.scss"; import sortableTableStyles from "src/sortedtable/sortedtable.module.scss"; -import { TimeScale } from "../../../timeScaleDropdown"; +import { commonStyles } from "../../../common"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -51,13 +58,15 @@ export type TransactionInsightsViewStateProps = { transactionsError: Error | null; filters: WorkloadInsightEventFilters; sortSetting: SortSetting; + isLoading?: boolean; dropDownSelect?: React.ReactElement; + timeScale?: TimeScale; }; export type TransactionInsightsViewDispatchProps = { onFiltersChange: (filters: WorkloadInsightEventFilters) => void; onSortChange: (ss: SortSetting) => void; - refreshTransactionInsights: () => void; + refreshTransactionInsights: (req: ExecutionInsightsRequest) => void; setTimeScale: (ts: TimeScale) => void; }; @@ -75,6 +84,8 @@ export const TransactionInsightsView: React.FC = ( transactions, transactionsError, filters, + timeScale, + isLoading, refreshTransactionInsights, onFiltersChange, onSortChange, @@ -92,13 +103,23 @@ export const TransactionInsightsView: React.FC = ( ); useEffect(() => { - // Refresh every 20 seconds. - refreshTransactionInsights(); - const interval = setInterval(refreshTransactionInsights, 20 * 1000); - return () => { - clearInterval(interval); - }; - }, [refreshTransactionInsights]); + if (timeScale.key !== "Custom") { + const req = executionInsightsRequestFromTimeScale(timeScale); + refreshTransactionInsights(req); + // Refresh every 10 seconds. + const interval = setInterval(refreshTransactionInsights, 10 * 1000, req); + return () => { + clearInterval(interval); + }; + } + }, [timeScale, refreshTransactionInsights]); + + useEffect(() => { + if (transactions === null || transactions.length < 1) { + const req = executionInsightsRequestFromTimeScale(timeScale); + refreshTransactionInsights(req); + } + }, [transactions, timeScale, refreshTransactionInsights]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -209,10 +230,17 @@ export const TransactionInsightsView: React.FC = ( filters={filters} /> + + +
InsightsError()} 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 618ce804cbd3..9adc51e9b01b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/workloadInsightsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/workloadInsightsPageConnected.tsx @@ -29,8 +29,9 @@ import { SortSetting } from "src/sortedtable"; import { actions as statementInsights, selectColumns, - selectStatementInsights, - selectStatementInsightsError, + selectExecutionInsights, + selectExecutionInsightsError, + selectExecutionInsightsLoading, } from "src/store/insights/statementInsights"; import { actions as transactionInsights, @@ -38,10 +39,12 @@ import { selectTransactionInsightsError, selectFilters, selectSortSetting, + selectTransactionInsightsLoading, } from "src/store/insights/transactionInsights"; import { Dispatch } from "redux"; import { TimeScale } from "../../timeScaleDropdown"; -import { actions as sqlStatsActions } from "../../store/sqlStats"; +import { ExecutionInsightsRequest } from "../../api"; +import { selectTimeScale } from "../../store/utils/selectors"; const transactionMapStateToProps = ( state: AppState, @@ -51,17 +54,21 @@ const transactionMapStateToProps = ( transactionsError: selectTransactionInsightsError(state), filters: selectFilters(state), sortSetting: selectSortSetting(state), + timeScale: selectTimeScale(state), + isLoading: selectTransactionInsightsLoading(state), }); const statementMapStateToProps = ( state: AppState, _props: RouteComponentProps, ): StatementInsightsViewStateProps => ({ - statements: selectStatementInsights(state), - statementsError: selectStatementInsightsError(state), + statements: selectExecutionInsights(state), + statementsError: selectExecutionInsightsError(state), filters: selectFilters(state), sortSetting: selectSortSetting(state), selectedColumnNames: selectColumns(state), + timeScale: selectTimeScale(state), + isLoading: selectExecutionInsightsLoading(state), }); const TransactionDispatchProps = ( @@ -83,13 +90,13 @@ const TransactionDispatchProps = ( ), setTimeScale: (ts: TimeScale) => { dispatch( - sqlStatsActions.updateTimeScale({ + transactionInsights.updateTimeScale({ ts: ts, }), ); }, - refreshTransactionInsights: () => { - dispatch(transactionInsights.refresh()); + refreshTransactionInsights: (req: ExecutionInsightsRequest) => { + dispatch(transactionInsights.refresh(req)); }, }); @@ -123,13 +130,13 @@ const StatementDispatchProps = ( ), setTimeScale: (ts: TimeScale) => { dispatch( - sqlStatsActions.updateTimeScale({ + statementInsights.updateTimeScale({ ts: ts, }), ); }, - refreshStatementInsights: () => { - dispatch(statementInsights.refresh()); + refreshStatementInsights: (req: ExecutionInsightsRequest) => { + dispatch(statementInsights.refresh(req)); }, }); 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 1c37381e5d8e..3c76eef6a47f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/selectors/insightsCommon.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/selectors/insightsCommon.selectors.ts @@ -35,7 +35,7 @@ export const selectStatementInsightDetailsCombiner = ( statementInsights: FlattenedStmtInsights, executionID: string, ): FlattenedStmtInsightEvent | null => { - if (!statementInsights) { + if (!statementInsights || statementInsights?.length < 1 || !executionID) { return null; } diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts index 7fbfb2fd46a0..3319e9223fd3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts @@ -21,7 +21,7 @@ import { } from "../util"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { TimeScale, toRoundedDateRange } from "../timeScaleDropdown"; -import { selectTimeScale } from "../statementsPage/statementsPage.selectors"; +import { selectTimeScale } from "../store/utils/selectors"; import moment from "moment"; type StatementDetailsResponseMessage = diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts index 2766986062c2..53364eb2a834 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts @@ -36,7 +36,7 @@ import { actions as analyticsActions } from "src/store/analytics"; import { actions as localStorageActions } from "src/store/localStorage"; import { actions as nodesActions } from "../store/nodes"; import { actions as nodeLivenessActions } from "../store/liveness"; -import { selectTimeScale } from "../statementsPage/statementsPage.selectors"; +import { selectTimeScale } from "../store/utils/selectors"; import { InsertStmtDiagnosticRequest, StatementDetailsRequest, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts index 814f0be340c3..dc4e2c881b21 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -222,11 +222,6 @@ export const selectColumns = createSelector( : null, ); -export const selectTimeScale = createSelector( - localStorageSelector, - localStorage => localStorage["timeScale/SQLActivity"], -); - export const selectSortSetting = createSelector( localStorageSelector, localStorage => localStorage["sortSetting/StatementsPage"], diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index a07861394b38..77633e821ca6 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -31,12 +31,12 @@ import { selectStatementsLastError, selectTotalFingerprints, selectColumns, - selectTimeScale, selectSortSetting, selectFilters, selectSearch, selectStatementsLastUpdated, } from "./statementsPage.selectors"; +import { selectTimeScale } from "../store/utils/selectors"; import { selectIsTenant, selectHasViewActivityRedactedRole, 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 d81aad3defe3..8cfc7144af5f 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 @@ -30,11 +30,13 @@ const txnInitialState: TransactionInsightDetailsState = { }; export type TransactionInsightDetailsCachedState = { - cachedData: Map; + cachedData: { + [id: string]: TransactionInsightDetailsState; + }; }; const initialState: TransactionInsightDetailsCachedState = { - cachedData: new Map(), + cachedData: {}, }; const transactionInsightDetailsSlice = createSlice({ @@ -42,22 +44,25 @@ const transactionInsightDetailsSlice = createSlice({ initialState, reducers: { received: (state, action: PayloadAction) => { - state.cachedData.set(action.payload.transactionExecutionID, { - data: action.payload, - valid: true, - lastError: null, - lastUpdated: moment.utc(), - }); + if (action?.payload?.transactionExecutionID) { + state.cachedData[action.payload.transactionExecutionID] = { + data: action.payload, + valid: true, + lastError: null, + lastUpdated: moment.utc(), + }; + } }, failed: (state, action: PayloadAction) => { - const txnInsight = - state.cachedData.get(action.payload.key) ?? txnInitialState; - txnInsight.valid = false; - txnInsight.lastError = action.payload.err; - state.cachedData.set(action.payload.key, txnInsight); + state.cachedData[action.payload.key] = { + data: null, + valid: false, + lastError: action?.payload?.err, + lastUpdated: null, + }; }, invalidated: (state, action: PayloadAction<{ key: string }>) => { - state.cachedData.delete(action.payload.key); + delete state.cachedData[action.payload.key]; }, refresh: ( _, 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 b794c25a0f72..a95f1644b4d5 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 @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { all, call, put, takeLatest } from "redux-saga/effects"; +import { all, call, put, takeLatest, takeEvery } from "redux-saga/effects"; import { actions } from "./transactionInsightDetails.reducer"; import { @@ -18,11 +18,18 @@ import { import { TxnContentionInsightDetails } from "src/insights"; import { PayloadAction } from "@reduxjs/toolkit"; import { ErrorWithKey } from "src/api"; +import { actions as stmtInsightActions } from "../../insights/statementInsights"; export function* refreshTransactionInsightDetailsSaga( action: PayloadAction, ) { yield put(actions.request(action.payload)); + yield put( + stmtInsightActions.request({ + start: action.payload.start, + end: action.payload.end, + }), + ); } export function* requestTransactionInsightDetailsSaga( @@ -50,18 +57,21 @@ const timeoutsByExecID = new Map(); export function receivedTxnInsightsDetailsSaga( action: PayloadAction, ) { - const execID = action.payload.transactionExecutionID; - clearTimeout(timeoutsByExecID.get(execID)); - const id = setTimeout(() => { - actions.invalidated({ key: execID }); - timeoutsByExecID.delete(execID); - }, CACHE_INVALIDATION_PERIOD); - timeoutsByExecID.set(execID, id); + if (action?.payload?.transactionExecutionID) { + const execID = action.payload.transactionExecutionID; + clearTimeout(timeoutsByExecID.get(execID)); + const id = setTimeout(() => { + actions.invalidated({ key: execID }); + stmtInsightActions.invalidated(); + timeoutsByExecID.delete(execID); + }, CACHE_INVALIDATION_PERIOD); + timeoutsByExecID.set(execID, id); + } } export function* transactionInsightDetailsSaga() { yield all([ - takeLatest(actions.refresh, refreshTransactionInsightDetailsSaga), + takeEvery(actions.refresh, refreshTransactionInsightDetailsSaga), takeLatest(actions.request, requestTransactionInsightDetailsSaga), takeLatest(actions.received, receivedTxnInsightsDetailsSaga), ]); 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 11a7d50f8725..5df987d3b448 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 @@ -18,7 +18,7 @@ const selectTxnContentionInsightsDetails = createSelector( (state: AppState) => state.adminUI.transactionInsightDetails.cachedData, selectID, (cachedTxnInsightDetails, execId) => { - return cachedTxnInsightDetails.get(execId); + return cachedTxnInsightDetails[execId]; }, ); @@ -26,7 +26,7 @@ const selectTxnInsightFromExecInsight = createSelector( (state: AppState) => state.adminUI.executionInsights?.data, selectID, (execInsights, execID): TxnInsightEvent => { - return execInsights.find(txn => txn.transactionExecutionID === execID); + return execInsights?.find(txn => txn.transactionExecutionID === execID); }, ); 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 500970a3e985..4c2c2de96864 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 @@ -9,22 +9,23 @@ // licenses/APL.txt. import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { DOMAIN_NAME, noopReducer } from "../../utils"; -import moment, { Moment } from "moment"; +import { DOMAIN_NAME } from "../../utils"; import { TxnInsightEvent } from "src/insights"; +import { ExecutionInsightsRequest } from "../../../api"; +import { UpdateTimeScalePayload } from "../../sqlStats"; export type ExecutionInsightsState = { data: TxnInsightEvent[]; - lastUpdated: Moment; lastError: Error; valid: boolean; + inFlight: boolean; }; const initialState: ExecutionInsightsState = { data: null, - lastUpdated: null, lastError: null, - valid: true, + valid: false, + inFlight: false, }; const statementInsightsSlice = createSlice({ @@ -35,18 +36,24 @@ const statementInsightsSlice = createSlice({ state.data = action.payload; state.valid = true; state.lastError = null; - state.lastUpdated = moment.utc(); + state.inFlight = false; }, failed: (state, action: PayloadAction) => { state.valid = false; state.lastError = action.payload; + state.inFlight = false; }, invalidated: state => { state.valid = false; }, - // Define actions that don't change state. - refresh: noopReducer, - request: noopReducer, + refresh: (_, _action: PayloadAction) => {}, + request: (_, _action: PayloadAction) => {}, + updateTimeScale: ( + state, + _action: PayloadAction, + ) => { + state.inFlight = true; + }, }, }); 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 5b4e581c9713..54387f784b01 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,24 +11,60 @@ import { all, call, put, takeLatest } from "redux-saga/effects"; import { actions } from "./statementInsights.reducer"; -import { getClusterInsightsApi } from "src/api/insightsApi"; +import { actions as txnInsightActions } from "../transactionInsights"; +import { + ExecutionInsightsRequest, + getClusterInsightsApi, +} from "src/api/insightsApi"; +import { PayloadAction } from "@reduxjs/toolkit"; +import { + UpdateTimeScalePayload, + actions as sqlStatsActions, +} from "../../sqlStats"; +import { actions as localStorageActions } from "../../localStorage"; +import { executionInsightsRequestFromTimeScale } from "../../../insights"; -export function* refreshStatementInsightsSaga() { - yield put(actions.request()); +export function* refreshStatementInsightsSaga( + action?: PayloadAction, +) { + yield put(actions.request(action?.payload)); } -export function* requestStatementInsightsSaga(): any { +export function* requestStatementInsightsSaga( + action?: PayloadAction, +): any { try { - const result = yield call(getClusterInsightsApi); + const result = yield call(getClusterInsightsApi, action?.payload); yield put(actions.received(result)); } catch (e) { yield put(actions.failed(e)); } } +export function* updateSQLStatsTimeScaleSaga( + action: PayloadAction, +) { + const { ts } = action.payload; + yield put( + localStorageActions.update({ + key: "timeScale/SQLActivity", + value: ts, + }), + ); + const req = executionInsightsRequestFromTimeScale(ts); + yield put(actions.invalidated()); + yield put(txnInsightActions.invalidated()); + yield put(sqlStatsActions.invalidated()); + yield put(actions.refresh(req)); +} + export function* statementInsightsSaga() { yield all([ takeLatest(actions.refresh, refreshStatementInsightsSaga), takeLatest(actions.request, requestStatementInsightsSaga), + takeLatest( + [actions.updateTimeScale, txnInsightActions.updateTimeScale], + updateSQLStatsTimeScaleSaga, + ), ]); } 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 2addc77bf133..dbaae343c2fc 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 @@ -17,16 +17,17 @@ import { selectStatementInsightDetailsCombiner, } from "src/selectors/insightsCommon.selectors"; import { selectID } from "src/selectors/common"; -export const selectStatementInsights = createSelector( + +export const selectExecutionInsights = createSelector( (state: AppState) => state.adminUI.executionInsights?.data, selectFlattenedStmtInsightsCombiner, ); -export const selectStatementInsightsError = (state: AppState) => +export const selectExecutionInsightsError = (state: AppState) => state.adminUI.executionInsights?.lastError; export const selectStatementInsightDetails = createSelector( - selectStatementInsights, + selectExecutionInsights, selectID, selectStatementInsightDetailsCombiner, ); @@ -38,3 +39,7 @@ export const selectColumns = createSelector( ? localStorage["showColumns/StatementInsightsPage"].split(",") : null, ); + +export const selectExecutionInsightsLoading = (state: AppState) => + !state.adminUI.executionInsights?.valid || + state.adminUI.executionInsights?.inFlight; 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 d93a03aefb25..a290a90cf47d 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 @@ -9,22 +9,24 @@ // licenses/APL.txt. import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { DOMAIN_NAME, noopReducer } from "src/store/utils"; +import { DOMAIN_NAME } from "src/store/utils"; import moment, { Moment } from "moment"; import { TxnContentionInsightEvent } from "src/insights"; +import { ExecutionInsightsRequest } from "../../../api"; +import { UpdateTimeScalePayload } from "../../sqlStats"; export type TransactionInsightsState = { data: TxnContentionInsightEvent[]; - lastUpdated: Moment; lastError: Error; valid: boolean; + inFlight: boolean; }; const initialState: TransactionInsightsState = { data: null, - lastUpdated: null, lastError: null, - valid: true, + valid: false, + inFlight: false, }; const transactionInsightsSlice = createSlice({ @@ -35,17 +37,24 @@ const transactionInsightsSlice = createSlice({ state.data = action.payload; state.valid = true; state.lastError = null; - state.lastUpdated = moment.utc(); + state.inFlight = false; }, failed: (state, action: PayloadAction) => { state.valid = false; state.lastError = action.payload; + state.inFlight = false; }, invalidated: state => { state.valid = false; }, - refresh: noopReducer, - request: noopReducer, + refresh: (_, _action: PayloadAction) => {}, + request: (_, _action: PayloadAction) => {}, + updateTimeScale: ( + state, + _action: PayloadAction, + ) => { + state.inFlight = true; + }, }, }); 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 e23c8881f02d..57a456252143 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 @@ -8,29 +8,64 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import { all, call, put, takeEvery } from "redux-saga/effects"; +import { all, call, put, takeLatest } from "redux-saga/effects"; import { actions } from "./transactionInsights.reducer"; -import { actions as stmtActions } from "../statementInsights/statementInsights.reducer"; -import { getTxnInsightEvents } from "src/api/insightsApi"; +import { actions as stmtInsightActions } from "../statementInsights"; +import { + ExecutionInsightsRequest, + getTxnInsightEvents, +} from "src/api/insightsApi"; +import { PayloadAction } from "@reduxjs/toolkit"; +import { + UpdateTimeScalePayload, + actions as sqlStatsActions, +} from "../../sqlStats"; +import { actions as localStorageActions } from "../../localStorage"; +import { executionInsightsRequestFromTimeScale } from "../../../insights"; -export function* refreshTransactionInsightsSaga() { - yield put(actions.request()); - yield put(stmtActions.request()); +export function* refreshTransactionInsightsSaga( + action?: PayloadAction, +) { + yield put(actions.request(action?.payload)); + yield put(stmtInsightActions.request(action.payload)); } -export function* requestTransactionInsightsSaga(): any { +export function* requestTransactionInsightsSaga( + action?: PayloadAction, +): any { try { - const result = yield call(getTxnInsightEvents); + const result = yield call(getTxnInsightEvents, action?.payload); yield put(actions.received(result)); } catch (e) { yield put(actions.failed(e)); } } +export function* updateSQLStatsTimeScaleSaga( + action: PayloadAction, +) { + const { ts } = action.payload; + yield put( + localStorageActions.update({ + key: "timeScale/SQLActivity", + value: ts, + }), + ); + const req = executionInsightsRequestFromTimeScale(ts); + yield put(actions.invalidated()); + yield put(stmtInsightActions.invalidated()); + yield put(sqlStatsActions.invalidated()); + yield put(actions.refresh(req)); +} + export function* transactionInsightsSaga() { yield all([ - takeEvery(actions.refresh, refreshTransactionInsightsSaga), - takeEvery(actions.request, requestTransactionInsightsSaga), + takeLatest(actions.refresh, refreshTransactionInsightsSaga), + takeLatest(actions.request, requestTransactionInsightsSaga), + takeLatest( + [actions.updateTimeScale, stmtInsightActions.updateTimeScale], + updateSQLStatsTimeScaleSaga, + ), ]); } 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 6e09d061831a..be07c04d2e65 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 @@ -14,10 +14,10 @@ import { selectTxnInsightsCombiner } from "src/selectors/insightsCommon.selector import { localStorageSelector } from "src/store/utils/selectors"; const selectTransactionInsightsData = (state: AppState) => - state.adminUI.transactionInsights.data; + state.adminUI.transactionInsights?.data; export const selectTransactionInsights = createSelector( - (state: AppState) => state.adminUI.executionInsights.data, + (state: AppState) => state.adminUI.executionInsights?.data, selectTransactionInsightsData, selectTxnInsightsCombiner, ); @@ -34,3 +34,7 @@ export const selectFilters = createSelector( localStorageSelector, localStorage => localStorage["filters/InsightsPage"], ); + +export const selectTransactionInsightsLoading = (state: AppState) => + !state.adminUI.transactionInsights?.valid || + state.adminUI.transactionInsights?.inFlight; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts index 6b1514167f00..af777f95958a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts @@ -24,6 +24,8 @@ import { } from "./sqlStats.reducer"; import { actions as sqlDetailsStatsActions } from "../statementDetails/statementDetails.reducer"; import { toRoundedDateRange } from "../../timeScaleDropdown"; +import { actions as stmtInsightActions } from "../insights/statementInsights"; +import { actions as txnInsightActions } from "../insights/transactionInsights"; export function* refreshSQLStatsSaga(action: PayloadAction) { yield put(sqlStatsActions.request(action.payload)); @@ -57,6 +59,8 @@ export function* updateSQLStatsTimeScaleSaga( end: Long.fromNumber(end.unix()), }); yield put(sqlStatsActions.invalidated()); + yield put(stmtInsightActions.invalidated()); + yield put(txnInsightActions.invalidated()); yield put(sqlStatsActions.refresh(req)); } diff --git a/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts b/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts index f12c84bcbf53..32a2a10ce243 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/utils/selectors.ts @@ -20,3 +20,8 @@ export const localStorageSelector = createSelector( adminUISelector, adminUiState => adminUiState.localStorage, ); + +export const selectTimeScale = createSelector( + localStorageSelector, + localStorage => localStorage["timeScale/SQLActivity"], +); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx index b7971a50f8e4..b6a7b54fd473 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionDetails/transactionDetailsConnected.tsx @@ -31,7 +31,7 @@ import { selectHasViewActivityRedactedRole, } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { selectTimeScale } from "src/statementsPage/statementsPage.selectors"; +import { selectTimeScale } from "../store/utils/selectors"; import { StatementsRequest } from "src/api/statementsApi"; import { txnFingerprintIdAttr, getMatchParamByName } from "../util"; import { TimeScale } from "../timeScaleDropdown"; diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx index a3ad9b42579a..3eadf2cc7f7e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx @@ -29,10 +29,8 @@ import { } from "./transactionsPage.selectors"; import { selectIsTenant } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { - selectTimeScale, - selectStatementsLastUpdated, -} from "src/statementsPage/statementsPage.selectors"; +import { selectStatementsLastUpdated } from "src/statementsPage/statementsPage.selectors"; +import { selectTimeScale } from "../store/utils/selectors"; import { StatementsRequest } from "src/api/statementsApi"; import { actions as localStorageActions } from "../store/localStorage"; import { Filters } from "../queryFilter"; diff --git a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts index 3140a76b9d77..0012128c0e72 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/constants.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/constants.ts @@ -44,3 +44,5 @@ export const serverToClientErrorMessageMap = new Map([ REMOTE_DEBUGGING_ERROR_TEXT, ], ]); + +export const NO_SAMPLES_FOUND = "no samples"; diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index 31a8ae0f2421..80c851b70da3 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -420,17 +420,16 @@ const transactionInsightsReducerObj = new CachedDataReducer( export const refreshTxnContentionInsights = transactionInsightsReducerObj.refresh; -export const refreshTransactionInsights = (): ThunkAction< - any, - any, - any, - Action -> => { +export const refreshTransactionInsights = ( + req?: clusterUiApi.ExecutionInsightsRequest, +): ThunkAction => { return (dispatch: ThunkDispatch) => { - dispatch(refreshTxnContentionInsights()); - dispatch(refreshExecutionInsights()); + dispatch(refreshTxnContentionInsights(req)); + dispatch(refreshExecutionInsights(req)); }; }; +export const invalidateTransactionInsights = + transactionInsightsReducerObj.invalidateData; const executionInsightsReducerObj = new CachedDataReducer( clusterUiApi.getClusterInsightsApi, @@ -439,6 +438,8 @@ const executionInsightsReducerObj = new CachedDataReducer( moment.duration(5, "m"), ); export const refreshExecutionInsights = executionInsightsReducerObj.refresh; +export const invalidateExecutionInsights = + executionInsightsReducerObj.invalidateData; export const transactionInsightRequestKey = ( req: clusterUiApi.TxnContentionInsightDetailsRequest, @@ -460,7 +461,7 @@ export const refreshTransactionInsightDetails = ( ): ThunkAction => { return (dispatch: ThunkDispatch) => { dispatch(refreshTxnContentionInsightDetails(req)); - dispatch(refreshExecutionInsights()); + dispatch(refreshExecutionInsights({ start: req.start, end: req.end })); }; }; diff --git a/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts b/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts index dd3f4e79200d..69084f214fcb 100644 --- a/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts +++ b/pkg/ui/workspaces/db-console/src/redux/statements/statementsSagas.ts @@ -27,6 +27,8 @@ import { refreshStatementDiagnosticsRequests, invalidateStatements, refreshStatements, + invalidateExecutionInsights, + invalidateTransactionInsights, } from "src/redux/apiReducers"; import { createStatementDiagnosticsAlertLocalSetting, @@ -133,6 +135,8 @@ export function* setCombinedStatementsTimeScaleSaga( end: Long.fromNumber(end.unix()), }); yield put(invalidateStatements()); + yield put(invalidateExecutionInsights()); + yield put(invalidateTransactionInsights()); yield put(refreshStatements(req) as any); } 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 830140f72080..976ae8c78687 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts +++ b/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts @@ -41,17 +41,29 @@ export const sortSettingLocalSetting = new LocalSetting< }); export const selectTransactionInsights = createSelector( - (state: AdminUIState) => state.cachedData.executionInsights?.data, - (state: AdminUIState) => state.cachedData.transactionInsights?.data, + (state: AdminUIState) => { + if (state.cachedData.executionInsights?.valid) { + return state.cachedData.executionInsights?.data; + } else return null; + }, + (state: AdminUIState) => { + if (state.cachedData.transactionInsights?.valid) { + return state.cachedData.transactionInsights?.data; + } else return null; + }, selectTxnInsightsCombiner, ); +export const selectTransactionInsightsLoading = (state: AdminUIState) => + !state.cachedData.transactionInsights?.valid && + state.cachedData.transactionInsights?.inFlight; + const selectTxnContentionInsightDetails = createSelector( [ (state: AdminUIState) => state.cachedData.transactionInsightDetails, selectID, ], - (insight, insightId): TxnContentionInsightDetails => { + (insight, insightId: string): TxnContentionInsightDetails => { if (!insight) { return null; } @@ -84,13 +96,18 @@ export const selectTransactionInsightDetailsError = createSelector( }, ); -export const selectStatementInsights = createSelector( - (state: AdminUIState) => state.cachedData.executionInsights?.data, - selectFlattenedStmtInsightsCombiner, -); +export const selectExecutionInsightsLoading = (state: AdminUIState) => + !state.cachedData.executionInsights?.valid && + state.cachedData.executionInsights?.inFlight; + +export const selectExecutionInsights = createSelector((state: AdminUIState) => { + if (state.cachedData?.executionInsights?.valid) { + return state.cachedData?.executionInsights?.data; + } else return null; +}, selectFlattenedStmtInsightsCombiner); export const selectStatementInsightDetails = createSelector( - selectStatementInsights, + selectExecutionInsights, selectID, selectStatementInsightDetailsCombiner, ); 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 cd379e378126..8a57e67ab5aa 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/statementInsightDetailsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/insights/statementInsightDetailsPage.tsx @@ -18,22 +18,22 @@ import { AdminUIState } from "src/redux/state"; import { refreshExecutionInsights } from "src/redux/apiReducers"; import { selectStatementInsightDetails } from "src/views/insights/insightsSelectors"; import { setGlobalTimeScaleAction } from "src/redux/statements"; +import { selectTimeScale } from "src/redux/timeScale"; const mapStateToProps = ( state: AdminUIState, props: RouteComponentProps, ): StatementInsightDetailsStateProps => { - const insightStatements = selectStatementInsightDetails(state, props); - const insightError = state.cachedData?.executionInsights?.lastError; return { - insightEventDetails: insightStatements, - insightError, + insightEventDetails: selectStatementInsightDetails(state, props), + insightError: state.cachedData?.executionInsights?.lastError, + timeScale: selectTimeScale(state), }; }; const mapDispatchToProps: StatementInsightDetailsDispatchProps = { - setTimeScale: setGlobalTimeScaleAction, refreshStatementInsights: refreshExecutionInsights, + setTimeScale: setGlobalTimeScaleAction, }; 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 079dce96a5f1..28569a6a3c61 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/transactionInsightDetailsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/insights/transactionInsightDetailsPage.tsx @@ -21,6 +21,7 @@ import { selectTransactionInsightDetailsError, } from "src/views/insights/insightsSelectors"; import { setGlobalTimeScaleAction } from "src/redux/statements"; +import { selectTimeScale } from "src/redux/timeScale"; const mapStateToProps = ( state: AdminUIState, @@ -29,11 +30,12 @@ const mapStateToProps = ( return { insightDetails: selectTxnInsightDetails(state, props), insightError: selectTransactionInsightDetailsError(state, props), + timeScale: selectTimeScale(state), }; }; const mapDispatchToProps: TransactionInsightDetailsDispatchProps = { - refreshTransactionInsightDetails, + refreshTransactionInsightDetails: refreshTransactionInsightDetails, setTimeScale: setGlobalTimeScaleAction, }; 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 955960309d93..3a0e3b132026 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/workloadInsightsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/insights/workloadInsightsPage.tsx @@ -27,13 +27,16 @@ import { } from "@cockroachlabs/cluster-ui"; import { filtersLocalSetting, - selectStatementInsights, + selectExecutionInsights, sortSettingLocalSetting, selectTransactionInsights, + selectExecutionInsightsLoading, + selectTransactionInsightsLoading, } from "src/views/insights/insightsSelectors"; import { bindActionCreators } from "redux"; import { LocalSetting } from "src/redux/localsettings"; import { setGlobalTimeScaleAction } from "src/redux/statements"; +import { selectTimeScale } from "src/redux/timeScale"; export const insightStatementColumnsLocalSetting = new LocalSetting< AdminUIState, @@ -52,18 +55,22 @@ const transactionMapStateToProps = ( transactionsError: state.cachedData?.transactionInsights?.lastError, filters: filtersLocalSetting.selector(state), sortSetting: sortSettingLocalSetting.selector(state), + timeScale: selectTimeScale(state), + isLoading: selectTransactionInsightsLoading(state), }); const statementMapStateToProps = ( state: AdminUIState, _props: RouteComponentProps, ): StatementInsightsViewStateProps => ({ - statements: selectStatementInsights(state), + statements: selectExecutionInsights(state), statementsError: state.cachedData?.executionInsights?.lastError, filters: filtersLocalSetting.selector(state), sortSetting: sortSettingLocalSetting.selector(state), selectedColumnNames: insightStatementColumnsLocalSetting.selectorToArray(state), + timeScale: selectTimeScale(state), + isLoading: selectExecutionInsightsLoading(state), }); const TransactionDispatchProps = {