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