From 1a3e8b6755a4b686f1e1979c1fa7104f861d5787 Mon Sep 17 00:00:00 2001 From: gtr Date: Mon, 28 Nov 2022 11:58:50 -0500 Subject: [PATCH] ui: Add search and filtering to the databases pages Part of [#68826](https://github.com/cockroachdb/cockroach/issues/68826), [68825](https://github.com/cockroachdb/cockroach/issues/68825). Previously, the databases and databases details pages did not contain search and filter components that the txns and stmts pages did. This change adds search and filter components to both the databases and databases details pages. The search box filters by database/table name while the filter allows filtering by node and region. Release note (ui change): the databases page and the databases details pages each now contain search and filter components, allowing the ability to search and filter through databases and their tables. --- .../databaseDetailsPage.tsx | 194 +++++++++++- .../databasesPage/databasesPage.stories.tsx | 14 + .../src/databasesPage/databasesPage.tsx | 279 +++++++++++++++++- .../cluster-ui/src/queryFilter/filter.tsx | 4 +- .../localStorage/localStorage.reducer.ts | 7 + .../workspaces/db-console/src/util/fakeApi.ts | 5 +- .../databaseDetailsPage/redux.spec.ts | 11 + .../databases/databaseDetailsPage/redux.ts | 24 ++ .../databases/databasesPage/redux.spec.ts | 26 +- .../views/databases/databasesPage/redux.ts | 20 ++ 10 files changed, 575 insertions(+), 9 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx index 205a762a5936..4feb5652abf9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databaseDetailsPage/databaseDetailsPage.tsx @@ -28,7 +28,7 @@ import { } from "src/sortedtable"; import * as format from "src/util/format"; import { DATE_FORMAT } from "src/util/format"; -import { mvccGarbage, syncHistory } from "../util"; +import { mvccGarbage, syncHistory, unique } from "../util"; import styles from "./databaseDetailsPage.module.scss"; import sortableTableStyles from "src/sortedtable/sortedtable.module.scss"; @@ -41,6 +41,14 @@ import { Caution } from "@cockroachlabs/icons"; import { Anchor } from "../anchor"; import LoadingError from "../sqlActivity/errorComponent"; import { Loading } from "../loading"; +import { Search } from "../search"; +import { + Filter, + Filters, + defaultFilters, + calculateActiveFilters, +} from "src/queryFilter"; +import { UIConfigState } from "src/store"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -61,6 +69,10 @@ const sortableTableCx = classNames.bind(sortableTableStyles); // name: string; // sortSettingTables: SortSetting; // sortSettingGrants: SortSetting; +// search: string; +// filters: Filters; +// nodeRegions: { [nodeId: string]: string }; +// isTenant: boolean; // viewMode: ViewMode; // tables: { // DatabaseDetailsPageDataTable[] // name: string; @@ -80,6 +92,7 @@ const sortableTableCx = classNames.bind(sortableTableStyles); // lastError: Error; // replicationSizeInBytes: number; // rangeCount: number; +// nodes: number[]; // nodesByRegionString: string; // }; // }[]; @@ -92,6 +105,10 @@ export interface DatabaseDetailsPageData { tables: DatabaseDetailsPageDataTable[]; sortSettingTables: SortSetting; sortSettingGrants: SortSetting; + search: string; + filters: Filters; + nodeRegions: { [nodeId: string]: string }; + isTenant?: UIConfigState["isTenant"]; viewMode: ViewMode; showNodeRegionsColumn?: boolean; } @@ -124,6 +141,11 @@ export interface DatabaseDetailsPageDataTableStats { lastError: Error; replicationSizeInBytes: number; rangeCount: number; + // 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. + // regionA(n1,n2), regionB(n3). Used for display in the table's + // "Regions/Nodes" column. nodesByRegionString?: string; } @@ -131,6 +153,8 @@ export interface DatabaseDetailsPageActions { refreshDatabaseDetails: (database: string) => void; refreshTableDetails: (database: string, table: string) => void; refreshTableStats: (database: string, table: string) => void; + onFilterChange?: (value: Filters) => void; + onSearchComplete?: (query: string) => void; onSortingTablesChange?: (columnTitle: string, ascending: boolean) => void; onSortingGrantsChange?: (columnTitle: string, ascending: boolean) => void; onViewModeChange?: (viewMode: ViewMode) => void; @@ -147,12 +171,35 @@ export enum ViewMode { interface DatabaseDetailsPageState { pagination: ISortedTablePagination; + filters?: Filters; + activeFilters?: number; lastStatsError: Error; lastDetailsError: Error; } class DatabaseSortedTable extends SortedTable {} +// filterBySearchQuery returns true if the search query matches the database name. +function filterBySearchQuery( + table: DatabaseDetailsPageDataTable, + search: string, +): boolean { + const matchString = table.name.toLowerCase(); + + if (search.startsWith('"') && search.endsWith('"')) { + search = search.substring(1, search.length - 1); + + return matchString.includes(search) + } + + const res = search + .toLowerCase() + .split(" ") + .every(val => matchString.includes(val)); + + return res; +} + export class DatabaseDetailsPage extends React.Component< DatabaseDetailsPageProps, DatabaseDetailsPageState @@ -286,6 +333,105 @@ export class DatabaseDetailsPage extends React.Component< } }; + onClearSearchField = (): void => { + if (this.props.onSearchComplete) { + this.props.onSearchComplete(""); + } + + syncHistory( + { + q: undefined, + }, + this.props.history, + ); + }; + + onSubmitSearchField = (search: string): void => { + if (this.props.onSearchComplete) { + this.props.onSearchComplete(search); + } + + this.resetPagination(); + syncHistory( + { + q: search, + }, + this.props.history, + ); + }; + + onSubmitFilters = (filters: Filters): void => { + if (this.props.onFilterChange) { + this.props.onFilterChange(filters); + } + + this.setState({ + filters: filters, + activeFilters: calculateActiveFilters(filters), + }); + + this.resetPagination(); + syncHistory( + { + regions: filters.regions, + nodes: filters.nodes, + }, + this.props.history, + ); + }; + + resetPagination = (): void => { + this.setState(prevState => { + return { + pagination: { + current: 1, + pageSize: prevState.pagination.pageSize, + } + }; + }); + }; + + // Returns a list of database tables to the display based on input from the + // search box and the applied filters. + filteredDatabaseTables = (): DatabaseDetailsPageDataTable[] => { + const { + search, + tables, + filters, + isTenant, + nodeRegions, + } = this.props; + + const regionsSelected = filters.regions.length > 0 ? filters.regions.split(",") : []; + const nodesSelected = filters.nodes.length > 0 ? filters.nodes.split(",") : []; + + return tables + .filter( + table => search ? filterBySearchQuery(table, search) : true + ) + .filter( + table => { + if (regionsSelected.length == 0 && nodesSelected.length == 0) return true; + if (isTenant) return true; + + let foundRegion = regionsSelected.length == 0; + let foundNode = nodesSelected.length == 0; + + table.stats.nodes?.forEach(node => { + if (foundRegion || regionsSelected.includes(nodeRegions[node.toString()])) { + foundRegion = true; + } + if (foundNode || nodesSelected.includes("n" + node.toString())) { + foundNode = true; + } + if (foundNode && foundRegion) return true; + }); + + return foundRegion && foundNode; + } + ); + }; + private changeViewMode(viewMode: ViewMode) { syncHistory( { @@ -594,11 +740,46 @@ export class DatabaseDetailsPage extends React.Component< } render(): React.ReactElement { + const { + search, + filters, + isTenant, + nodeRegions, + } = this.props; + + console.log("databaseDetailsPage.isTenant =", isTenant); + + const tablesToDisplay = this.filteredDatabaseTables(); + const activeFilters = calculateActiveFilters(filters); + + const nodes = Object.keys(nodeRegions) + .map(n => Number(n)) + .sort(); + + const regions = unique(Object.values(nodeRegions)); + const sortSetting = this.props.viewMode == ViewMode.Tables ? this.props.sortSettingTables : this.props.sortSettingGrants; + // Only show the filter component when the viewMode is Tables. + const filterComponent = this.props.viewMode == ViewMode.Tables + ? ( + "n" + n.toString())} + activeFilters={activeFilters} + filters={defaultFilters} + onSubmitFilters={this.onSubmitFilters} + showNodes={!isTenant && nodes.length > 1} + showRegions={regions.length > 1} + /> + ) + : (<>); + return (
@@ -631,6 +812,15 @@ export class DatabaseDetailsPage extends React.Component< View: {this.props.viewMode} + + + + {filterComponent}
@@ -652,7 +842,7 @@ export class DatabaseDetailsPage extends React.Component< render={() => ( {}, refreshDatabases: () => {}, refreshSettings: () => {}, @@ -54,6 +58,9 @@ const withoutData: DatabasesPageProps = { ascending: false, columnTitle: "name", }, + search: "", + filters: defaultFilters, + nodeRegions: {}, onSortingChange: () => {}, refreshDatabases: () => {}, refreshSettings: () => {}, @@ -79,6 +86,13 @@ const withData: DatabasesPageProps = { ascending: false, columnTitle: "name", }, + search: "", + filters: defaultFilters, + nodeRegions: { + "1": "gcp-us-east1", + "6": "gcp-us-west1", + "8": "gcp-europe-west1", + }, databases: Array(42).map(() => { return { loading: false, diff --git a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx index 8e4f7591e14a..f9571e2e296d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/databasesPage/databasesPage.tsx @@ -19,8 +19,10 @@ import { Anchor } from "src/anchor"; import { StackIcon } from "src/icon/stackIcon"; import { Pagination, ResultsPerPageLabel } from "src/pagination"; import { BooleanSetting } from "src/settings/booleanSetting"; +import { PageConfig, PageConfigItem } from "src/pageConfig"; import { ColumnDescriptor, + handleSortSettingFromQueryString, ISortedTablePagination, SortedTable, SortSetting, @@ -33,11 +35,21 @@ import { baseHeadingClasses, statisticsClasses, } from "src/transactionsPage/transactionsPageClasses"; -import { syncHistory, tableStatsClusterSetting } from "src/util"; +import { nodeIDAttr, syncHistory, tableStatsClusterSetting, unique } from "src/util"; import booleanSettingStyles from "../settings/booleanSetting.module.scss"; import { CircleFilled } from "../icon"; import LoadingError from "../sqlActivity/errorComponent"; import { Loading } from "../loading"; +import { Search } from "../search"; +import { + calculateActiveFilters, + Filter, + Filters, + defaultFilters, + handleFiltersFromQueryString +} from "../queryFilter"; +import { merge } from "lodash"; +import { UIConfigState } from "src/store"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -57,6 +69,10 @@ const booleanSettingCx = classnames.bind(booleanSettingStyles); // loaded: boolean; // lastError: Error; // sortSetting: SortSetting; +// search: string; +// filters: Filters; +// nodeRegions: { [nodeId: string]: string }; +// isTenant: boolean; // databases: { // DatabasesPageDataDatabase[] // loading: boolean; // loaded: boolean; @@ -64,6 +80,7 @@ const booleanSettingCx = classnames.bind(booleanSettingStyles); // sizeInBytes: number; // tableCount: number; // rangeCount: number; +// nodes: number[]; // nodesByRegionString: string; // missingTables: { // DatabasesPageDataMissingTable[] // loading: boolean; @@ -77,6 +94,10 @@ export interface DatabasesPageData { lastError: Error; databases: DatabasesPageDataDatabase[]; sortSetting: SortSetting; + search: string; + filters: Filters; + nodeRegions: { [nodeId: string]: string }; + isTenant?: UIConfigState["isTenant"]; automaticStatsCollectionEnabled?: boolean; showNodeRegionsColumn?: boolean; } @@ -90,8 +111,11 @@ export interface DatabasesPageDataDatabase { tableCount: number; rangeCount: number; missingTables: DatabasesPageDataMissingTable[]; + // 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. - // regionA(n1,n2), regionB(n3) + // regionA(n1,n2), regionB(n3). Used for display in the table's + // "Regions/Nodes" column. nodesByRegionString?: string; numIndexRecommendations: number; } @@ -114,6 +138,8 @@ export interface DatabasesPageActions { refreshTableStats: (database: string, table: string) => void; refreshSettings: () => void; refreshNodes?: () => void; + onFilterChange?: (value: Filters) => void; + onSearchComplete?: (query: string) => void; onSortingChange?: ( name: string, columnTitle: string, @@ -127,11 +153,32 @@ export type DatabasesPageProps = DatabasesPageData & interface DatabasesPageState { pagination: ISortedTablePagination; + filters?: Filters; + activeFilters?: number; lastDetailsError: Error; } class DatabasesSortedTable extends SortedTable {} +// filterBySearchQuery returns true if the search query matches the database name. +function filterBySearchQuery( + database: DatabasesPageDataDatabase, + search: string, +): boolean { + const matchString = database.name.toLowerCase(); + + if (search.startsWith('"') && search.endsWith('"')) { + search = search.substring(1, search.length - 1); + + return matchString.includes(search) + } + + return search + .toLowerCase() + .split(" ") + .every(val => matchString.includes(val));; +} + export class DatabasesPage extends React.Component< DatabasesPageProps, DatabasesPageState @@ -140,6 +187,7 @@ export class DatabasesPage extends React.Component< super(props); this.state = { + filters: defaultFilters, pagination: { current: 1, pageSize: 20, @@ -147,6 +195,9 @@ export class DatabasesPage extends React.Component< lastDetailsError: null, }; + const stateFromHistory = this.getStateFromHistory(); + this.state = merge(this.state, stateFromHistory); + const { history } = this.props; const searchParams = new URLSearchParams(history.location.search); const ascending = (searchParams.get("ascending") || undefined) === "true"; @@ -163,11 +214,65 @@ export class DatabasesPage extends React.Component< } } + getStateFromHistory = (): Partial => { + const { + history, + search, + sortSetting, + filters, + onFilterChange, + onSearchComplete, + onSortingChange, + } = this.props; + + const searchParams = new URLSearchParams(history.location.search); + + const searchQuery = searchParams.get("q") || undefined; + if (onSearchComplete && searchQuery && search != searchQuery) { + onSearchComplete(searchQuery); + } + + handleSortSettingFromQueryString( + "Databases", + history.location.search, + sortSetting, + onSortingChange, + ); + + const latestFilter = handleFiltersFromQueryString( + history, + filters, + onFilterChange, + ) + + return { + filters: latestFilter, + activeFilters: calculateActiveFilters(latestFilter), + }; + }; + componentDidMount(): void { this.refresh(); } + updateQueryParams(): void { + const { history, search } = this.props; + + // Search + const searchParams = new URLSearchParams(history.location.search); + const searchQueryString = searchParams.get("q") || ""; + if (search && search != searchQueryString) { + syncHistory( + { + q: search, + }, + history, + ); + } + } + componentDidUpdate(): void { + this.updateQueryParams(); this.refresh(); } @@ -232,6 +337,132 @@ export class DatabasesPage extends React.Component< } }; + resetPagination = (): void => { + this.setState(prevState => { + return { + pagination: { + current: 1, + pageSize: prevState.pagination.pageSize, + } + }; + }); + }; + + onClearSearchField = (): void => { + if (this.props.onSearchComplete) { + this.props.onSearchComplete(""); + } + + syncHistory( + { + q: undefined, + }, + this.props.history, + ); + }; + + onClearFilters = (): void => { + if (this.props.onFilterChange) { + this.props.onFilterChange(defaultFilters); + } + + this.setState({ + filters: defaultFilters, + activeFilters: 0, + }); + + this.resetPagination(); + syncHistory( + { + regions: undefined, + nodes: undefined, + }, + this.props.history, + ); + } + + onSubmitSearchField = (search: string): void => { + if (this.props.onSearchComplete) { + this.props.onSearchComplete(search); + } + + this.resetPagination(); + syncHistory( + { + q: search, + }, + this.props.history, + ); + }; + + onSubmitFilters = (filters: Filters): void => { + if (this.props.onFilterChange) { + this.props.onFilterChange(filters); + } + + this.setState({ + filters: filters, + activeFilters: calculateActiveFilters(filters), + }); + + this.resetPagination(); + syncHistory( + { + app: filters.app, + timeNumber: filters.timeNumber, + timeUnit: filters.timeUnit, + fullScan: filters.fullScan.toString(), + sqlType: filters.sqlType, + database: filters.database, + regions: filters.regions, + nodes: filters.nodes, + }, + this.props.history, + ); + }; + + // Returns a list of databses to the display based on input from the search + // box and the applied filters. + filteredDatabasesData = (): DatabasesPageDataDatabase[] => { + const { + search, + databases, + filters, + isTenant, + nodeRegions, + } = this.props; + + // The regions and nodes selected from the filter dropdown. + const regionsSelected = filters.regions.length > 0 ? filters.regions.split(",") : []; + const nodesSelected = filters.nodes.length > 0 ? filters.nodes.split(",") : []; + + return databases + .filter( + db => search ? filterBySearchQuery(db, search) : true + ) + .filter( + db => { + if (regionsSelected.length == 0 && nodesSelected.length == 0) return true; + if (isTenant) return true; + + let foundRegion = regionsSelected.length == 0; + let foundNode = nodesSelected.length == 0; + + db.nodes?.forEach(node => { + if (foundRegion || regionsSelected.includes(nodeRegions[node.toString()])) { + foundRegion = true; + } + if (foundNode || nodesSelected.includes("n" + node.toString())) { + foundNode = true; + } + if (foundNode && foundRegion) return true; + }); + + return foundRegion && foundNode; + } + ); + } + private renderIndexRecommendations = ( database: DatabasesPageDataDatabase, ): React.ReactNode => { @@ -369,6 +600,25 @@ export class DatabasesPage extends React.Component< const displayColumns = this.columns.filter( col => col.showByDefault !== false, ); + + const { + filters, + search, + nodeRegions, + isTenant, + } = this.props; + + console.log("databasesPage.isTenant =", isTenant); + + const databasesToDisplay = this.filteredDatabasesData(); + const activeFilters = calculateActiveFilters(filters); + + const nodes = Object.keys(nodeRegions) + .map(n => Number(n)) + .sort(); + + const regions = unique(Object.values(nodeRegions)); + return (
@@ -396,6 +646,29 @@ export class DatabasesPage extends React.Component< )}
+ + + + + + "n" + n.toString())} + activeFilters={activeFilters} + filters={defaultFilters} + onSubmitFilters={this.onSubmitFilters} + showNodes={!isTenant && nodes.length > 1} + showRegions={regions.length > 1} + /> + +

( { showRegions, showNodes, timeLabel, + hideTimeLabel, showUsername, showSessionStatus, showSchemaInsightTypes, @@ -672,7 +674,7 @@ export class Filter extends React.Component { {showSqlType ? sqlTypeFilter : ""} {showRegions ? regionsFilter : ""} {showNodes ? nodesFilter : ""} - {filters.timeUnit && ( + {hideTimeLabel ? "" : filters.timeUnit && ( <>
{timeLabel diff --git a/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts index 8f0ca42fd25d..194336e5a7c6 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/localStorage/localStorage.reducer.ts @@ -41,11 +41,13 @@ export type LocalStorageState = { "filters/ActiveTransactionsPage": Filters; "filters/StatementsPage": Filters; "filters/TransactionsPage": Filters; + "filters/DatabasesPage": string; "filters/SessionsPage": Filters; "filters/InsightsPage": WorkloadInsightEventFilters; "filters/SchemaInsightsPage": Filters; "search/StatementsPage": string; "search/TransactionsPage": string; + "search/DatabasesPage": string; "typeSetting/JobsPage": number; "statusSetting/JobsPage": string; "showSetting/JobsPage": string; @@ -169,6 +171,9 @@ const initialState: LocalStorageState = { "filters/TransactionsPage": JSON.parse(localStorage.getItem("filters/TransactionsPage")) || defaultFilters, + "filters/DatabasesPage": + JSON.parse(localStorage.getItem("filters/DatabasessPage")) || + defaultFilters, "filters/SessionsPage": JSON.parse(localStorage.getItem("filters/SessionsPage")) || defaultFilters, "filters/InsightsPage": @@ -181,6 +186,8 @@ const initialState: LocalStorageState = { JSON.parse(localStorage.getItem("search/StatementsPage")) || null, "search/TransactionsPage": JSON.parse(localStorage.getItem("search/TransactionsPage")) || null, + "search/DatabasesPage": + JSON.parse(localStorage.getItem("search/DatabasesPage")) || null, "typeSetting/JobsPage": JSON.parse(localStorage.getItem("typeSetting/JobsPage")) || defaultJobTypeSetting, diff --git a/pkg/ui/workspaces/db-console/src/util/fakeApi.ts b/pkg/ui/workspaces/db-console/src/util/fakeApi.ts index 1e6f421655f3..45f3035a5846 100644 --- a/pkg/ui/workspaces/db-console/src/util/fakeApi.ts +++ b/pkg/ui/workspaces/db-console/src/util/fakeApi.ts @@ -72,7 +72,10 @@ export function buildSQLApiDatabasesResponse(databases: string[]) { owner: "root", primary_region: null, secondary_region: null, - regions: [], + regions: [ + "gcp-europe-west1", + "gcp-europe-west2", + ], survival_goal: null, }; }); 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 d5c7d769df7c..948ba46d2da3 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 @@ -18,6 +18,7 @@ import { DatabaseDetailsPageData, DatabaseDetailsPageDataTableDetails, DatabaseDetailsPageDataTableStats, + defaultFilters, util, ViewMode, } from "@cockroachlabs/cluster-ui"; @@ -127,6 +128,9 @@ describe("Database Details Page", function () { loaded: false, lastError: undefined, name: "things", + search: null, + filters: defaultFilters, + nodeRegions: {}, showNodeRegionsColumn: false, viewMode: ViewMode.Tables, sortSettingTables: { ascending: true, columnTitle: "name" }, @@ -147,6 +151,9 @@ describe("Database Details Page", function () { loaded: true, lastError: null, name: "things", + search: null, + filters: defaultFilters, + nodeRegions: {}, showNodeRegionsColumn: false, viewMode: ViewMode.Tables, sortSettingTables: { ascending: true, columnTitle: "name" }, @@ -173,6 +180,7 @@ describe("Database Details Page", function () { loading: false, loaded: false, lastError: undefined, + nodes: [], replicationSizeInBytes: 0, rangeCount: 0, nodesByRegionString: "", @@ -199,6 +207,7 @@ describe("Database Details Page", function () { loading: false, loaded: false, lastError: undefined, + nodes: [], replicationSizeInBytes: 0, rangeCount: 0, nodesByRegionString: "", @@ -451,6 +460,7 @@ describe("Database Details Page", function () { loading: false, loaded: true, lastError: null, + nodes: [], replicationSizeInBytes: 44040192, rangeCount: 4200, nodesByRegionString: "", @@ -460,6 +470,7 @@ describe("Database Details Page", function () { loading: false, loaded: true, lastError: null, + nodes: [], replicationSizeInBytes: 8675309, rangeCount: 1023, nodesByRegionString: "", 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 2b16d9a7184d..41a5eba494e8 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 @@ -14,6 +14,8 @@ import { LocalSetting } from "src/redux/localsettings"; import _ from "lodash"; import { DatabaseDetailsPageData, + defaultFilters, + Filters, util, ViewMode, } from "@cockroachlabs/cluster-ui"; @@ -89,6 +91,18 @@ const viewModeLocalSetting = new LocalSetting( ViewMode.Tables, ); +const filtersLocalTablesSetting = new LocalSetting( + "filters/DatabasesDetailsTablesPage", + (state: AdminUIState) => state.localSettings, + defaultFilters, +); + +const searchLocalTablesSetting = new LocalSetting( + "search/DatabasesDetailsTablesPage", + (state: AdminUIState) => state.localSettings, + null, +) + export const mapStateToProps = createSelector( (_state: AdminUIState, props: RouteComponentProps): string => getMatchParamByName(props.match, databaseNameAttr), @@ -101,6 +115,8 @@ export const mapStateToProps = createSelector( state => viewModeLocalSetting.selector(state), state => sortSettingTablesLocalSetting.selector(state), state => sortSettingGrantsLocalSetting.selector(state), + state => filtersLocalTablesSetting.selector(state), + state => searchLocalTablesSetting.selector(state), ( database, databaseDetails, @@ -111,6 +127,8 @@ export const mapStateToProps = createSelector( viewMode, sortSettingTables, sortSettingGrants, + filtersLocalTables, + searchLocalTables, ): DatabaseDetailsPageData => { return { loading: !!databaseDetails[database]?.inFlight, @@ -121,6 +139,9 @@ export const mapStateToProps = createSelector( viewMode, sortSettingTables, sortSettingGrants, + filters: filtersLocalTables, + search: searchLocalTables, + nodeRegions: nodeRegions, tables: _.map(databaseDetails[database]?.data?.table_names, table => { const tableId = generateTableID(database, table); @@ -164,6 +185,7 @@ export const mapStateToProps = createSelector( replicationSizeInBytes: FixLong( stats?.data?.approximate_disk_bytes || 0, ).toNumber(), + nodes: nodes, rangeCount: FixLong(stats?.data?.range_count || 0).toNumber(), nodesByRegionString: getNodesByRegionString(nodes, nodeRegions), }, @@ -196,4 +218,6 @@ export const mapDispatchToProps = { ascending: ascending, columnTitle: columnName, }), + onSearchComplete: (query: string) => searchLocalTablesSetting.set(query), + onFilterChange: (filters: Filters) => filtersLocalTablesSetting.set(filters), }; 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 53deeb9e0c68..491120f3a647 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 @@ -17,6 +17,7 @@ import { DatabasesPageData, DatabasesPageDataDatabase, DatabasesPageDataMissingTable, + defaultFilters, } from "@cockroachlabs/cluster-ui"; import { AdminUIState, createAdminUIStore } from "src/redux/state"; @@ -106,6 +107,9 @@ describe("Databases Page", function () { loaded: false, lastError: undefined, databases: [], + search: null, + filters: defaultFilters, + nodeRegions: {}, sortSetting: { ascending: true, columnTitle: "name" }, automaticStatsCollectionEnabled: true, showNodeRegionsColumn: false, @@ -133,6 +137,7 @@ describe("Databases Page", function () { loaded: false, lastError: undefined, name: "system", + nodes: [], sizeInBytes: 0, tableCount: 0, rangeCount: 0, @@ -145,6 +150,7 @@ describe("Databases Page", function () { loaded: false, lastError: undefined, name: "test", + nodes: [], sizeInBytes: 0, tableCount: 0, rangeCount: 0, @@ -153,6 +159,9 @@ describe("Databases Page", function () { numIndexRecommendations: 0, }, ], + search: null, + filters: defaultFilters, + nodeRegions: {}, sortSetting: { ascending: true, columnTitle: "name" }, showNodeRegionsColumn: false, automaticStatsCollectionEnabled: true, @@ -165,6 +174,7 @@ describe("Databases Page", function () { fakeApi.stubDatabaseDetails("system", { table_names: ["foo", "bar"], stats: { + node_ids: [1, 2, 4], missing_tables: [], range_count: new Long(3), approximate_disk_bytes: new Long(7168), @@ -174,6 +184,7 @@ describe("Databases Page", function () { fakeApi.stubDatabaseDetails("test", { table_names: ["widgets"], stats: { + node_ids: [3, 5], missing_tables: [], range_count: new Long(42), approximate_disk_bytes: new Long(1234), @@ -189,10 +200,11 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "system", + nodes: [1, 2, 4], sizeInBytes: 7168, tableCount: 2, rangeCount: 3, - nodesByRegionString: "", + nodesByRegionString: "undefined(n1,n2,n4)", missingTables: [], numIndexRecommendations: 0, }); @@ -202,10 +214,11 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "test", + nodes: [3, 5], sizeInBytes: 1234, tableCount: 1, rangeCount: 42, - nodesByRegionString: "", + nodesByRegionString: "undefined(n3,n5)", missingTables: [], numIndexRecommendations: 0, }); @@ -233,6 +246,7 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "system", + nodes: [], sizeInBytes: 7168, tableCount: 2, rangeCount: 3, @@ -268,6 +282,7 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "system", + nodes: [], sizeInBytes: 8192, tableCount: 2, rangeCount: 8, @@ -294,6 +309,7 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "system", + nodes: [], sizeInBytes: 0, tableCount: 2, rangeCount: 0, @@ -332,6 +348,7 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "system", + nodes: [], sizeInBytes: 7168, tableCount: 2, rangeCount: 3, @@ -347,6 +364,7 @@ describe("Databases Page", function () { loaded: true, lastError: null, name: "system", + nodes: [], sizeInBytes: 8192, tableCount: 2, rangeCount: 8, @@ -358,3 +376,7 @@ describe("Databases Page", function () { }); }); }); + +// function makeStateWithNodeRegions() { +// const store = createAdminUIStore(createMemoryHistory()); +// } 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 447d12b67fac..65a5a076bc4a 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 @@ -14,6 +14,8 @@ import { LocalSetting } from "src/redux/localsettings"; import { DatabasesPageData, DatabasesPageDataDatabase, + defaultFilters, + Filters, } from "@cockroachlabs/cluster-ui"; import { cockroach } from "src/js/protos"; @@ -57,6 +59,18 @@ const sortSettingLocalSetting = new LocalSetting( { ascending: true, columnTitle: "name" }, ); +const filtersLocalSetting = new LocalSetting( + "filters/DatabasesPage", + (state: AdminUIState) => state.localSettings, + defaultFilters, +); + +const searchLocalSetting = new LocalSetting( + "search/DatabsesPage", + (state: AdminUIState) => state.localSettings, + null, +); + const selectDatabases = createSelector( (state: AdminUIState) => state.cachedData.databases.data?.databases, (state: AdminUIState) => state.cachedData.databaseDetails, @@ -116,6 +130,7 @@ const selectDatabases = createSelector( sizeInBytes: sizeInBytes, tableCount: details?.data?.table_names?.length || 0, rangeCount: rangeCount, + nodes: nodes, nodesByRegionString, numIndexRecommendations, missingTables: missingTables.map(table => { @@ -134,6 +149,9 @@ export const mapStateToProps = (state: AdminUIState): DatabasesPageData => ({ lastError: selectLastError(state), databases: selectDatabases(state), sortSetting: sortSettingLocalSetting.selector(state), + filters: filtersLocalSetting.selector(state), + search: searchLocalSetting.selector(state), + nodeRegions: nodeRegionsByIDSelector(state), automaticStatsCollectionEnabled: selectAutomaticStatsCollectionEnabled(state), showNodeRegionsColumn: selectIsMoreThanOneNode(state), }); @@ -159,4 +177,6 @@ export const mapDispatchToProps = { ascending: ascending, columnTitle: columnName, }), + onSearchComplete: (query: string) => searchLocalSetting.set(query), + onFilterChange: (filters: Filters) => filtersLocalSetting.set(filters), };