diff --git a/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts index c4c271cb88c9..1d8d5d070dfa 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/databasesApi.ts @@ -11,6 +11,7 @@ import { executeInternalSql, LARGE_RESULT_SIZE, + SqlExecutionErrorMessage, SqlExecutionRequest, sqlResultsAreEmpty, } from "./sqlApi"; @@ -19,19 +20,18 @@ import moment from "moment"; export type DatabasesColumns = { database_name: string; - owner: string; - primary_region: string; - secondary_region: string; - regions: string[]; - survival_goal: string; }; -export type DatabasesListResponse = { databases: string[] }; +export type DatabasesListResponse = { + databases: string[]; + error: SqlExecutionErrorMessage; +}; export const databasesRequest: SqlExecutionRequest = { statements: [ { - sql: `SHOW DATABASES`, + sql: `select database_name + from [show databases]`, }, ], execute: true, @@ -49,19 +49,23 @@ export function getDatabasesList( executeInternalSql(databasesRequest), timeout, ).then(result => { - // If request succeeded but query failed, throw error (caught by saga/cacheDataReducer). - if (result.error) { + // 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; } if (sqlResultsAreEmpty(result)) { - return { databases: [] }; + return { databases: [], error: result.error }; } const dbNames: string[] = result.execution.txn_results[0].rows.map( row => row.database_name, ); - return { databases: dbNames }; + return { databases: dbNames, error: result.error }; }); } diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx index 0f61f6cce34b..255449bb8cc0 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx @@ -178,6 +178,9 @@ function filterBySearchQuery( .every(val => matchString.includes(val)); } +const tablePageSize = 20; +const disableTableSortSize = tablePageSize * 2; + export class DatabasesPage extends React.Component< DatabasesPageProps, DatabasesPageState @@ -189,7 +192,7 @@ export class DatabasesPage extends React.Component< filters: defaultFilters, pagination: { current: 1, - pageSize: 20, + pageSize: tablePageSize, }, lastDetailsError: null, }; @@ -293,22 +296,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 => { @@ -478,7 +510,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; @@ -681,6 +716,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/util/fakeApi.ts b/pkg/ui/workspaces/db-console/src/util/fakeApi.ts index fc089371b689..689f5b4138d4 100644 --- a/pkg/ui/workspaces/db-console/src/util/fakeApi.ts +++ b/pkg/ui/workspaces/db-console/src/util/fakeApi.ts @@ -70,11 +70,6 @@ export function buildSQLApiDatabasesResponse(databases: string[]) { const rows: clusterUiApi.DatabasesColumns[] = databases.map(database => { return { database_name: database, - owner: "root", - primary_region: null, - secondary_region: null, - regions: ["gcp-europe-west1", "gcp-europe-west2"], - survival_goal: null, }; }); 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),