From 0c180d98a70e3b1195f99280ca0000c181f80394 Mon Sep 17 00:00:00 2001 From: j82w Date: Thu, 2 Feb 2023 10:17:25 -0500 Subject: [PATCH] ui: databases shows partial results for size limit error The databases page displays partial results instead of just showing an error message. Sorting is disabled if there are more than 2 pages of results which is currently configured to 40dbs. This still allows most user to use sort functionality, but prevents large customers from breaking when it would need to do a network call per a database. The database details are now loaded on demand for the first page only. Previously a network call was done for all databases which would result in 2k network calls. It now only loads the page of details the user is looking at. part of: #94332 Release note: none --- .../src/databasesPage/databasesPage.tsx | 47 +++++++++++++++++-- .../src/sortedtable/sortedtable.tsx | 15 +++++- .../src/sqlActivity/errorComponent.tsx | 8 ++++ .../views/databases/databasesPage/redux.ts | 39 +++++++++++++-- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx index 1a46c848ea1d..7236b2cd5fc9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx @@ -180,6 +180,9 @@ function filterBySearchQuery( .every(val => matchString.includes(val)); } +const tablePageSize = 20; +const disableTableSortSize = tablePageSize * 2; + export class DatabasesPage extends React.Component< DatabasesPageProps, DatabasesPageState @@ -191,7 +194,7 @@ export class DatabasesPage extends React.Component< filters: defaultFilters, pagination: { current: 1, - pageSize: 20, + pageSize: tablePageSize, }, lastDetailsError: null, }; @@ -295,22 +298,51 @@ export class DatabasesPage extends React.Component< } let lastDetailsError: Error; - this.props.databases.forEach(database => { + + // load everything by default + let filteredDbs = 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. + if (this.props.databases.length > disableTableSortSize) { + const startIndex = + this.state.pagination.pageSize * (this.state.pagination.current - 1); + // Result maybe filtered so get db names from filtered results + if (this.props.search && this.props.search.length > 0) { + filteredDbs = this.filteredDatabasesData(); + } + + if (!filteredDbs || filteredDbs.length === 0) { + return; + } + + // Only load the first page + filteredDbs = filteredDbs.slice( + startIndex, + startIndex + this.state.pagination.pageSize, + ); + } + + filteredDbs.forEach(database => { if (database.lastError !== undefined) { lastDetailsError = database.lastError; } + if ( lastDetailsError && this.state.lastDetailsError?.name != lastDetailsError?.name ) { this.setState({ lastDetailsError: lastDetailsError }); } + if ( !database.loaded && !database.loading && - database.lastError === undefined + (database.lastError === undefined || + database.lastError?.name === "GetDatabaseInfoError") ) { - return this.props.refreshDatabaseDetails(database.name); + this.props.refreshDatabaseDetails(database.name); + return; } database.missingTables.forEach(table => { @@ -480,7 +512,10 @@ export class DatabasesPage extends React.Component< database: DatabasesPageDataDatabase, cell: React.ReactNode, ): React.ReactNode => { - if (database.lastError) { + if ( + database.lastError && + database.lastError.name !== "GetDatabaseInfoError" + ) { return "(unavailable)"; } return cell; @@ -674,6 +709,7 @@ export class DatabasesPage extends React.Component< onChangeSortSetting={this.changeSortSetting} pagination={this.state.pagination} loading={this.props.loading} + disableSortSizeLimit={disableTableSortSize} renderNoResult={
diff --git a/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx b/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx index 0c7af1a4bcca..4a0c26ec9fc9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx @@ -103,6 +103,7 @@ interface SortedTableProps { pagination?: ISortedTablePagination; loading?: boolean; loadingLabel?: string; + disableSortSizeLimit?: number; // empty state for table empty?: boolean; emptyProps?: EmptyPanelProps; @@ -225,6 +226,14 @@ export class SortedTable extends React.Component< if (!sortSetting) { return this.paginatedData(); } + + if ( + this.props.disableSortSizeLimit && + data.length > this.props.disableSortSizeLimit + ) { + return this.paginatedData(); + } + const sortColumn = columns.find(c => c.name === sortSetting.columnTitle); if (!sortColumn || !sortColumn.sort) { return this.paginatedData(); @@ -253,13 +262,17 @@ export class SortedTable extends React.Component< rollups: React.ReactNode[], columns: ColumnDescriptor[], ) => { + const sort = + !this.props.disableSortSizeLimit || + this.props.data.length <= this.props.disableSortSizeLimit; + return columns.map((cd, ii): SortableColumn => { return { name: cd.name, title: cd.title, hideTitleUnderline: cd.hideTitleUnderline, cell: index => cd.cell(sorted[index]), - columnTitle: cd.sort ? cd.name : undefined, + columnTitle: sort && cd.sort ? cd.name : undefined, rollup: rollups[ii], className: cd.className, titleAlign: cd.titleAlign, diff --git a/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx b/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx index dc053af8c690..ac5ba9cf5fe1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sqlActivity/errorComponent.tsx @@ -17,9 +17,17 @@ const cx = classNames.bind(styles); interface SQLActivityErrorProps { statsType: string; timeout?: boolean; + error?: Error; } const LoadingError: React.FC = props => { + if (props.error && props.error.name === "GetDatabaseInfoError") { + return ( +
+ {props.error.message} +
+ ); + } const error = props.timeout ? "a timeout" : "an unexpected error"; return (
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 83c751141071..0700a35b1fcd 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 @@ -21,11 +21,11 @@ import { import { cockroach } from "src/js/protos"; import { generateTableID, - refreshDatabases, refreshDatabaseDetails, - refreshTableStats, + refreshDatabases, refreshNodes, refreshSettings, + refreshTableStats, } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; import { FixLong } from "src/util/fixLong"; @@ -75,7 +75,7 @@ const searchLocalSetting = new LocalSetting( ); const selectDatabases = createSelector( - (state: AdminUIState) => state.cachedData.databases.data?.databases, + (state: AdminUIState) => state.cachedData.databases.data, (state: AdminUIState) => state.cachedData.databaseDetails, (state: AdminUIState) => state.cachedData.tableStats, (state: AdminUIState) => nodeRegionsByIDSelector(state), @@ -87,7 +87,7 @@ const selectDatabases = createSelector( nodeRegions, isTenant, ): DatabasesPageDataDatabase[] => - (databases || []).map(database => { + (databases?.databases || []).map(database => { const details = databaseDetails[database]; const stats = details?.data?.stats; @@ -131,10 +131,15 @@ const selectDatabases = createSelector( ); const numIndexRecommendations = stats?.num_index_recommendations || 0; + const combinedErr = combineLoadingErrors( + details?.lastError, + databases?.error?.message, + ); + return { loading: !!details?.inFlight, loaded: !!details?.valid, - lastError: details?.lastError, + lastError: combinedErr, name: database, sizeInBytes: sizeInBytes, tableCount: details?.data?.table_names?.length || 0, @@ -152,6 +157,30 @@ const selectDatabases = createSelector( }), ); +function combineLoadingErrors(detailsErr: Error, dbList: string): Error { + if (!dbList) { + return detailsErr; + } + + if (!detailsErr) { + return new GetDatabaseInfoError( + `Failed to load all databases. Partial results are shown. Debug info: ${dbList}`, + ); + } + + return new GetDatabaseInfoError( + `Failed to load all databases and database details. Partial results are shown. Debug info: ${dbList}, details error: ${detailsErr}`, + ); +} + +export class GetDatabaseInfoError extends Error { + constructor(message: string) { + super(message); + + this.name = this.constructor.name; + } +} + export const mapStateToProps = (state: AdminUIState): DatabasesPageData => ({ loading: selectLoading(state), loaded: selectLoaded(state),