diff --git a/pkg/sql/planner.go b/pkg/sql/planner.go index e42ddc31ec45..6bd0b345192c 100644 --- a/pkg/sql/planner.go +++ b/pkg/sql/planner.go @@ -962,7 +962,7 @@ func (p *planner) GetDetailsForSpanStats( return p.QueryIteratorEx( ctx, "crdb_internal.database_span_stats", - sessiondata.NoSessionDataOverride, + sessiondata.NodeUserSessionDataOverride, query, args..., ) diff --git a/pkg/ui/workspaces/cluster-ui/src/api/databaseDetailsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/databaseDetailsApi.ts index 722b3973a23a..9444ce642c2f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/databaseDetailsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/databaseDetailsApi.ts @@ -9,6 +9,7 @@ // licenses/APL.txt. import { + combineQueryErrors, createSqlExecutionRequest, executeInternalSql, formatApiResult, @@ -17,6 +18,7 @@ import { SqlApiResponse, SqlExecutionErrorMessage, SqlExecutionRequest, + sqlResultsAreEmpty, SqlStatement, SqlTxnResult, txnResultIsEmpty, @@ -100,7 +102,7 @@ const getDatabaseId: DatabaseDetailsQuery = { } }, handleMaxSizeError: (_dbName, _response, _dbDetail) => { - return new Promise(() => false); + return Promise.resolve(false); }, }; @@ -138,12 +140,12 @@ const getDatabaseGrantsQuery: DatabaseDetailsQuery = { } }, handleMaxSizeError: (_dbName, _response, _dbDetail) => { - return new Promise(() => false); + return Promise.resolve(false); }, }; // Database Tables -type DatabaseTablesResponse = { +export type DatabaseTablesResponse = { tables: string[]; }; @@ -281,11 +283,11 @@ const getDatabaseZoneConfig: DatabaseDetailsQuery = { } } if (txn_result.error) { - resp.idResp.error = txn_result.error; + resp.zoneConfigResp.error = txn_result.error; } }, handleMaxSizeError: (_dbName, _response, _dbDetail) => { - return new Promise(() => false); + return Promise.resolve(false); }, }; @@ -296,7 +298,7 @@ type DatabaseDetailsStats = { indexStats: SqlApiQueryResponse; }; -type DatabaseSpanStatsRow = { +export type DatabaseSpanStatsRow = { approximate_disk_bytes: number; live_bytes: number; total_bytes: number; @@ -338,7 +340,7 @@ const getDatabaseSpanStats: DatabaseDetailsQuery = { } }, handleMaxSizeError: (_dbName, _response, _dbDetail) => { - return new Promise(() => false); + return Promise.resolve(false); }, }; @@ -383,7 +385,7 @@ const getDatabaseReplicasAndRegions: DatabaseDetailsQuery { - return new Promise(() => false); + return Promise.resolve(false); }, }; @@ -428,7 +430,7 @@ const getDatabaseIndexUsageStats: DatabaseDetailsQuery = { } }, handleMaxSizeError: (_dbName, _response, _dbDetail) => { - return new Promise(() => false); + return Promise.resolve(false); }, }; @@ -467,12 +469,15 @@ const databaseDetailQueries: DatabaseDetailsQuery[] = [ export function createDatabaseDetailsReq( params: DatabaseDetailsReqParams, ): SqlExecutionRequest { - return createSqlExecutionRequest( - params.database, - databaseDetailQueries.map(query => - query.createStmt(params.database, params.csIndexUnusedDuration), + return { + ...createSqlExecutionRequest( + params.database, + databaseDetailQueries.map(query => + query.createStmt(params.database, params.csIndexUnusedDuration), + ), ), - ); + separate_txns: true, + }; } export async function getDatabaseDetails( @@ -488,23 +493,28 @@ async function fetchDatabaseDetails( const detailsResponse: DatabaseDetailsResponse = newDatabaseDetailsResponse(); const req: SqlExecutionRequest = createDatabaseDetailsReq(params); const resp = await executeInternalSql(req); + const errs: Error[] = []; resp.execution.txn_results.forEach(txn_result => { - if (txn_result.rows) { - const query: DatabaseDetailsQuery = - databaseDetailQueries[txn_result.statement - 1]; - query.addToDatabaseDetail(txn_result, detailsResponse); + if (txn_result.error) { + errs.push(txn_result.error); } + const query: DatabaseDetailsQuery = + databaseDetailQueries[txn_result.statement - 1]; + query.addToDatabaseDetail(txn_result, detailsResponse); }); if (resp.error) { - if (resp.error.message.includes("max result size exceeded")) { + if (isMaxSizeError(resp.error.message)) { return fetchSeparatelyDatabaseDetails(params); } detailsResponse.error = resp.error; } + + detailsResponse.error = combineQueryErrors(errs, detailsResponse.error); return formatApiResult( detailsResponse, detailsResponse.error, - "retrieving database details information", + `retrieving database details information for database '${params.database}'`, + false, ); } @@ -512,6 +522,7 @@ async function fetchSeparatelyDatabaseDetails( params: DatabaseDetailsReqParams, ): Promise> { const detailsResponse: DatabaseDetailsResponse = newDatabaseDetailsResponse(); + const errs: Error[] = []; for (const databaseDetailQuery of databaseDetailQueries) { const req = createSqlExecutionRequest(params.database, [ databaseDetailQuery.createStmt( @@ -520,10 +531,14 @@ async function fetchSeparatelyDatabaseDetails( ), ]); const resp = await executeInternalSql(req); + if (sqlResultsAreEmpty(resp)) { + continue; + } const txn_result = resp.execution.txn_results[0]; - if (txn_result.rows) { - databaseDetailQuery.addToDatabaseDetail(txn_result, detailsResponse); + if (txn_result.error) { + errs.push(txn_result.error); } + databaseDetailQuery.addToDatabaseDetail(txn_result, detailsResponse); if (resp.error) { const handleFailure = await databaseDetailQuery.handleMaxSizeError( @@ -537,9 +552,11 @@ async function fetchSeparatelyDatabaseDetails( } } + detailsResponse.error = combineQueryErrors(errs, detailsResponse.error); return formatApiResult( detailsResponse, detailsResponse.error, - "retrieving database details information", + `retrieving database details information for database '${params.database}'`, + false, ); } diff --git a/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts index 0aa9a5e5b0b8..b837ebd3ec55 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts @@ -42,30 +42,25 @@ export const databasesRequest: SqlExecutionRequest = { // getDatabasesList from cluster-ui will need to pass a timeout argument for // promise timeout handling (callers from db-console already have promise // timeout handling as part of the cacheDataReducer). -export function getDatabasesList( +export async function getDatabasesList( timeout?: moment.Duration, ): Promise { - return withTimeout( + const resp = await withTimeout( executeInternalSql(databasesRequest), timeout, - ).then(result => { - // If there is an error and there are no result throw error. - const noTxnResultsExist = result?.execution?.txn_results?.length === 0; - if ( - result.error && - (noTxnResultsExist || result.execution.txn_results[0].rows.length === 0) - ) { - throw result.error; - } + ); + // Encountered a response level error, or empty response. + if (resp.error || sqlResultsAreEmpty(resp)) { + return { databases: [], error: resp.error }; + } - if (sqlResultsAreEmpty(result)) { - return { databases: [], error: result.error }; - } + // Get database names. + const dbNames: string[] = resp.execution.txn_results[0].rows.map( + row => row.database_name, + ); - const dbNames: string[] = result.execution.txn_results[0].rows.map( - row => row.database_name, - ); - - return { databases: dbNames, error: result.error }; - }); + // Note that we do not surface the txn_result error in the returned payload. + // This request only contains a single txn_result, any error encountered by the txn_result + // will be surfaced at the response level by the sql api. + return { databases: dbNames, error: resp.error }; } diff --git a/pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts index 3a18399bc4b9..86fadc8bc9ba 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts @@ -9,6 +9,7 @@ // licenses/APL.txt. import { fetchDataJSON } from "./fetchData"; +import { getLogger } from "../util"; export type SqlExecutionRequest = { statements: SqlStatement[]; @@ -17,6 +18,7 @@ export type SqlExecutionRequest = { application_name?: string; // Defaults to '$ api-v2-sql' database?: string; // Defaults to system max_result_size?: number; // Default 10kib + separate_txns?: boolean; }; export type SqlStatement = { @@ -165,7 +167,8 @@ export function sqlApiErrorMessage(message: string): string { message = message.replace("run-query-via-api: ", ""); if (message.includes(":")) { - return message.split(":")[1]; + const idx = message.indexOf(":") + 1; + return idx < message.length ? message.substring(idx) : message; } return message; @@ -184,23 +187,43 @@ export function createSqlExecutionRequest( }; } +export function isSeparateTxnError(message: string): boolean { + return !!message?.includes( + "separate transaction payload encountered transaction error", + ); +} + export function isMaxSizeError(message: string): boolean { return !!message?.includes("max result size exceeded"); } +export function isPrivilegeError(code: string): boolean { + return code === "42501"; +} + export function formatApiResult( results: ResultType, error: SqlExecutionErrorMessage, errorMessageContext: string, + shouldThrowOnQueryError = true, ): SqlApiResponse { const maxSizeError = isMaxSizeError(error?.message); if (error && !maxSizeError) { - throw new Error( - `Error while ${errorMessageContext}: ${sqlApiErrorMessage( - error?.message, - )}`, - ); + if (shouldThrowOnQueryError) { + throw new Error( + `Error while ${errorMessageContext}: ${sqlApiErrorMessage( + error?.message, + )}`, + ); + } else { + // Otherwise, just log. + getLogger().warn( + `Error while ${errorMessageContext}: ${sqlApiErrorMessage( + error?.message, + )}`, + ); + } } return { @@ -209,6 +232,24 @@ export function formatApiResult( }; } +export function combineQueryErrors( + errs: Error[], + sqlError?: SqlExecutionErrorMessage, +): SqlExecutionErrorMessage { + if (errs.length === 0 && !sqlError) { + return; + } + const errMsgs = errs.map(err => `\n-` + sqlApiErrorMessage(err.message)); + let sqlErrMsg = sqlError.message; + if (isSeparateTxnError(sqlErrMsg)) { + sqlErrMsg = "Encountered query error(s) fetching data:"; + } + return { + ...sqlError, + message: [sqlErrMsg, ...errMsgs].join(``), + }; +} + export function txnResultIsEmpty(txn_result: SqlTxnResult): boolean { return !txn_result.rows || txn_result.rows?.length === 0; } diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts b/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts index 7cc40d8a0986..743eeefdbd7f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databases/combiners.ts @@ -12,7 +12,6 @@ import { DatabasesListResponse, SqlExecutionErrorMessage } from "../api"; import { DatabasesPageDataDatabase } from "../databasesPage"; import { buildIndexStatToRecommendationsMap, - combineLoadingErrors, getNodesByRegionString, normalizePrivileges, normalizeRoles, @@ -70,8 +69,6 @@ const deriveDatabaseDetails = ( isTenant: boolean, ): DatabasesPageDataDatabase => { const dbStats = dbDetails?.data?.results.stats; - const sizeInBytes = dbStats?.spanStats?.approximate_disk_bytes || 0; - const rangeCount = dbStats?.spanStats.range_count || 0; const nodes = dbStats?.replicaData.replicas || []; const nodesByRegionString = getNodesByRegionString( nodes, @@ -81,20 +78,14 @@ const deriveDatabaseDetails = ( const numIndexRecommendations = dbStats?.indexStats.num_index_recommendations || 0; - const combinedErr = combineLoadingErrors( - dbDetails?.lastError, - dbDetails?.data?.maxSizeReached, - dbListError?.message, - ); - return { loading: !!dbDetails?.inFlight, loaded: !!dbDetails?.valid, - lastError: combinedErr, + requestError: dbDetails?.lastError, + queryError: dbDetails?.data?.results?.error, name: database, - sizeInBytes: sizeInBytes, - tableCount: dbDetails?.data?.results.tablesResp.tables?.length || 0, - rangeCount: rangeCount, + spanStats: dbStats?.spanStats, + tables: dbDetails?.data?.results.tablesResp, nodes: nodes, nodesByRegionString, numIndexRecommendations, diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/util.ts b/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx similarity index 74% rename from pkg/ui/workspaces/cluster-ui/src/databases/util.ts rename to pkg/ui/workspaces/cluster-ui/src/databases/util.tsx index 5d82092f82f8..2a831dfef803 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databases/util.ts +++ b/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx @@ -9,6 +9,15 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import React from "react"; +import { Tooltip } from "antd"; +import "antd/lib/tooltip/style"; +import { + isMaxSizeError, + isPrivilegeError, + sqlApiErrorMessage, + SqlExecutionErrorMessage, +} from "../api"; type IndexUsageStatistic = cockroach.server.serverpb.TableIndexStatsResponse.IExtendedCollectedIndexUsageStatistics; @@ -190,3 +199,62 @@ export function buildIndexStatToRecommendationsMap( }); return recommendationsMap; } + +export function checkInfoAvailable( + requestError: Error, + queryError: Error, + cell: React.ReactNode, +): React.ReactNode { + let tooltipMsg = ""; + if (requestError) { + tooltipMsg = `Encountered a network error fetching data for this cell: ${requestError.name}`; + } else if (queryError) { + tooltipMsg = getQueryErrorMessage(queryError); + } else if (cell == null) { + tooltipMsg = "Empty result"; + } + // If we encounter an error gathering data for this cell, + // render it "unavailable" with a tooltip message for the error. + if (tooltipMsg !== "") { + return ( + + (unavailable) + + ); + } + return cell; +} + +export const getNetworkErrorMessage = (requestError: Error): string => { + return `Encountered a network error: ${requestError.message}`; +}; + +export const getQueryErrorMessage = ( + queryError: SqlExecutionErrorMessage | Error, +): string => { + if (checkPrivilegeError(queryError)) { + return ( + `User has insufficient privileges:\n` + + sqlApiErrorMessage(queryError.message) + ); + } + if (isMaxSizeError(queryError.message)) { + return `Only partial data available, total data size exceeds limit in the console`; + } + // Unexpected error - return the error message. + return sqlApiErrorMessage(queryError.message); +}; + +const checkPrivilegeError = ( + err: SqlExecutionErrorMessage | Error, +): boolean => { + if ("code" in err) { + return isPrivilegeError(err.code); + } + // If the error message includes any mention of privilege, consider it a privilege error. + return err.message.includes("privilege"); +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/helperComponents.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx similarity index 58% rename from pkg/ui/workspaces/cluster-ui/src/databasesPage/helperComponents.tsx rename to pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx index db3daf3ee51f..8dc1d19e748e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/helperComponents.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx @@ -9,6 +9,8 @@ // licenses/APL.txt. import React, { useContext } from "react"; +import { Tooltip } from "antd"; +import "antd/lib/tooltip/style"; import { CircleFilled } from "../icon"; import { DatabasesPageDataDatabase } from "./databasesPage"; import classNames from "classnames/bind"; @@ -17,6 +19,9 @@ import { EncodeDatabaseUri } from "../util"; import { Link } from "react-router-dom"; import { StackIcon } from "../icon/stackIcon"; import { CockroachCloudContext } from "../contexts"; +import { checkInfoAvailable, getNetworkErrorMessage } from "../databases"; +import * as format from "../util/format"; +import { Caution } from "@cockroachlabs/icons"; const cx = classNames.bind(styles); @@ -24,6 +29,20 @@ interface CellProps { database: DatabasesPageDataDatabase; } +export const DiskSizeCell = ({ database }: CellProps): JSX.Element => { + return ( + <> + {checkInfoAvailable( + database.requestError, + database.spanStats?.error, + database.spanStats?.approximate_disk_bytes + ? format.Bytes(database.spanStats?.approximate_disk_bytes) + : null, + )} + + ); +}; + export const IndexRecCell = ({ database }: CellProps): JSX.Element => { const text = database.numIndexRecommendations > 0 @@ -46,10 +65,28 @@ export const DatabaseNameCell = ({ database }: CellProps): JSX.Element => { const linkURL = isCockroachCloud ? `${location.pathname}/${database.name}` : EncodeDatabaseUri(database.name); + let icon = ; + if (database.requestError || database.queryError) { + icon = ( + + + + ); + } return ( - - - {database.name} - + <> + + {icon} + {database.name} + + ); }; diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.module.scss b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.module.scss index 8e85ee5819d5..4e076c6faeb2 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.module.scss @@ -67,6 +67,10 @@ &--primary { fill: $colors--primary-text; } + + &--warning { + fill: $colors--functional-orange-3; + } } .index-recommendations-icon { diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx index 879108dde39e..9788df712ffc 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx @@ -27,7 +27,6 @@ import { SortedTable, SortSetting, } from "src/sortedtable"; -import * as format from "src/util/format"; import styles from "./databasesPage.module.scss"; import sortableTableStyles from "src/sortedtable/sortedtable.module.scss"; import { baseHeadingClasses } from "src/transactionsPage/transactionsPageClasses"; @@ -46,7 +45,20 @@ import { import { merge } from "lodash"; import { UIConfigState } from "src/store"; import { TableStatistics } from "../tableStatistics"; -import { DatabaseNameCell, IndexRecCell } from "./helperComponents"; +import { + DatabaseNameCell, + IndexRecCell, + DiskSizeCell, +} from "./databaseTableCells"; +import { + DatabaseSpanStatsRow, + DatabaseTablesResponse, + isMaxSizeError, + SqlApiQueryResponse, + SqlExecutionErrorMessage, +} from "../api"; +import { InlineAlert } from "@cockroachlabs/ui-components"; +import { checkInfoAvailable } from "../databases"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -84,7 +96,10 @@ const booleanSettingCx = classnames.bind(booleanSettingStyles); export interface DatabasesPageData { loading: boolean; loaded: boolean; - lastError: Error; + // Request error when getting database names. + requestError: Error; + // Query error when getting database names. + queryError: SqlExecutionErrorMessage; databases: DatabasesPageDataDatabase[]; sortSetting: SortSetting; search: string; @@ -100,11 +115,13 @@ export interface DatabasesPageData { export interface DatabasesPageDataDatabase { loading: boolean; loaded: boolean; - lastError: Error; + // Request error when getting database details. + requestError: Error; + // Query error when getting database details. + queryError: SqlExecutionErrorMessage; name: string; - sizeInBytes: number; - tableCount: number; - rangeCount: number; + spanStats?: SqlApiQueryResponse; + tables?: SqlApiQueryResponse; // Array of node IDs used to unambiguously filter by node and region. nodes?: number[]; // String of nodes grouped by region in alphabetical order, e.g. @@ -139,7 +156,6 @@ interface DatabasesPageState { pagination: ISortedTablePagination; filters?: Filters; activeFilters?: number; - lastDetailsError: Error; columns: ColumnDescriptor[]; } @@ -180,7 +196,6 @@ export class DatabasesPage extends React.Component< current: 1, pageSize: tablePageSize, }, - lastDetailsError: null, columns: this.columns(), }; @@ -252,7 +267,7 @@ export class DatabasesPage extends React.Component< if ( !this.props.loaded && !this.props.loading && - this.props.lastError === undefined + this.props.requestError === undefined ) { return this.props.refreshDatabases(); } else { @@ -297,10 +312,8 @@ export class DatabasesPage extends React.Component< } private refresh(): void { - let lastDetailsError: Error; - // load everything by default - let filteredDbs = this.props.databases; + let filteredDbs: DatabasesPageDataDatabase[] = this.props.databases; // Loading only the first page if there are more than // 40 dbs. If there is more than 40 dbs sort will be disabled. @@ -324,21 +337,10 @@ export class DatabasesPage extends React.Component< } filteredDbs.forEach(database => { - if (database.lastError) { - lastDetailsError = database.lastError; - } - - if ( - lastDetailsError && - this.state.lastDetailsError?.name != lastDetailsError?.name - ) { - this.setState({ lastDetailsError: lastDetailsError }); - } - if ( !database.loaded && !database.loading && - database.lastError === undefined + database.requestError === undefined ) { this.props.refreshDatabaseDetails( database.name, @@ -506,7 +508,7 @@ export class DatabasesPage extends React.Component< i++ ) { const db = filteredDatabases[i]; - if (db.loaded || db.loading || db.lastError != undefined) { + if (db.loaded || db.loading || db.requestError != undefined) { continue; } // Info is not loaded for a visible database. @@ -516,19 +518,6 @@ export class DatabasesPage extends React.Component< return false; } - checkInfoAvailable = ( - database: DatabasesPageDataDatabase, - cell: React.ReactNode, - ): React.ReactNode => { - if ( - database.lastError && - database.lastError.name !== "GetDatabaseInfoError" - ) { - return "(unavailable)"; - } - return cell; - }; - private columns(): ColumnDescriptor[] { const columns: ColumnDescriptor[] = [ { @@ -551,9 +540,8 @@ export class DatabasesPage extends React.Component< Size ), - cell: database => - this.checkInfoAvailable(database, format.Bytes(database.sizeInBytes)), - sort: database => database.sizeInBytes, + cell: database => , + sort: database => database.spanStats?.approximate_disk_bytes, className: cx("databases-table__col-size"), name: "size", }, @@ -567,8 +555,12 @@ export class DatabasesPage extends React.Component< ), cell: database => - this.checkInfoAvailable(database, database.tableCount), - sort: database => database.tableCount, + checkInfoAvailable( + database.requestError, + database.tables?.error, + database.tables?.tables?.length, + ), + sort: database => database.tables?.tables.length ?? 0, className: cx("databases-table__col-table-count"), name: "tableCount", }, @@ -582,8 +574,12 @@ export class DatabasesPage extends React.Component< ), cell: database => - this.checkInfoAvailable(database, database.rangeCount), - sort: database => database.rangeCount, + checkInfoAvailable( + database.requestError, + database.spanStats?.error, + database.spanStats?.range_count, + ), + sort: database => database.spanStats?.range_count, className: cx("databases-table__col-range-count"), name: "rangeCount", }, @@ -597,9 +593,10 @@ export class DatabasesPage extends React.Component< ), cell: database => - this.checkInfoAvailable( - database, - database.nodesByRegionString || "None", + checkInfoAvailable( + database.requestError, + null, + database.nodesByRegionString ? database.nodesByRegionString : null, ), sort: database => database.nodesByRegionString, className: cx("databases-table__col-node-regions"), @@ -715,52 +712,54 @@ export class DatabasesPage extends React.Component< ( - - - This cluster has no databases. - - } - /> - )} + error={ + isMaxSizeError(this.props.queryError?.message) + ? new Error(this.props.queryError?.message) + : this.props.requestError + } renderError={() => LoadingError({ statsType: "databases", - error: this.props.lastError, + error: isMaxSizeError(this.props.queryError?.message) + ? new Error(this.props.queryError?.message) + : this.props.requestError, }) } - /> - {!this.props.loading && ( - + {isMaxSizeError(this.props.queryError?.message) && ( + + Not all databases are displayed because the maximum number + of databases was reached in the console.  + + } + /> + )} + <>} - renderError={() => - LoadingError({ - statsType: "part of the information", - error: this.state.lastDetailsError, - }) + disableSortSizeLimit={disableTableSortSize} + renderNoResult={ +
+ + This cluster has no databases. +
} /> - )} +
{ return { loading: !!databasesListState?.inFlight, loaded: !!databasesListState?.valid, - lastError: databasesListState?.lastError, + requestError: databasesListState?.lastError, + queryError: databasesListState?.data?.error, databases: deriveDatabaseDetailsMemoized({ dbListResp: databasesListState?.data, databaseDetails: state.adminUI?.databaseDetails, diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts index fb7ace249fdf..2114fd0e71cd 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.spec.ts @@ -95,7 +95,8 @@ describe("Databases Page", function () { driver.assertProperties({ loading: false, loaded: false, - lastError: undefined, + requestError: undefined, + queryError: undefined, databases: [], search: null, filters: defaultFilters, @@ -139,29 +140,30 @@ describe("Databases Page", function () { driver.assertProperties({ loading: false, loaded: true, - lastError: null, + requestError: null, + queryError: undefined, databases: [ { loading: false, loaded: false, - lastError: undefined, + requestError: undefined, + queryError: undefined, name: "system", nodes: [], - sizeInBytes: 0, - tableCount: 0, - rangeCount: 0, + spanStats: undefined, + tables: undefined, nodesByRegionString: "", numIndexRecommendations: 0, }, { loading: false, loaded: false, - lastError: undefined, + requestError: undefined, + queryError: undefined, name: "test", nodes: [], - sizeInBytes: 0, - tableCount: 0, - rangeCount: 0, + spanStats: undefined, + tables: undefined, nodesByRegionString: "", numIndexRecommendations: 0, }, @@ -293,12 +295,19 @@ describe("Databases Page", function () { driver.assertDatabaseProperties("test", { loading: false, loaded: true, - lastError: undefined, + requestError: null, + queryError: undefined, name: "test", nodes: [1, 2, 3], - sizeInBytes: 100, - tableCount: 2, - rangeCount: 400, + spanStats: { + approximate_disk_bytes: 100, + live_bytes: 200, + total_bytes: 300, + range_count: 400, + }, + tables: { + tables: [`"public"."foo"`, `"public"."bar"`], + }, nodesByRegionString: "gcp-europe-west1(n3), gcp-us-east1(n1,n2)", numIndexRecommendations: 1, }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts index 05bb02c76bb4..d617922fc537 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databasesPage/redux.ts @@ -77,7 +77,8 @@ export const mapStateToProps = (state: AdminUIState): DatabasesPageData => { return { loading: selectLoading(state), loaded: selectLoaded(state), - lastError: selectLastError(state), + requestError: selectLastError(state), + queryError: dbListResp?.error, databases: deriveDatabaseDetailsMemoized({ dbListResp, databaseDetails,