From 364fd1fd7165881e41dcd8058c65bba51346ac6b Mon Sep 17 00:00:00 2001 From: Eric Harmeling Date: Tue, 1 Feb 2022 12:17:50 -0500 Subject: [PATCH] Stats collection details added to Database pages Closes: #67510 Release justification: low-risk, high benefit changes to existing functionality Release note (ui change): - Added status of automatic statistics collection to the Database and Database table pages on the DB Console. - Added timestamp of last statistics collection to the Database details and Database table pages on the DB Console. --- docs/generated/http/full.md | 1 + docs/generated/swagger/spec.json | 6 +++ pkg/server/admin.go | 27 +++++++++- pkg/server/admin_test.go | 17 ++++++ pkg/server/serverpb/admin.proto | 2 + .../cluster-ui/src/core/colors.module.scss | 1 + .../databaseDetailsPage.stories.tsx | 2 + .../databaseDetailsPage.tsx | 23 ++++++++ .../databaseTablePage.stories.tsx | 6 +++ .../databaseTablePage/databaseTablePage.tsx | 47 +++++++++++++++- .../databasesPage/databasesPage.stories.tsx | 6 +++ .../src/databasesPage/databasesPage.tsx | 32 ++++++++++- .../src/settings/booleanSetting.module.scss | 35 ++++++++++++ .../src/settings/booleanSetting.tsx | 53 +++++++++++++++++++ .../cluster-ui/src/settings/index.ts | 11 ++++ .../cluster-ui/src/summaryCard/index.tsx | 48 +++++++++++++++++ .../clusterSettings.selectors.ts | 11 ++++ .../databaseDetailsPage/redux.spec.ts | 13 +++++ .../databases/databaseDetailsPage/redux.ts | 13 ++++- .../databases/databaseTablePage/redux.spec.ts | 6 +++ .../databases/databaseTablePage/redux.ts | 12 +++++ .../databases/databasesPage/redux.spec.ts | 7 +++ .../views/databases/databasesPage/redux.ts | 4 ++ 23 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.module.scss create mode 100644 pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.tsx create mode 100644 pkg/ui/workspaces/cluster-ui/src/settings/index.ts diff --git a/docs/generated/http/full.md b/docs/generated/http/full.md index 9fdfb19932da..4d034e8b1ac4 100644 --- a/docs/generated/http/full.md +++ b/docs/generated/http/full.md @@ -4487,6 +4487,7 @@ a table. | zone_config_level | [ZoneConfigurationLevel](#cockroach.server.serverpb.TableDetailsResponse-cockroach.server.serverpb.ZoneConfigurationLevel) | | The level at which this object's zone configuration is set. | [reserved](#support-status) | | descriptor_id | [int64](#cockroach.server.serverpb.TableDetailsResponse-int64) | | descriptor_id is an identifier used to uniquely identify this table. It can be used to find events pertaining to this table by filtering on the 'target_id' field of events. | [reserved](#support-status) | | configure_zone_statement | [string](#cockroach.server.serverpb.TableDetailsResponse-string) | | configure_zone_statement is the output of "SHOW ZONE CONFIGURATION FOR TABLE" for this table. It is a SQL statement that would re-configure the table's current zone if executed. | [reserved](#support-status) | +| stats_last_created_at | [google.protobuf.Timestamp](#cockroach.server.serverpb.TableDetailsResponse-google.protobuf.Timestamp) | | stats_last_created_at is the time at which statistics were last created. | [reserved](#support-status) | diff --git a/docs/generated/swagger/spec.json b/docs/generated/swagger/spec.json index 7213cf361fb0..18cdafde089a 100644 --- a/docs/generated/swagger/spec.json +++ b/docs/generated/swagger/spec.json @@ -1274,6 +1274,12 @@ "format": "int64", "x-go-name": "RangeCount" }, + "stats_last_created_at": { + "description": "stats_last_created_at is the time at which statistics were last created.", + "type": "string", + "format": "date-time", + "x-go-name": "StatsLastCreatedAt" + }, "zone_config": { "$ref": "#/definitions/ZoneConfig" }, diff --git a/pkg/server/admin.go b/pkg/server/admin.go index 438bba38c5b2..2daa0acb85a0 100644 --- a/pkg/server/admin.go +++ b/pkg/server/admin.go @@ -873,6 +873,31 @@ func (s *adminServer) tableDetailsHelper( resp.CreateTableStatement = createStmt } + // Marshal SHOW STATISTICS result. + row, cols, err = s.server.sqlServer.internalExecutor.QueryRowExWithCols( + ctx, "admin-show-statistics", nil, /* txn */ + sessiondata.InternalExecutorOverride{User: userName}, + fmt.Sprintf("SELECT max(created) AS created FROM [SHOW STATISTICS FOR TABLE %s]", escQualTable), + ) + if row == nil { + return nil, s.serverErrorf("table statistics response not available.") + } + if err = s.maybeHandleNotFoundError(err); err != nil { + return nil, err + } + { + scanner := makeResultScanner(cols) + const createdCol = "created" + noTableStats, _ := scanner.IsNull(row, createdCol) + if !noTableStats { + var createdTs time.Time + if err := scanner.Scan(row, createdCol, &createdTs); err != nil { + return nil, err + } + resp.StatsLastCreatedAt = createdTs + } + } + // Marshal SHOW ZONE CONFIGURATION result. row, cols, err = s.server.sqlServer.internalExecutor.QueryRowExWithCols( ctx, "admin-show-zone-config", nil, /* txn */ @@ -2888,7 +2913,7 @@ func (rs resultScanner) ScanIndex(row tree.Datums, index int, dst interface{}) e case *time.Time: s, ok := src.(*tree.DTimestamp) if !ok { - return errors.Errorf("source type assertion failed") + return errors.Errorf("source type assertion failed on *time.Time") } *d = s.Time diff --git a/pkg/server/admin_test.go b/pkg/server/admin_test.go index 6cf039e17cda..71e01151cca4 100644 --- a/pkg/server/admin_test.go +++ b/pkg/server/admin_test.go @@ -673,6 +673,7 @@ func TestAdminAPITableDetails(t *testing.T) { "CREATE USER app", fmt.Sprintf("GRANT SELECT ON %s.%s TO readonly", escDBName, tblName), fmt.Sprintf("GRANT SELECT,UPDATE,DELETE ON %s.%s TO app", escDBName, tblName), + fmt.Sprintf("CREATE STATISTICS test_stats FROM %s.%s", escDBName, tblName), } pgURL, cleanupGoDB := sqlutils.PGUrl( t, s.ServingSQLAddr(), "StartServer" /* prefix */, url.User(security.RootUser)) @@ -779,6 +780,22 @@ func TestAdminAPITableDetails(t *testing.T) { } } + // Verify statistics last updated. + { + + showStatisticsForTableQuery := fmt.Sprintf("SELECT max(created) AS created FROM [SHOW STATISTICS FOR TABLE %s.%s]", escDBName, tblName) + + row := db.QueryRow(showStatisticsForTableQuery) + var createdTs time.Time + if err := row.Scan(&createdTs); err != nil { + t.Fatal(err) + } + + if a, e := resp.StatsLastCreatedAt, createdTs; reflect.DeepEqual(a, e) { + t.Fatalf("mismatched statistics creation timestamp; expected %s, got %s", e, a) + } + } + // Verify Descriptor ID. tableID, err := ts.admin.queryTableID(ctx, security.RootUserName(), tc.dbName, tc.tblName) if err != nil { diff --git a/pkg/server/serverpb/admin.proto b/pkg/server/serverpb/admin.proto index aeb4f11215e8..cd00e6ed7da1 100644 --- a/pkg/server/serverpb/admin.proto +++ b/pkg/server/serverpb/admin.proto @@ -210,6 +210,8 @@ message TableDetailsResponse { // for this table. It is a SQL statement that would re-configure the table's current // zone if executed. string configure_zone_statement = 9; + // stats_last_created_at is the time at which statistics were last created. + google.protobuf.Timestamp stats_last_created_at = 10 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; } // TableStatsRequest is a request for detailed, computationally expensive diff --git a/pkg/ui/workspaces/cluster-ui/src/core/colors.module.scss b/pkg/ui/workspaces/cluster-ui/src/core/colors.module.scss index 9f2277fd1814..8eef5360c24d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/core/colors.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/core/colors.module.scss @@ -59,6 +59,7 @@ $colors--functional-orange-5: #764205; $colors--title: $colors--neutral-8; $colors--primary-text: $colors--neutral-7; $colors--secondary-text: $colors--neutral-6; +$colors--success: $colors--primary-green-3; $colors--disabled: $colors--neutral-5; $colors--link: $colors--primary-blue-3; $colors--white: $colors--neutral-0; diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx index 738b9b5dadb8..c00140b4b5f9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.stories.tsx @@ -25,6 +25,7 @@ import { } from "./databaseDetailsPage"; import * as H from "history"; +import moment from "moment"; const history = H.createHashHistory(); const withLoadingIndicator: DatabaseDetailsPageProps = { @@ -107,6 +108,7 @@ const withData: DatabaseDetailsPageProps = { userCount: roles.length, roles: roles, grants: grants, + statsLastUpdated: moment("0001-01-01T00:00:00Z"), }, showNodeRegionsColumn: true, stats: { diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx index ecb01d305275..8bc879a243dd 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx @@ -36,6 +36,8 @@ import { baseHeadingClasses, statisticsClasses, } from "src/transactionsPage/transactionsPageClasses"; +import moment, { Moment } from "moment"; +import { formatDate } from "antd/es/date-picker/utils"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -101,6 +103,7 @@ export interface DatabaseDetailsPageDataTableDetails { userCount: number; roles: string[]; grants: string[]; + statsLastUpdated?: Moment; } export interface DatabaseDetailsPageDataTableStats { @@ -351,6 +354,26 @@ export class DatabaseDetailsPage extends React.Component< showByDefault: this.props.showNodeRegionsColumn, hideIfTenant: true, }, + { + title: ( + + Table Stats Last Updated (UTC) + + ), + cell: table => + table.details.statsLastUpdated.isSame(moment.utc("0001-01-01")) + ? "No table statistics found" + : formatDate( + table.details.statsLastUpdated, + "MMM DD, YYYY [at] h:mm A", + ), + sort: table => table.details.statsLastUpdated, + className: cx("database-table__col--table-stats"), + name: "tableStatsUpdated", + }, ]; } diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx index d244bf4f8747..bb444ba90f9d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.stories.tsx @@ -26,6 +26,7 @@ const history = H.createHashHistory(); const withLoadingIndicator: DatabaseTablePageProps = { databaseName: randomName(), name: randomName(), + automaticStatsCollectionEnabled: true, details: { loading: true, loaded: false, @@ -33,6 +34,7 @@ const withLoadingIndicator: DatabaseTablePageProps = { replicaCount: 0, indexNames: [], grants: [], + statsLastUpdated: moment("0001-01-01T00:00:00Z"), }, stats: { loading: true, @@ -58,6 +60,7 @@ const withLoadingIndicator: DatabaseTablePageProps = { refreshTableStats: () => {}, refreshIndexStats: () => {}, resetIndexUsageStats: () => {}, + refreshSettings: () => {}, }; const name = randomName(); @@ -65,6 +68,7 @@ const name = randomName(); const withData: DatabaseTablePageProps = { databaseName: randomName(), name: name, + automaticStatsCollectionEnabled: true, details: { loading: false, loaded: true, @@ -89,6 +93,7 @@ const withData: DatabaseTablePageProps = { }; }), ), + statsLastUpdated: moment("0001-01-01T00:00:00Z"), }, showNodeRegionsSection: true, stats: { @@ -136,6 +141,7 @@ const withData: DatabaseTablePageProps = { refreshTableStats: () => {}, refreshIndexStats: () => {}, resetIndexUsageStats: () => {}, + refreshSettings: () => {}, }; storiesOf("Database Table Page", module) diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx index f1f797d3bfb1..46c678e28c2d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx @@ -21,7 +21,11 @@ import { CaretRight } from "src/icon/caretRight"; import { StackIcon } from "src/icon/stackIcon"; import { SqlBox } from "src/sql"; import { ColumnDescriptor, SortSetting, SortedTable } from "src/sortedtable"; -import { SummaryCard, SummaryCardItem } from "src/summaryCard"; +import { + SummaryCard, + SummaryCardItem, + SummaryCardItemBoolSetting, +} from "src/summaryCard"; import * as format from "src/util/format"; import { syncHistory } from "src/util"; @@ -32,7 +36,10 @@ import moment, { Moment } from "moment"; import { Search as IndexIcon } from "@cockroachlabs/icons"; import { formatDate } from "antd/es/date-picker/utils"; import { Link } from "react-router-dom"; +import classnames from "classnames/bind"; +import booleanSettingStyles from "../settings/booleanSetting.module.scss"; const cx = classNames.bind(styles); +const booleanSettingCx = classnames.bind(booleanSettingStyles); const { TabPane } = Tabs; @@ -84,6 +91,7 @@ export interface DatabaseTablePageData { stats: DatabaseTablePageDataStats; indexStats: DatabaseTablePageIndexStats; showNodeRegionsSection?: boolean; + automaticStatsCollectionEnabled: boolean; } export interface DatabaseTablePageDataDetails { @@ -93,6 +101,7 @@ export interface DatabaseTablePageDataDetails { replicaCount: number; indexNames: string[]; grants: Grant[]; + statsLastUpdated: Moment; } export interface DatabaseTablePageIndexStats { @@ -125,6 +134,7 @@ export interface DatabaseTablePageDataStats { export interface DatabaseTablePageActions { refreshTableDetails: (database: string, table: string) => void; refreshTableStats: (database: string, table: string) => void; + refreshSettings: () => void; refreshIndexStats?: (database: string, table: string) => void; resetIndexUsageStats?: (database: string, table: string) => void; refreshNodes?: () => void; @@ -203,6 +213,10 @@ export class DatabaseTablePage extends React.Component< this.props.name, ); } + + if (this.props.refreshSettings != null) { + this.props.refreshSettings(); + } } minDate = moment.utc("0001-01-01"); // minimum value as per UTC @@ -359,6 +373,37 @@ export class DatabaseTablePage extends React.Component< label="Ranges" value={this.props.stats.rangeCount} /> + {!this.props.details.statsLastUpdated.isSame( + this.minDate, + ) && ( + + )} + + {" "} + Automatic statistics can help improve query + performance. Learn how to{" "} + + manage statistics collection + + . + + } + /> diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.stories.tsx index d7514ed43df8..5107bbb00052 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.stories.tsx @@ -22,6 +22,7 @@ const history = H.createHashHistory(); const withLoadingIndicator: DatabasesPageProps = { loading: true, loaded: false, + automaticStatsCollectionEnabled: true, databases: [], sortSetting: { ascending: false, @@ -29,6 +30,7 @@ const withLoadingIndicator: DatabasesPageProps = { }, onSortingChange: () => {}, refreshDatabases: () => {}, + refreshSettings: () => {}, refreshDatabaseDetails: () => {}, refreshTableStats: () => {}, location: history.location, @@ -44,6 +46,7 @@ const withLoadingIndicator: DatabasesPageProps = { const withoutData: DatabasesPageProps = { loading: false, loaded: true, + automaticStatsCollectionEnabled: true, databases: [], sortSetting: { ascending: false, @@ -51,6 +54,7 @@ const withoutData: DatabasesPageProps = { }, onSortingChange: () => {}, refreshDatabases: () => {}, + refreshSettings: () => {}, refreshDatabaseDetails: () => {}, refreshTableStats: () => {}, location: history.location, @@ -67,6 +71,7 @@ const withData: DatabasesPageProps = { loading: false, loaded: true, showNodeRegionsColumn: true, + automaticStatsCollectionEnabled: true, sortSetting: { ascending: false, columnTitle: "name", @@ -86,6 +91,7 @@ const withData: DatabasesPageProps = { }), onSortingChange: () => {}, refreshDatabases: () => {}, + refreshSettings: () => {}, refreshDatabaseDetails: () => {}, refreshTableStats: () => {}, location: history.location, diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx index be5443227fe0..2c3a59bf9528 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx @@ -16,6 +16,7 @@ import _ from "lodash"; import { StackIcon } from "src/icon/stackIcon"; import { Pagination, ResultsPerPageLabel } from "src/pagination"; +import { BooleanSetting } from "src/settings/booleanSetting"; import { ColumnDescriptor, ISortedTablePagination, @@ -31,9 +32,12 @@ import { statisticsClasses, } from "src/transactionsPage/transactionsPageClasses"; import { syncHistory } from "../util"; +import classnames from "classnames/bind"; +import booleanSettingStyles from "../settings/booleanSetting.module.scss"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); +const booleanSettingCx = classnames.bind(booleanSettingStyles); // We break out separate interfaces for some of the nested objects in our data // both so that they can be available as SortedTable rows and for making @@ -67,6 +71,7 @@ export interface DatabasesPageData { loaded: boolean; databases: DatabasesPageDataDatabase[]; sortSetting: SortSetting; + automaticStatsCollectionEnabled: boolean; showNodeRegionsColumn?: boolean; } @@ -99,6 +104,7 @@ export interface DatabasesPageActions { refreshDatabases: () => void; refreshDatabaseDetails: (database: string) => void; refreshTableStats: (database: string, table: string) => void; + refreshSettings: () => void; refreshNodes?: () => void; onSortingChange?: ( name: string, @@ -160,6 +166,10 @@ export class DatabasesPage extends React.Component< this.props.refreshNodes(); } + if (this.props.refreshSettings != null) { + this.props.refreshSettings(); + } + if (!this.props.loaded && !this.props.loading) { return this.props.refreshDatabases(); } @@ -280,9 +290,29 @@ export class DatabasesPage extends React.Component< const displayColumns = this.columns.filter( col => col.showByDefault !== false, ); + const tipText = ( + + {" "} + Automatic statistics can help improve query performance. Learn how to{" "} + + manage statistics collection + + . + + ); return (
-

