diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.module.scss b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.module.scss
index 77c1abf2dc2e..6293d67aa9ae 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.module.scss
+++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.module.scss
@@ -30,6 +30,15 @@
&__no-result {
@include text--body-strong;
}
+
+ &__cell-error {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ svg {
+ fill: $colors--warning;
+ }
+ }
}
.sorted-table {
diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx
index 52147407a229..940221186204 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx
+++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx
@@ -46,7 +46,7 @@ import {
TableSchemaDetailsRow,
TableSpanStatsRow,
} from "../api";
-import { checkInfoAvailable } from "../databases";
+import { LoadingCell } from "../databases";
import { Timestamp, Timezone } from "../timestamp";
import { Search } from "../search";
import { Loading } from "../loading";
@@ -562,12 +562,16 @@ export class DatabaseDetailsPage extends React.Component<
Ranges
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.spanStats?.error,
- table.details.spanStats?.range_count,
- ),
+ cell: table => (
+
+ {table.details.spanStats?.range_count}
+
+ ),
sort: table => table.details.spanStats?.range_count,
className: cx("database-table__col-range-count"),
name: "rangeCount",
@@ -581,12 +585,16 @@ export class DatabaseDetailsPage extends React.Component<
Columns
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.schemaDetails?.error,
- table.details.schemaDetails?.columns?.length,
- ),
+ cell: table => (
+
+ {table.details.schemaDetails?.columns?.length}
+
+ ),
sort: table => table.details.schemaDetails?.columns?.length,
className: cx("database-table__col-column-count"),
name: "columnCount",
@@ -619,15 +627,19 @@ export class DatabaseDetailsPage extends React.Component<
Regions
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- null,
- table.details.nodesByRegionString &&
- table.details.nodesByRegionString.length > 0
+ cell: table => (
+
+ {table.details.nodesByRegionString &&
+ table.details.nodesByRegionString.length > 0
? table.details.nodesByRegionString
- : null,
- ),
+ : null}
+
+ ),
sort: table => table.details.nodesByRegionString,
className: cx("database-table__col--regions"),
name: "regions",
@@ -652,16 +664,19 @@ export class DatabaseDetailsPage extends React.Component<
% of Live Data
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.spanStats?.error,
- table.details.spanStats ? (
+ cell: table => (
+
+ {table.details.spanStats ? (
- ) : null,
- ),
+ ) : null}
+
+ ),
sort: table => table.details.spanStats?.live_percentage,
- className: cx("database-table__col-column-count"),
name: "livePercentage",
},
{
@@ -673,16 +688,20 @@ export class DatabaseDetailsPage extends React.Component<
Table Stats Last Updated
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.statsLastUpdated?.error,
+ cell: table => (
+
,
- ),
+ />
+
+ ),
sort: table => table.details.statsLastUpdated,
className: cx("database-table__col--table-stats"),
name: "tableStatsUpdated",
@@ -721,12 +740,16 @@ export class DatabaseDetailsPage extends React.Component<
Users
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.grants?.error,
- table.details.grants?.roles.length,
- ),
+ cell: table => (
+
+ {table.details.grants?.roles.length}
+
+ ),
sort: table => table.details.grants?.roles.length,
className: cx("database-table__col-user-count"),
name: "userCount",
@@ -737,12 +760,16 @@ export class DatabaseDetailsPage extends React.Component<
Roles
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.grants?.error,
- table.details.grants?.roles.join(", "),
- ),
+ cell: table => (
+
+ {table.details.grants?.roles.join(", ")}
+
+ ),
sort: table => table.details.grants?.roles.join(", "),
className: cx("database-table__col-roles"),
name: "roles",
@@ -753,12 +780,16 @@ export class DatabaseDetailsPage extends React.Component<
Grants
),
- cell: table =>
- checkInfoAvailable(
- table.requestError,
- table.details.grants?.error,
- table.details.grants?.privileges.join(", "),
- ),
+ cell: table => (
+
+ {table.details.grants?.privileges.join(", ")}
+
+ ),
sort: table => table.details.grants?.privileges?.join(", "),
className: cx("database-table__col-grants"),
name: "grants",
diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/tableCells.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/tableCells.tsx
index 2016352e9ed4..70d84d2c1aea 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/tableCells.tsx
+++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/tableCells.tsx
@@ -25,7 +25,7 @@ import * as format from "../util/format";
import { Breadcrumbs } from "../breadcrumbs";
import { CaretRight } from "../icon/caretRight";
import { CockroachCloudContext } from "../contexts";
-import { checkInfoAvailable, getNetworkErrorMessage } from "../databases";
+import { LoadingCell, getNetworkErrorMessage } from "../databases";
import { DatabaseIcon } from "../icon/databaseIcon";
import styles from "./databaseDetailsPage.module.scss";
@@ -45,13 +45,18 @@ export const DiskSizeCell = ({
}): JSX.Element => {
return (
<>
- {checkInfoAvailable(
- table.requestError,
- table.details?.spanStats?.error,
- table.details?.spanStats?.approximate_disk_bytes
- ? format.Bytes(table.details?.spanStats?.approximate_disk_bytes)
- : null,
- )}
+ {
+
+ {table.details?.spanStats?.approximate_disk_bytes
+ ? format.Bytes(table.details?.spanStats?.approximate_disk_bytes)
+ : null}
+
+ }
>
);
};
@@ -111,11 +116,16 @@ export const IndexesCell = ({
}): JSX.Element => {
const elem = (
<>
- {checkInfoAvailable(
- table.requestError,
- table.details?.schemaDetails?.error,
- table.details?.schemaDetails?.indexes?.length,
- )}
+ {
+
+ {table.details?.schemaDetails?.indexes?.length}
+
+ }
>
);
// If index recommendations are not enabled or we don't have any index recommendations,
diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss
index f685f2248f52..af6569d122af 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss
+++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.module.scss
@@ -40,6 +40,15 @@
@include text--body;
overflow-wrap: anywhere;
}
+
+ &__error-cell{
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ svg {
+ fill: $colors--warning;
+ }
+ }
}
.icon {
diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx
index 630f1ed70c00..f88834c1409e 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx
+++ b/pkg/ui/workspaces/cluster-ui/src/databaseTablePage/databaseTablePage.tsx
@@ -52,7 +52,7 @@ import {
TableSchemaDetailsRow,
TableSpanStatsRow,
} from "../api";
-import { checkInfoAvailable } from "../databases";
+import { LoadingCell } from "../databases";
import {
ActionCell,
@@ -469,55 +469,82 @@ export class DatabaseTablePage extends React.Component<
+ {details.spanStats?.approximate_disk_bytes
+ ? format.Bytes(
+ details.spanStats?.approximate_disk_bytes,
+ )
+ : null}
+
+ }
/>
+ {details.replicaData?.replicaCount}
+
+ }
/>
+ {details.spanStats?.range_count}
+
+ }
/>
,
- )}
+ value={
+
+
+
+ }
/>
{details.statsLastUpdated && (
,
- )}
+ value={
+
+
+
+ }
/>
)}
{this.props.automaticStatsCollectionEnabled !=
@@ -552,14 +579,21 @@ export class DatabaseTablePage extends React.Component<
{this.props.showNodeRegionsSection && (
+ {details.nodesByRegionString &&
details.nodesByRegionString?.length
- ? details.nodesByRegionString
- : null,
- )}
+ ? details.nodesByRegionString
+ : null}
+
+ }
/>
)}
+ {details.indexData?.indexes?.join(", ")}
+
+ }
className={cx(
"database-table-page__indexes--value",
)}
diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts b/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.tsx
similarity index 72%
rename from pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts
rename to pkg/ui/workspaces/cluster-ui/src/databases/util.spec.tsx
index 9e3a6ba2c31c..234a8a37be51 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.ts
+++ b/pkg/ui/workspaces/cluster-ui/src/databases/util.spec.tsx
@@ -8,6 +8,9 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
+import React from "react";
+import { render } from "@testing-library/react";
+
import { INodeStatus } from "../util";
import {
@@ -17,6 +20,7 @@ import {
getNodeIdsFromStoreIds,
normalizePrivileges,
normalizeRoles,
+ LoadingCell,
} from "./util";
describe("Getting nodes by region string", () => {
@@ -174,3 +178,74 @@ describe("Normalize roles", () => {
expect(result).toEqual(["admin", "public"]);
});
});
+
+describe("LoadingCell", () => {
+ it("renders empty data", () => {
+ const { getByText } = render(
+
+ {null}
+ ,
+ );
+
+ expect(getByText("No data")).not.toBeNull();
+ });
+ it("renders with undefined children", () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText("No data")).not.toBeNull();
+ });
+ it("renders skeleton heading when loading", () => {
+ const { getByRole } = render(
+
+ {null}
+ ,
+ );
+
+ expect(getByRole("heading")).not.toBeNull();
+ });
+ it("renders error name and status icon", () => {
+ const { getByRole } = render(
+
+ {null}
+ ,
+ );
+
+ // TODO(davidh): rendering of antd Tooltip component doesn't work
+ // here and hence can't be directly tested to contain the error
+ // name.
+ expect(getByRole("status")).not.toBeNull();
+ });
+ it("renders children with no error", () => {
+ const { getByText } = render(
+
+ inner data
+ ,
+ );
+
+ expect(getByText("inner data")).not.toBeNull();
+ });
+ it("renders children with error together", () => {
+ const { getByText, getByRole } = render(
+
+ inner data
+ ,
+ );
+
+ expect(getByRole("status")).not.toBeNull();
+ expect(getByText("inner data")).not.toBeNull();
+ });
+});
diff --git a/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx b/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx
index 734f7113ea24..e7be98df2d33 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx
+++ b/pkg/ui/workspaces/cluster-ui/src/databases/util.tsx
@@ -10,7 +10,8 @@
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import React from "react";
-import { Tooltip } from "antd";
+import { Skeleton, Tooltip } from "antd";
+import { Caution } from "@cockroachlabs/icons";
import {
isMaxSizeError,
@@ -240,34 +241,56 @@ export function buildIndexStatToRecommendationsMap(
return recommendationsMap;
}
-export function checkInfoAvailable(
- requestError: Error,
- queryError: Error,
- cell: React.ReactNode,
-): React.ReactNode {
+interface LoadingCellProps {
+ requestError: Error;
+ queryError?: Error;
+ loading: boolean;
+ errorClassName: string;
+}
+
+export const LoadingCell: React.FunctionComponent = ({
+ loading,
+ requestError,
+ queryError,
+ errorClassName,
+ children,
+}) => {
+ if (loading) {
+ return (
+
+ );
+ }
+
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";
}
+
+ let childrenOrNoData = <>{children}>;
+ if (children == null) {
+ childrenOrNoData = <>{"No data"}>;
+ }
+
// If we encounter an error gathering data for this cell,
- // render it "unavailable" with a tooltip message for the error.
+ // render a warning icon with a tooltip message for the error.
if (tooltipMsg !== "") {
return (
- (unavailable)
+
+ {childrenOrNoData}
);
+ } else {
+ return childrenOrNoData;
}
- return cell;
-}
+};
export const getNetworkErrorMessage = (requestError: Error): string => {
return `Encountered a network error: ${requestError.message}`;
diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx
index 3e8c4a1e1c72..7fb06cad6c38 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx
+++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databaseTableCells.tsx
@@ -19,7 +19,7 @@ import { EncodeDatabaseUri } from "../util";
import { StackIcon } from "../icon/stackIcon";
import { CockroachCloudContext } from "../contexts";
import {
- checkInfoAvailable,
+ LoadingCell,
getNetworkErrorMessage,
getQueryErrorMessage,
} from "../databases";
@@ -34,19 +34,18 @@ interface CellProps {
database: DatabasesPageDataDatabase;
}
-export const DiskSizeCell = ({ database }: CellProps): JSX.Element => {
- return (
- <>
- {checkInfoAvailable(
- database.spanStatsRequestError,
- database.spanStats?.error,
- database.spanStats?.approximate_disk_bytes
- ? format.Bytes(database.spanStats?.approximate_disk_bytes)
- : null,
- )}
- >
- );
-};
+export const DiskSizeCell = ({ database }: CellProps) => (
+
+ {database.spanStats?.approximate_disk_bytes
+ ? format.Bytes(database.spanStats?.approximate_disk_bytes)
+ : null}
+
+);
export const IndexRecCell = ({ database }: CellProps): JSX.Element => {
const text =
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 b9362859d1e0..852f8ea57e53 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.module.scss
+++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.module.scss
@@ -31,6 +31,15 @@
&__no-result {
@include text--body-strong;
}
+
+ &__cell-error {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ svg {
+ fill: $colors--warning;
+ }
+ }
}
.sorted-table {
diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx
index 9e5d8ee010ae..fb004c1fcde5 100644
--- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx
+++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx
@@ -51,7 +51,7 @@ import {
SqlApiQueryResponse,
SqlExecutionErrorMessage,
} from "../api";
-import { checkInfoAvailable } from "../databases";
+import { LoadingCell } from "../databases";
import {
DatabaseNameCell,
@@ -562,12 +562,16 @@ export class DatabasesPage extends React.Component<
Tables
),
- cell: database =>
- checkInfoAvailable(
- database.detailsRequestError,
- database.tables?.error,
- database.tables?.tables?.length,
- ),
+ cell: database => (
+
+ {database.tables?.tables?.length}
+
+ ),
sort: database => database.tables?.tables.length ?? 0,
className: cx("databases-table__col-table-count"),
name: "tableCount",
@@ -581,12 +585,16 @@ export class DatabasesPage extends React.Component<
Range Count
),
- cell: database =>
- checkInfoAvailable(
- database.spanStatsRequestError,
- database.spanStats?.error,
- database.spanStats?.range_count,
- ),
+ cell: database => (
+
+ {database.spanStats?.range_count}
+
+ ),
sort: database => database.spanStats?.range_count,
className: cx("databases-table__col-range-count"),
name: "rangeCount",
@@ -600,12 +608,15 @@ export class DatabasesPage extends React.Component<
{this.props.isTenant ? "Regions" : "Regions/Nodes"}
),
- cell: database =>
- checkInfoAvailable(
- database.detailsRequestError,
- null,
- database.nodesByRegionString ? database.nodesByRegionString : null,
- ),
+ cell: database => (
+
+ {database.nodesByRegionString ? database.nodesByRegionString : null}
+
+ ),
sort: database => database.nodesByRegionString,
className: cx("databases-table__col-node-regions"),
name: "nodeRegions",