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 = {