From 9b2571f9f3eda48878b3f57814d331fbc57f881f 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 | 94 +++++++++++++------ .../detailsPanels/waitTimeInsightsPanel.tsx | 4 +- .../cluster-ui/src/insights/utils.ts | 27 +++++- .../statementInsightDetailsConnected.tsx | 6 +- .../transactionInsightDetailsOverviewTab.tsx | 15 +-- .../statementInsightsTable.tsx | 3 +- .../statementInsightsView.tsx | 45 ++++++--- .../transactionInsightsView.tsx | 41 ++++++-- .../workloadInsightsPageConnected.tsx | 29 +++--- .../src/selectors/insightsCommon.selectors.ts | 2 +- .../statementDetails.selectors.ts | 2 +- .../statementDetailsConnected.ts | 2 +- .../statementsPage.selectors.ts | 5 - .../statementsPageConnected.tsx | 2 +- .../transactionInsightDetails.selectors.ts | 2 +- .../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 | 13 +-- .../src/views/insights/insightsSelectors.ts | 12 ++- .../views/insights/workloadInsightsPage.tsx | 11 ++- 29 files changed, 362 insertions(+), 140 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..9573c146e877 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts @@ -37,8 +37,18 @@ import { FixFingerprintHexValue } from "../util"; // txnContentionQuery selects all transaction contention events that are // above the insights latency threshold. -const txnContentionQuery = ` -SELECT * FROM +function txnContentionQuery(filters?: QueryFilterFields) { + let whereClause = ` WHERE encode(waiting_txn_fingerprint_id, 'hex') != '0000000000000000'`; + 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 `SELECT * FROM ( SELECT waiting_txn_id, @@ -59,12 +69,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 +206,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 +216,7 @@ export async function getTxnInsightEvents(): Promise< // latency threshold. const contentionResults = await executeInternalSql( - makeInsightsSqlRequest([txnContentionQuery]), + makeInsightsSqlRequest([txnContentionQuery(req)]), ); if (sqlResultsAreEmpty(contentionResults)) { return []; @@ -651,23 +662,41 @@ 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 = { + stmtFingerprintId?: string; + txnFingerprintId?: 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 +725,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 +757,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..862ca23b5ee6 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_DATA_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_DATA_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..2f26987f02ec 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,13 @@ export function dedupInsights(insights: Insight[]): Insight[] { return deduped; }, []); } + +export function executionInsightsRequestFromTimeScale( + ts: TimeScale, +): ExecutionInsightsRequest { + const [startTime, endTime] = toDateRange(ts); + return { + start: startTime, + end: endTime, + }; +} 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..a1e3d748f7b4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsightDetails/statementInsightDetailsConnected.tsx @@ -19,7 +19,7 @@ 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"; @@ -30,7 +30,7 @@ const mapStateToProps = ( props: RouteComponentProps, ): StatementInsightDetailsStateProps => { const insightStatements = selectStatementInsightDetails(state, props); - const insightError = selectStatementInsightsError(state); + const insightError = selectExecutionInsightsError(state); return { insightEventDetails: insightStatements, insightError: insightError, @@ -42,7 +42,7 @@ const mapDispatchToProps = ( dispatch: Dispatch, ): StatementInsightDetailsDispatchProps => ({ refreshStatementInsights: () => { - dispatch(statementInsights.refresh()); + dispatch(statementInsights.refresh({})); }, setTimeScale: (ts: TimeScale) => { dispatch( 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..0a515a25eef0 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_DATA_FOUND } from "src/util"; import { InsightsSortedTable, makeInsightsColumns, @@ -102,10 +103,10 @@ export const TransactionInsightDetailsOverviewTab: React.FC = ({ const rowsRead = stmtInsights?.reduce((count, stmt) => (count += stmt.rowsRead), 0) ?? - "no samples"; + NO_DATA_FOUND; const rowsWritten = stmtInsights?.reduce((count, stmt) => (count += stmt.rowsWritten), 0) ?? - "no samples"; + NO_DATA_FOUND; return (
@@ -125,21 +126,21 @@ export const TransactionInsightDetailsOverviewTab: React.FC = ({ value={ insightDetails.startTime?.format( DATE_WITH_SECONDS_AND_MILLISECONDS_FORMAT_24_UTC, - ) ?? "no samples" + ) ?? NO_DATA_FOUND } /> stmt.isFullScan) - ?.toString() ?? "no samples" + ?.toString() ?? NO_DATA_FOUND } /> @@ -148,7 +149,7 @@ export const TransactionInsightDetailsOverviewTab: React.FC = ({ {insightDetails.lastRetryReason && ( = ({ )} !item.totalContentionTime - ? "no samples" + ? NO_DATA_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..005a414bb417 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, @@ -45,7 +49,12 @@ 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 { + defaultTimeScaleOptions, + TimeScale, + TimeScaleDropdown, +} 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,16 @@ 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); + }; + } + }, [refreshStatementInsights, timeScale]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -232,10 +248,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..3d93379c3817 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,16 @@ 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); + }; + } + }, [refreshTransactionInsights, timeScale]); useEffect(() => { // We use this effect to sync settings defined on the URL (sort, filters), @@ -209,10 +223,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..558b5290c6b2 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 || !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 c1345c64fbb8..26aa12d6c433 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"; type StatementDetailsResponseMessage = cockroach.server.serverpb.StatementDetailsResponse; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts index 10826a12a1b4..66223867ff8b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts @@ -39,7 +39,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 c42d81fa0663..499b6ff99da2 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -231,11 +231,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 45c5a377f70b..feae4747faaa 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -30,12 +30,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.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/store/insightDetails/transactionInsightDetails/transactionInsightDetails.selectors.ts index 11a7d50f8725..9342c9466e37 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 @@ -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 5d7bf675d2b0..5ff6d266c5f5 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..6640c730a0a7 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_DATA_FOUND = "no data"; diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index 24b09864c79d..409632144613 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -419,15 +419,12 @@ 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)); }; }; 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..cb0afb7f7514 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts +++ b/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts @@ -46,6 +46,10 @@ export const selectTransactionInsights = createSelector( selectTxnInsightsCombiner, ); +export const selectTransactionInsightsLoading = (state: AdminUIState) => + !state.cachedData.transactionInsights?.valid && + state.cachedData.transactionInsights?.inFlight; + const selectTxnContentionInsightDetails = createSelector( [ (state: AdminUIState) => state.cachedData.transactionInsightDetails, @@ -84,13 +88,17 @@ export const selectTransactionInsightDetailsError = createSelector( }, ); -export const selectStatementInsights = createSelector( +export const selectExecutionInsightsLoading = (state: AdminUIState) => + !state.cachedData.executionInsights?.valid && + state.cachedData.executionInsights?.inFlight; + +export const selectExecutionInsights = createSelector( (state: AdminUIState) => state.cachedData.executionInsights?.data, selectFlattenedStmtInsightsCombiner, ); export const selectStatementInsightDetails = createSelector( - selectStatementInsights, + selectExecutionInsights, selectID, selectStatementInsightDetailsCombiner, ); 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 = {