Skip to content

Commit

Permalink
ui: Add search and filtering to the databases pages
Browse files Browse the repository at this point in the history
Part of #68826, #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.
  • Loading branch information
gtr committed Dec 9, 2022
1 parent 153448e commit 54bd1f9
Show file tree
Hide file tree
Showing 13 changed files with 707 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {

import * as H from "history";
import moment from "moment";
import { defaultFilters } from "src/queryFilter";
const history = H.createHashHistory();

const withLoadingIndicator: DatabaseDetailsPageProps = {
Expand All @@ -49,6 +50,9 @@ const withLoadingIndicator: DatabaseDetailsPageProps = {
refreshDatabaseDetails: () => {},
refreshTableDetails: () => {},
refreshTableStats: () => {},
search: null,
filters: defaultFilters,
nodeRegions: {},
location: history.location,
history,
match: {
Expand Down Expand Up @@ -79,6 +83,9 @@ const withoutData: DatabaseDetailsPageProps = {
refreshDatabaseDetails: () => {},
refreshTableDetails: () => {},
refreshTableStats: () => {},
search: null,
filters: defaultFilters,
nodeRegions: {},
location: history.location,
history,
match: {
Expand Down Expand Up @@ -144,6 +151,9 @@ const withData: DatabaseDetailsPageProps = {
refreshDatabaseDetails: () => {},
refreshTableDetails: () => {},
refreshTableStats: () => {},
search: null,
filters: defaultFilters,
nodeRegions: {},
location: history.location,
history,
match: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -41,6 +41,15 @@ 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";
import { TableStatistics } from "src/tableStatistics";

const cx = classNames.bind(styles);
const sortableTableCx = classNames.bind(sortableTableStyles);
Expand All @@ -61,6 +70,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;
Expand All @@ -80,6 +93,7 @@ const sortableTableCx = classNames.bind(sortableTableStyles);
// lastError: Error;
// replicationSizeInBytes: number;
// rangeCount: number;
// nodes: number[];
// nodesByRegionString: string;
// };
// }[];
Expand All @@ -92,6 +106,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;
}
Expand Down Expand Up @@ -124,13 +142,20 @@ 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;
}

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;
Expand All @@ -147,12 +172,35 @@ export enum ViewMode {

interface DatabaseDetailsPageState {
pagination: ISortedTablePagination;
filters?: Filters;
activeFilters?: number;
lastStatsError: Error;
lastDetailsError: Error;
}

class DatabaseSortedTable extends SortedTable<DatabaseDetailsPageDataTable> {}

// 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
Expand Down Expand Up @@ -286,6 +334,120 @@ export class DatabaseDetailsPage extends React.Component<
}
};

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(
{
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, 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;

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(
{
Expand Down Expand Up @@ -594,11 +756,42 @@ export class DatabaseDetailsPage extends React.Component<
}

render(): React.ReactElement {
const { search, filters, isTenant, nodeRegions } = this.props;

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 ? (
<PageConfigItem>
<Filter
hideAppNames={true}
regions={regions}
hideTimeLabel={true}
nodes={nodes.map(n => "n" + n.toString())}
activeFilters={activeFilters}
filters={defaultFilters}
onSubmitFilters={this.onSubmitFilters}
showNodes={!isTenant && nodes.length > 1}
showRegions={regions.length > 1}
/>
</PageConfigItem>
) : (
<></>
);

return (
<div className="root table-area">
<section className={baseHeadingClasses.wrapper}>
Expand Down Expand Up @@ -631,28 +824,33 @@ export class DatabaseDetailsPage extends React.Component<
View: {this.props.viewMode}
</Dropdown>
</PageConfigItem>
<PageConfigItem>
<Search
onSubmit={this.onSubmitSearchField}
onClear={this.onClearSearchField}
defaultValue={search}
placeholder={"Search Tables"}
/>
</PageConfigItem>
{filterComponent}
</PageConfig>

<section className={sortableTableCx("cl-table-container")}>
<div className={statisticsClasses.statistic}>
<h4 className={statisticsClasses.countTitle}>
<ResultsPerPageLabel
pagination={{
...this.state.pagination,
total: this.props.tables.length,
}}
pageName={this.props.tables.length == 1 ? "table" : "tables"}
/>
</h4>
</div>
<TableStatistics
pagination={this.state.pagination}
totalCount={tablesToDisplay.length}
arrayItemName="tables"
activeFilters={activeFilters}
onClearFilters={this.onClearFilters}
/>
<Loading
loading={this.props.loading}
page={"databases"}
error={this.props.lastError}
render={() => (
<DatabaseSortedTable
className={cx("database-table")}
data={this.props.tables}
data={tablesToDisplay}
columns={this.columns()}
sortSetting={sortSetting}
onChangeSortSetting={this.changeSortSetting}
Expand Down
Loading

0 comments on commit 54bd1f9

Please sign in to comment.