diff --git a/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx b/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx index dc8056f937f8..d7edf00cd72d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx @@ -16,15 +16,14 @@ const cx = classNames.bind(styles); interface SQLActivityErrorProps { statsType: string; + timeout?: boolean; } const SQLActivityError: React.FC = props => { + const error = props.timeout ? "a timeout" : "an unexpected error"; return (
- - This page had an unexpected error while loading - {" " + props.statsType}. - + {`This page had ${error} while loading ${props.statsType}.`}   longToInt(item.stats.count), + cell: (item: PlanHashStats) => Count(longToInt(item.stats.count)), sort: (item: PlanHashStats) => longToInt(item.stats.count), }, { 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 8b192c5aa8f8..991b39f06fb9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.selectors.ts @@ -39,6 +39,7 @@ export const selectStatementDetails = createSelector( ): { statementDetails: StatementDetailsResponseMessage; isLoading: boolean; + lastError: Error; } => { // Since the aggregation interval is 1h, we want to round the selected timeScale to include // the full hour. If a timeScale is between 14:32 - 15:17 we want to search for values @@ -56,9 +57,10 @@ export const selectStatementDetails = createSelector( return { statementDetails: statementDetailsStatsData[key].data, isLoading: statementDetailsStatsData[key].inFlight, + lastError: statementDetailsStatsData[key].lastError, }; } - return { statementDetails: null, isLoading: true }; + return { statementDetails: null, isLoading: true, lastError: null }; }, ); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx index c4db1d722f34..68cecd7ce5be 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetails.tsx @@ -14,7 +14,7 @@ import { cockroach, google } from "@cockroachlabs/crdb-protobuf-client"; import { Text, InlineAlert } from "@cockroachlabs/ui-components"; import { ArrowLeft } from "@cockroachlabs/icons"; import { Location } from "history"; -import _ from "lodash"; +import _, { isNil } from "lodash"; import Long from "long"; import { Helmet } from "react-helmet"; import { Link, RouteComponentProps } from "react-router-dom"; @@ -34,7 +34,7 @@ import { TimestampToMoment, DATE_FORMAT_24_UTC, } from "src/util"; -import { Loading } from "src/loading"; +import { getValidErrorsList, Loading } from "src/loading"; import { Button } from "src/button"; import { SqlBox, SqlBoxSize } from "src/sql"; import { SortSetting } from "src/sortedtable"; @@ -68,6 +68,8 @@ import { generateRowsProcessedTimeseries, generateContentionTimeseries, } from "./timeseriesUtils"; +import { Delayed } from "../delayed"; +import moment from "moment"; type IDuration = google.protobuf.IDuration; type StatementDetailsResponse = cockroach.server.serverpb.StatementDetailsResponse; type IStatementDiagnosticsReport = cockroach.server.serverpb.IStatementDiagnosticsReport; @@ -358,6 +360,22 @@ export class StatementDetails extends React.Component< onDiagnosticsModalOpen, } = this.props; const app = queryByName(this.props.location, appAttr); + const longLoadingMessage = this.props.isLoading && + isNil(this.props.statementDetails) && + isNil(getValidErrorsList(this.props.statementsError)) && ( + + + + ); + + const hasTimeout = this.props.statementsError?.name + ?.toLowerCase() + .includes("timeout"); + const error = hasTimeout ? null : this.props.statementsError; + return (
@@ -380,7 +398,7 @@ export class StatementDetails extends React.Component< SQLActivityError({ @@ -388,6 +406,7 @@ export class StatementDetails extends React.Component< }) } /> + {longLoadingMessage} { const { currentTab } = this.state; - const { stats } = this.props.statementDetails.statement; - const hasData = Number(stats.count) > 0; + const hasTimeout = this.props.statementsError?.name + ?.toLowerCase() + .includes("timeout"); + const hasData = + Number(this.props.statementDetails?.statement?.stats?.count) > 0; const period = timeScaleToString(this.props.timeScale); return ( @@ -413,10 +435,10 @@ export class StatementDetails extends React.Component< activeKey={currentTab} > - {this.renderOverviewTabContent(hasData, period)} + {this.renderOverviewTabContent(hasTimeout, hasData, period)} - {this.renderExplainPlanTabContent(hasData, period)} + {this.renderExplainPlanTabContent(hasTimeout, hasData, period)} {!this.props.isTenant && !this.props.hasViewActivityRedactedRole && ( ); - renderNoDataWithTimeScaleAndSqlBoxTabContent = (): React.ReactElement => ( + renderNoDataWithTimeScaleAndSqlBoxTabContent = ( + hasTimeout: boolean, + ): React.ReactElement => ( <> @@ -462,20 +486,32 @@ export class StatementDetails extends React.Component< )} - + {hasTimeout && ( + + )} + {!hasTimeout && ( + + )} ); renderOverviewTabContent = ( + hasTimeout: boolean, hasData: boolean, period: string, ): React.ReactElement => { if (!hasData) { - return this.renderNoDataWithTimeScaleAndSqlBoxTabContent(); + return this.renderNoDataWithTimeScaleAndSqlBoxTabContent(hasTimeout); } const { nodeRegions, isTenant } = this.props; const { stats } = this.props.statementDetails.statement; @@ -713,11 +749,12 @@ export class StatementDetails extends React.Component< }; renderExplainPlanTabContent = ( + hasTimeout: boolean, hasData: boolean, period: string, ): React.ReactElement => { if (!hasData) { - return this.renderNoDataWithTimeScaleAndSqlBoxTabContent(); + return this.renderNoDataWithTimeScaleAndSqlBoxTabContent(hasTimeout); } const { statement_statistics_per_plan_hash } = this.props.statementDetails; const { formatted_query } = this.props.statementDetails.statement.metadata; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts index cc51ddb000ff..a8b2b3a904e5 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/statementDetailsConnected.ts @@ -56,14 +56,17 @@ const CancelStatementDiagnosticsReportRequest = // For tenant cases, we don't show information about node, regions and // diagnostics. const mapStateToProps = (state: AppState, props: RouteComponentProps) => { - const { statementDetails, isLoading } = selectStatementDetails(state, props); + const { statementDetails, isLoading, lastError } = selectStatementDetails( + state, + props, + ); return { statementFingerprintID: getMatchParamByName(props.match, statementAttr), statementDetails, isLoading: isLoading, latestQuery: state.adminUI.sqlDetailsStats.latestQuery, latestFormattedQuery: state.adminUI.sqlDetailsStats.latestFormattedQuery, - statementsError: state.adminUI.sqlStats.lastError, + statementsError: lastError, timeScale: selectTimeScale(state), nodeNames: selectIsTenant(state) ? {} : nodeDisplayNameByIDSelector(state), nodeRegions: selectIsTenant(state) ? {} : nodeRegionsByIDSelector(state), diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index 92f690577c38..39274b960db4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -702,6 +702,9 @@ export class StatementsPage extends React.Component< renderError={() => SQLActivityError({ statsType: "statements", + timeout: this.props.statementsError?.name + ?.toLowerCase() + .includes("timeout"), }) } /> diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx index 9459f00de3ce..6ac4e9859ee4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -540,6 +540,9 @@ export class TransactionsPage extends React.Component< renderError={() => SQLActivityError({ statsType: "transactions", + timeout: this.props?.error?.name + ?.toLowerCase() + .includes("timeout"), }) } /> diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index 8f9820410669..63e70c77f387 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -333,6 +333,7 @@ export const statementDetailsReducerObj = new KeyedCachedDataReducer( statementDetailsActionNamespace, statementDetailsRequestToID, moment.duration(5, "m"), + moment.duration(30, "m"), ); export const invalidateStatementDetails = diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx b/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx index 9b98b51457cf..b77426ce5454 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementDetails.tsx @@ -75,6 +75,7 @@ export const selectStatementDetails = createSelector( ): { statementDetails: StatementDetailsResponseMessage; isLoading: boolean; + lastError: Error; } => { // Since the aggregation interval is 1h, we want to round the selected timeScale to include // the full hour. If a timeScale is between 14:32 - 15:17 we want to search for values @@ -92,9 +93,10 @@ export const selectStatementDetails = createSelector( return { statementDetails: statementDetailsStats[key].data, isLoading: statementDetailsStats[key].inFlight, + lastError: statementDetailsStats[key].lastError, }; } - return { statementDetails: null, isLoading: true }; + return { statementDetails: null, isLoading: true, lastError: null }; }, ); @@ -102,7 +104,10 @@ const mapStateToProps = ( state: AdminUIState, props: RouteComponentProps, ): StatementDetailsStateProps => { - const { statementDetails, isLoading } = selectStatementDetails(state, props); + const { statementDetails, isLoading, lastError } = selectStatementDetails( + state, + props, + ); return { statementFingerprintID: getMatchParamByName(props.match, statementAttr), statementDetails, @@ -110,7 +115,7 @@ const mapStateToProps = ( latestQuery: state.sqlActivity.statementDetailsLatestQuery, latestFormattedQuery: state.sqlActivity.statementDetailsLatestFormattedQuery, - statementsError: state.cachedData.statements.lastError, + statementsError: lastError, timeScale: selectTimeScale(state), nodeNames: nodeDisplayNameByIDSelector(state), nodeRegions: nodeRegionsByIDSelector(state),