Databases

+
+

Databases

+ +

diff --git a/pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.module.scss b/pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.module.scss new file mode 100644 index 000000000000..4e6c45befed5 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.module.scss @@ -0,0 +1,35 @@ +@import "src/core/index.module"; + +.crl-hover-text { + font-weight: inherit; + + &__dashed-underline { + color: inherit; + border-bottom-width: 1px; + border-bottom-style: dashed; + border-bottom-color: inherit; + cursor: default; + } + + &__link-text { + color: inherit + } +} + +.bool-setting-icon { + + &__enabled { + fill: $colors--success; + margin-right: 8px; + height: 8px; + width: 8px; + } + + &__disabled { + fill: $colors--disabled; + margin-right: 8px; + height: 8px; + width: 8px; + } +} + diff --git a/pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.tsx b/pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.tsx new file mode 100644 index 000000000000..da5457890fa4 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/settings/booleanSetting.tsx @@ -0,0 +1,53 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import * as React from "react"; +import { CircleFilled } from "src/icon"; +import { Tooltip } from "antd"; +import classNames from "classnames/bind"; +import styles from "./booleanSetting.module.scss"; + +const cx = classNames.bind(styles); + +export interface BooleanSettingProps { + text: string; + enabled: boolean; + tooltipText: JSX.Element; +} + +export function BooleanSetting(props: BooleanSettingProps) { + const { text, enabled, tooltipText } = props; + if (enabled) { + return ( +
+ + + {text} - enabled + +
+ ); + } + return ( +
+ + + {text} - disabled + +
+ ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/settings/index.ts b/pkg/ui/workspaces/cluster-ui/src/settings/index.ts new file mode 100644 index 000000000000..7cbedc8da889 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/settings/index.ts @@ -0,0 +1,11 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +export * from "./booleanSetting"; diff --git a/pkg/ui/workspaces/cluster-ui/src/summaryCard/index.tsx b/pkg/ui/workspaces/cluster-ui/src/summaryCard/index.tsx index ac396f426944..2ef4d3276fc0 100644 --- a/pkg/ui/workspaces/cluster-ui/src/summaryCard/index.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/summaryCard/index.tsx @@ -11,6 +11,9 @@ import React from "react"; import classnames from "classnames/bind"; import styles from "./summaryCard.module.scss"; +import booleanSettingStyles from "../settings/booleanSetting.module.scss"; +import { CircleFilled } from "src/icon"; +import { Tooltip } from "antd"; interface ISummaryCardProps { children: React.ReactNode; @@ -18,6 +21,7 @@ interface ISummaryCardProps { } const cx = classnames.bind(styles); +const booleanSettingCx = classnames.bind(booleanSettingStyles); // tslint:disable-next-line: variable-name export const SummaryCard: React.FC = ({ @@ -31,6 +35,10 @@ interface ISummaryCardItemProps { className?: string; } +interface ISummaryCardItemBoolSettingProps extends ISummaryCardItemProps { + toolTipText: React.ReactNode; +} + export const SummaryCardItem: React.FC = ({ label, value, @@ -41,3 +49,43 @@ export const SummaryCardItem: React.FC = ({

{value}

); + +export const SummaryCardItemBoolSetting: React.FC = ({ + label, + value, + toolTipText, + className, +}) => + value ? ( +
+

{label}

+

+ + + Enabled + +

+
+ ) : ( +
+

{label}

+

+ + + Disabled + +

+
+ ); diff --git a/pkg/ui/workspaces/db-console/src/redux/clusterSettings/clusterSettings.selectors.ts b/pkg/ui/workspaces/db-console/src/redux/clusterSettings/clusterSettings.selectors.ts index 1d9908649bba..62d0e3601e27 100644 --- a/pkg/ui/workspaces/db-console/src/redux/clusterSettings/clusterSettings.selectors.ts +++ b/pkg/ui/workspaces/db-console/src/redux/clusterSettings/clusterSettings.selectors.ts @@ -41,3 +41,14 @@ export const selectResolution30mStorageTTL = createSelector( return util.durationFromISO8601String(value); }, ); + +export const selectAutomaticStatsCollectionEnabled = createSelector( + selectClusterSettings, + (settings): boolean | undefined => { + if (!settings) { + return undefined; + } + const value = settings["sql.stats.automatic_collection.enabled"]?.value; + return value === "true"; + }, +); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts index 89624b130c62..7b530da07524 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.spec.ts @@ -19,6 +19,7 @@ import { DatabaseDetailsPageData, DatabaseDetailsPageDataTableDetails, DatabaseDetailsPageDataTableStats, + util, ViewMode, } from "@cockroachlabs/cluster-ui"; @@ -26,6 +27,8 @@ import { AdminUIState, createAdminUIStore } from "src/redux/state"; import { databaseNameAttr } from "src/util/constants"; import * as fakeApi from "src/util/fakeApi"; import { mapStateToProps, mapDispatchToProps } from "./redux"; +import moment from "moment"; +import { makeTimestamp } from "oss/src/views/databases/utils"; function fakeRouteComponentProps( key: string, @@ -159,6 +162,7 @@ describe("Database Details Page", function() { userCount: 0, roles: [], grants: [], + statsLastUpdated: moment(), }, stats: { loading: false, @@ -178,6 +182,7 @@ describe("Database Details Page", function() { userCount: 0, roles: [], grants: [], + statsLastUpdated: moment(), }, stats: { loading: false, @@ -250,6 +255,7 @@ describe("Database Details Page", function() { implicit: false, }, ], + stats_last_created_at: makeTimestamp("0001-01-01T00:00:00Z"), }); fakeApi.stubTableDetails("things", "bar", { @@ -298,6 +304,7 @@ describe("Database Details Page", function() { implicit: false, }, ], + stats_last_created_at: makeTimestamp("0001-01-01T00:00:00Z"), }); await driver.refreshDatabaseDetails(); @@ -312,6 +319,9 @@ describe("Database Details Page", function() { userCount: 2, roles: ["admin", "public"], grants: ["CREATE", "SELECT"], + statsLastUpdated: util.TimestampToMoment( + makeTimestamp("0001-01-01T00:00:00Z"), + ), }); driver.assertTableDetails("bar", { @@ -322,6 +332,9 @@ describe("Database Details Page", function() { userCount: 3, roles: ["root", "app", "data"], grants: ["ALL", "SELECT", "INSERT"], + statsLastUpdated: util.TimestampToMoment( + makeTimestamp("0001-01-01T00:00:00Z"), + ), }); }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts index 96052d4a1a5a..c768c4c04c8d 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseDetailsPage/redux.ts @@ -12,7 +12,11 @@ import { RouteComponentProps } from "react-router"; import { createSelector } from "reselect"; import { LocalSetting } from "src/redux/localsettings"; import _ from "lodash"; -import { DatabaseDetailsPageData, ViewMode } from "@cockroachlabs/cluster-ui"; +import { + DatabaseDetailsPageData, + util, + ViewMode, +} from "@cockroachlabs/cluster-ui"; import { cockroach } from "src/js/protos"; import { @@ -30,6 +34,8 @@ import { selectIsMoreThanOneNode, } from "src/redux/nodes"; import { getNodesByRegionString } from "../utils"; +import moment from "moment"; + const { DatabaseDetailsRequest, TableDetailsRequest, @@ -132,7 +138,6 @@ export const mapStateToProps = createSelector( const numIndexes = _.uniq( _.map(details?.data?.indexes, index => index.name), ).length; - return { name: table, details: { @@ -143,6 +148,10 @@ export const mapStateToProps = createSelector( userCount: roles.length, roles: roles, grants: grants, + statsLastUpdated: util.TimestampToMoment( + details?.data?.stats_last_created_at, + moment.utc("0001-01-01"), + ), }, stats: { loading: !!stats?.inFlight, diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts index ef1a4e859d78..00b8e813748f 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.spec.ts @@ -156,6 +156,7 @@ describe("Database Table Page", function() { it("starts in a pre-loading state", function() { driver.assertProperties( { + automaticStatsCollectionEnabled: false, databaseName: "DATABASE", name: "TABLE", showNodeRegionsSection: false, @@ -166,6 +167,7 @@ describe("Database Table Page", function() { replicaCount: 0, indexNames: [], grants: [], + statsLastUpdated: moment(), }, stats: { loading: false, @@ -200,6 +202,7 @@ describe("Database Table Page", function() { zone_config: { num_replicas: 5, }, + stats_last_created_at: makeTimestamp("0001-01-01T00:00:00Z"), }); await driver.refreshTableDetails(); @@ -215,6 +218,9 @@ describe("Database Table Page", function() { { user: "admin", privilege: "DROP" }, { user: "public", privilege: "SELECT" }, ], + statsLastUpdated: util.TimestampToMoment( + makeTimestamp("0001-01-01T00:00:00Z"), + ), }); }); diff --git a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts index f68857437327..8e4f0be76ea8 100644 --- a/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts +++ b/pkg/ui/workspaces/db-console/src/views/databases/databaseTablePage/redux.ts @@ -20,6 +20,7 @@ import { refreshTableStats, refreshNodes, refreshIndexStats, + refreshSettings, } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; import { databaseNameAttr, tableNameAttr } from "src/util/constants"; @@ -31,6 +32,8 @@ import { } from "src/redux/nodes"; import { getNodesByRegionString } from "../utils"; import { resetIndexUsageStatsAction } from "src/redux/indexUsageStats"; +import { selectAutomaticStatsCollectionEnabled } from "oss/src/redux/clusterSettings"; +import moment from "moment"; const { TableDetailsRequest, @@ -49,6 +52,7 @@ export const mapStateToProps = createSelector( state => state.cachedData.indexStats, state => nodeRegionsByIDSelector(state), state => selectIsMoreThanOneNode(state), + state => selectAutomaticStatsCollectionEnabled(state), ( database, @@ -58,6 +62,7 @@ export const mapStateToProps = createSelector( indexUsageStats, nodeRegions, showNodeRegionsSection, + automaticStatsCollectionEnabled, ): DatabaseTablePageData => { const details = tableDetails[generateTableID(database, table)]; const stats = tableStats[generateTableID(database, table)]; @@ -102,8 +107,13 @@ export const mapStateToProps = createSelector( replicaCount: details?.data?.zone_config?.num_replicas || 0, indexNames: _.uniq(_.map(details?.data?.indexes, index => index.name)), grants: grants, + statsLastUpdated: util.TimestampToMoment( + details?.data?.stats_last_created_at, + moment.utc("0001-01-01"), + ), }, showNodeRegionsSection, + automaticStatsCollectionEnabled, stats: { loading: !!stats?.inFlight, loaded: !!stats?.valid, @@ -139,4 +149,6 @@ export const mapDispatchToProps = { resetIndexUsageStats: resetIndexUsageStatsAction, refreshNodes, + + refreshSettings, }; 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 03765a96aaff..6d40ef02049a 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 @@ -48,6 +48,10 @@ class TestDriver { return this.actions.refreshTableStats(database, table); } + async refreshSettings() { + return this.actions.refreshSettings(); + } + assertProperties(expected: DatabasesPageData) { assert.deepEqual(this.properties(), expected); } @@ -97,6 +101,7 @@ describe("Databases Page", function() { databases: [], sortSetting: { ascending: true, columnTitle: "name" }, showNodeRegionsColumn: false, + automaticStatsCollectionEnabled: false, }); }); @@ -106,6 +111,7 @@ describe("Databases Page", function() { }); await driver.refreshDatabases(); + await driver.refreshSettings(); driver.assertProperties({ loading: false, @@ -134,6 +140,7 @@ describe("Databases Page", function() { ], sortSetting: { ascending: true, columnTitle: "name" }, showNodeRegionsColumn: false, + automaticStatsCollectionEnabled: false, }); }); 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 3034237e2d0d..08f37ecc016f 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 @@ -23,6 +23,7 @@ import { refreshDatabaseDetails, refreshTableStats, refreshNodes, + refreshSettings, } from "src/redux/apiReducers"; import { AdminUIState } from "src/redux/state"; import { FixLong } from "src/util/fixLong"; @@ -31,6 +32,7 @@ import { selectIsMoreThanOneNode, } from "src/redux/nodes"; import { getNodesByRegionString } from "../utils"; +import { selectAutomaticStatsCollectionEnabled } from "oss/src/redux/clusterSettings"; const { DatabaseDetailsRequest, TableStatsRequest } = cockroach.server.serverpb; @@ -124,6 +126,7 @@ export const mapStateToProps = (state: AdminUIState): DatabasesPageData => ({ databases: selectDatabases(state), sortSetting: sortSettingLocalSetting.selector(state), showNodeRegionsColumn: selectIsMoreThanOneNode(state), + automaticStatsCollectionEnabled: selectAutomaticStatsCollectionEnabled(state), }); export const mapDispatchToProps = { @@ -137,6 +140,7 @@ export const mapDispatchToProps = { return refreshTableStats(new TableStatsRequest({ database, table })); }, refreshNodes, + refreshSettings, onSortingChange: ( _tableName: string, columnName: string,