From 1cd2a34cf5f2f66fc06260d2746b0c01c13d65ed Mon Sep 17 00:00:00 2001 From: Marylia Gutierrez Date: Thu, 18 Nov 2021 18:15:59 -0500 Subject: [PATCH] ui: save filters on cache for Transactions page Previously, a sort selection was not maintained when the page change (e.g. changing tabs/pages). This commits saves the selected value to be used. Partially adresses #71851 Release note: None --- .../src/queryFilter/filter.spec.tsx | 77 ++++++++++++ .../cluster-ui/src/queryFilter/filter.tsx | 105 +++++++++++++++- .../src/sortedtable/sortedtable.tsx | 88 +++++++++++++- .../src/statementsPage/statementsPage.tsx | 113 ++++++------------ .../statementsPageConnected.tsx | 2 +- .../localStorage/localStorage.reducer.ts | 4 + .../transactionsPage/transactions.fixture.ts | 11 ++ .../transactionsPage.selectors.ts | 5 + .../transactionsPage.stories.tsx | 17 +++ .../src/transactionsPage/transactionsPage.tsx | 96 ++++++++++----- .../transactionsPageConnected.tsx | 16 ++- .../views/transactions/transactionsPage.tsx | 14 ++- 12 files changed, 432 insertions(+), 116 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.spec.tsx diff --git a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.spec.tsx new file mode 100644 index 000000000000..4244af2e3e51 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.spec.tsx @@ -0,0 +1,77 @@ +// 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 { Filters, getFiltersFromQueryString } from "./filter"; + +describe("Test filter functions", (): void => { + describe("Test get filters from query string", (): void => { + it("no values on query string", (): void => { + const expectedFilters: Filters = { + app: "", + timeNumber: "0", + timeUnit: "seconds", + fullScan: false, + sqlType: "", + database: "", + regions: "", + nodes: "", + }; + const resultFilters = getFiltersFromQueryString(""); + expect(resultFilters).toEqual(expectedFilters); + }); + }); + + it("different values from default values on query string", (): void => { + const expectedFilters: Filters = { + app: "$ internal", + timeNumber: "1", + timeUnit: "milliseconds", + fullScan: true, + sqlType: "DML", + database: "movr", + regions: "us-central", + nodes: "n1,n2", + }; + const resultFilters = getFiltersFromQueryString( + "app=%24+internal&timeNumber=1&timeUnit=milliseconds&fullScan=true&sqlType=DML&database=movr®ions=us-central&nodes=n1,n2", + ); + expect(resultFilters).toEqual(expectedFilters); + }); + + it("testing boolean with full scan = true", (): void => { + const expectedFilters: Filters = { + app: "", + timeNumber: "0", + timeUnit: "seconds", + fullScan: true, + sqlType: "", + database: "", + regions: "", + nodes: "", + }; + const resultFilters = getFiltersFromQueryString("fullScan=true"); + expect(resultFilters).toEqual(expectedFilters); + }); + + it("testing boolean with full scan = false", (): void => { + const expectedFilters: Filters = { + app: "", + timeNumber: "0", + timeUnit: "seconds", + fullScan: false, + sqlType: "", + database: "", + regions: "", + nodes: "", + }; + const resultFilters = getFiltersFromQueryString("fullScan=false"); + expect(resultFilters).toEqual(expectedFilters); + }); +}); diff --git a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx index c5c172254ccc..1fb81d126631 100644 --- a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx @@ -13,6 +13,8 @@ import Select from "react-select"; import { Button } from "../button"; import { CaretDown } from "@cockroachlabs/icons"; import { Input } from "antd"; +import { History } from "history"; +import { isEqual } from "lodash"; import { dropdownButton, dropdownContentWrapper, @@ -25,6 +27,7 @@ import { checkbox, } from "./filterClasses"; import { MultiSelectCheckbox } from "../multiSelectCheckbox/multiSelectCheckbox"; +import { syncHistory } from "../util"; interface QueryFilter { onSubmitFilters: (filters: Filters) => void; @@ -100,14 +103,112 @@ export const getFiltersFromQueryString = ( const queryStringFilter = searchParams.get(filter); const filterValue = queryStringFilter == null - ? defaultValue - : defaultValue.constructor(searchParams.get(filter)); + ? defaultValue // If this filter doesn't exist on query string, use default value. + : typeof defaultValue == "boolean" + ? searchParams.get(filter) === "true" // If it's a Boolean, convert from String to Boolean; + : defaultValue.constructor(searchParams.get(filter)); // Otherwise, use the constructor for that class. + // Boolean is converted without using its own constructor because the value from the query + // params is a string and Boolean('false') = true, which would be incorrect. return { [filter]: filterValue, ...filters }; }, {}, ); }; +/** + * Get Filters from Query String and if its value is different from the current + * filters value, it calls the onFilterChange function. + * @param history History + * @param filters the current active filters + * @param onFilterChange function to be called if the values from the search + * params are different from the current ones. This function can update + * the value stored on localStorage for example + * @returns Filters the active filters + */ +export const handleFiltersFromQueryString = ( + history: History, + filters: Filters, + onFilterChange: (value: Filters) => void, +): Filters => { + const filtersQueryString = getFiltersFromQueryString(history.location.search); + const searchParams = new URLSearchParams(history.location.search); + let hasFilter = false; + + for (const key of Object.keys(defaultFilters)) { + if (searchParams.get(key)) { + hasFilter = true; + break; + } + } + + if (onFilterChange && hasFilter && !isEqual(filtersQueryString, filters)) { + // If we have filters on query string and they're different + // from the current filter state on props (localStorage), + // we want to update the value on localStorage. + onFilterChange(filtersQueryString); + } else if (!isEqual(filters, defaultFilters)) { + // If the filters on props (localStorage) are different + // from the default values, we want to update the History, + // so the url can be easily shared with the filters selected. + 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, + }, + history, + ); + } + // If we have a new filter selection on query params, they + // take precedent on what is stored on localStorage. + return hasFilter ? filtersQueryString : filters; +}; + +/** + * Update the query params to the current values of the Filter. + * When we change tabs inside the SQL Activity page for example, + * the constructor is called only on the first time. + * The component update event is called frequently and can be used to + * update the query params by using this function that only updates + * the query params if the values did change and we're on the correct tab. + * @param tab which the query params should update + * @param filters the current filters + * @param history + */ +export const updateFiltersQueryParamsOnTab = ( + tab: string, + filters: Filters, + history: History, +): void => { + const filtersQueryString = getFiltersFromQueryString(history.location.search); + const searchParams = new URLSearchParams(history.location.search); + const currentTab = searchParams.get("tab") || ""; + if ( + currentTab === tab && + !isEqual(filters, defaultFilters) && + !isEqual(filters, filtersQueryString) + ) { + 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, + }, + history, + ); + } +}; + /** * The State of the filter that is consider inactive. * It's different from defaultFilters because we don't want to take diff --git a/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx b/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx index 9ec4330a4947..cf6635c65be9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sortedtable/sortedtable.tsx @@ -11,6 +11,7 @@ import React from "react"; import _ from "lodash"; import * as Long from "long"; +import { History } from "history"; import { Moment } from "moment"; import { createSelector } from "reselect"; @@ -319,7 +320,7 @@ export class SortedTable extends React.Component< return sortData ? sortData.slice(start, end) : data.slice(start, end); }; - render() { + render(): React.ReactElement { const { data, loading, @@ -397,7 +398,10 @@ export class SortedTable extends React.Component< * @param maxLength the max length to which it should display value * and hide the remaining. */ -export function longListWithTooltip(value: string, maxLength: number) { +export function longListWithTooltip( + value: string, + maxLength: number, +): React.ReactElement { const summary = value.length > maxLength ? value.slice(0, maxLength) + "..." : value; return ( @@ -411,3 +415,83 @@ export function longListWithTooltip(value: string, maxLength: number) { ); } + +/** + * Get Sort Setting from Query String and if it's different from current + * sortSetting calls the onSortChange function. + * @param page the page where the table was added (used for analytics) + * @param queryString searchParams + * @param sortSetting the current sort Setting on the page + * @param onSortingChange function to be called if the values from the search + * params are different from the current ones. This function can update + * the value stored on localStorage for example. + */ +export const handleSortSettingFromQueryString = ( + page: string, + queryString: string, + sortSetting: SortSetting, + onSortingChange: ( + name: string, + columnTitle: string, + ascending: boolean, + ) => void, +): void => { + const searchParams = new URLSearchParams(queryString); + const ascending = (searchParams.get("ascending") || undefined) === "true"; + const columnTitle = searchParams.get("columnTitle") || undefined; + if ( + onSortingChange && + columnTitle && + (sortSetting.columnTitle != columnTitle || + sortSetting.ascending != ascending) + ) { + onSortingChange(page, columnTitle, ascending); + } +}; + +/** + * Update the query params to the current values of the Sort Setting. + * When we change tabs inside the SQL Activity page for example, + * the constructor is called only on the first time. + * The component update event is called frequently and can be used to + * update the query params by using this function that only updates + * the query params if the values did change and we're on the correct tab. + * @param tab which the query params should update + * @param sortSetting the current sort settings + * @param defaultSortSetting the default sort settings + * @param history + */ +export const updateSortSettingQueryParamsOnTab = ( + tab: string, + sortSetting: SortSetting, + defaultSortSetting: SortSetting, + history: History, +): void => { + const searchParams = new URLSearchParams(history.location.search); + const currentTab = searchParams.get("tab") || ""; + const ascending = + (searchParams.get("ascending") || + defaultSortSetting.ascending.toString()) === "true"; + const columnTitle = + searchParams.get("columnTitle") || defaultSortSetting.columnTitle; + if ( + currentTab === tab && + (sortSetting.columnTitle != columnTitle || + sortSetting.ascending != ascending) + ) { + const params = { + ascending: sortSetting.ascending.toString(), + columnTitle: sortSetting.columnTitle, + }; + const nextSearchParams = new URLSearchParams(history.location.search); + Object.entries(params).forEach(([key, value]) => { + if (!value) { + nextSearchParams.delete(key); + } else { + nextSearchParams.set(key, value); + } + }); + history.location.search = nextSearchParams.toString(); + history.replace(history.location); + } +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index b31bda99b768..c200ee9c4ac9 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -10,12 +10,17 @@ import React from "react"; import { RouteComponentProps } from "react-router-dom"; -import { isNil, merge, isEqual } from "lodash"; +import { isNil, merge } from "lodash"; import moment, { Moment } from "moment"; import classNames from "classnames/bind"; import { Loading } from "src/loading"; import { PageConfig, PageConfigItem } from "src/pageConfig"; -import { ColumnDescriptor, SortSetting } from "src/sortedtable"; +import { + ColumnDescriptor, + handleSortSettingFromQueryString, + SortSetting, + updateSortSettingQueryParamsOnTab, +} from "src/sortedtable"; import { Search } from "src/search"; import { Pagination } from "src/pagination"; import { DateRange } from "src/dateRange"; @@ -25,8 +30,9 @@ import { Filters, defaultFilters, calculateActiveFilters, - getFiltersFromQueryString, getTimeValueInSeconds, + handleFiltersFromQueryString, + updateFiltersQueryParamsOnTab, } from "../queryFilter"; import { @@ -157,51 +163,19 @@ export class StatementsPage extends React.Component< const searchQuery = searchParams.get("q") || undefined; // Sort Settings. - const ascending = (searchParams.get("ascending") || undefined) === "true"; - const columnTitle = searchParams.get("columnTitle") || undefined; - if ( - this.props.onSortingChange && - columnTitle && - (sortSetting.columnTitle != columnTitle || - sortSetting.ascending != ascending) - ) { - this.props.onSortingChange("Statements", columnTitle, ascending); - } + handleSortSettingFromQueryString( + "Statements", + history.location.search, + sortSetting, + this.props.onSortingChange, + ); // Filters. - const filtersQueryString = getFiltersFromQueryString( - history.location.search, + const latestFilter = handleFiltersFromQueryString( + history, + filters, + this.props.onFilterChange, ); - const hasFilter = searchParams.get("app") || undefined; - if ( - this.props.onFilterChange && - hasFilter && - !isEqual(filtersQueryString, filters) - ) { - // If we have filters on query string and they're different - // from the current filter state on props (localStorage), - // we want to update the value on localStorage. - this.props.onFilterChange(filtersQueryString); - } else if (!isEqual(filters, defaultFilters)) { - // If the filters on props (localStorage) are different - // from the default values, we want to update the History, - // so the url can be easily shared with the filters selected. - syncHistory( - { - app: filters.app, - timeNumber: filters.timeNumber, - timeUnit: filters.timeUnit, - sqlType: filters.sqlType, - database: filters.database, - regions: filters.regions, - nodes: filters.nodes, - }, - this.props.history, - ); - } - // If we have a new filter selection on query params, they - // take precedent on what is stored on localStorage. - const latestFilter = hasFilter ? filtersQueryString : filters; return { search: searchQuery, @@ -260,47 +234,29 @@ export class StatementsPage extends React.Component< } } - // When we change tabs inside the SQL Activity page, - // the constructor is called only on the first time. - // The component update event is called frequently - // and can be used to update the query params by using - // this function that makes sure it's only updating - // if the values did change and we're on the correct - // tab (Statements). - updateQueryParamsOnTabSwitch(): void { - const { filters } = this.state; - const filtersQueryString = getFiltersFromQueryString( - this.props.history.location.search, + updateQueryParams(): void { + updateFiltersQueryParamsOnTab( + "Statements", + this.state.filters, + this.props.history, ); - const searchParams = new URLSearchParams( - this.props.history.location.search, + + updateSortSettingQueryParamsOnTab( + "Statements", + this.props.sortSetting, + { + ascending: false, + columnTitle: "executionCount", + }, + this.props.history, ); - const tab = searchParams.get("tab") || ""; - if ( - tab === "Statements" && - !isEqual(filters, defaultFilters) && - !isEqual(filters, filtersQueryString) - ) { - syncHistory( - { - app: filters.app, - timeNumber: filters.timeNumber, - timeUnit: filters.timeUnit, - sqlType: filters.sqlType, - database: filters.database, - regions: filters.regions, - nodes: filters.nodes, - }, - this.props.history, - ); - } } componentDidUpdate = ( __: StatementsPageProps, prevState: StatementsPageState, ): void => { - this.updateQueryParamsOnTabSwitch(); + this.updateQueryParams(); if (this.state.search && this.state.search !== prevState.search) { this.props.onSearchComplete(this.filteredStatementsData()); } @@ -347,6 +303,7 @@ export class StatementsPage extends React.Component< app: filters.app, timeNumber: filters.timeNumber, timeUnit: filters.timeUnit, + fullScan: filters.fullScan.toString(), sqlType: filters.sqlType, database: filters.database, regions: filters.regions, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index bd2e070852ea..7c37c0526309 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -122,7 +122,7 @@ export const ConnectedStatementsPage = withRouter( dispatch( localStorageActions.update({ key: "filters/StatementsPage", - value: { filters: value }, + value: value, }), ); }, 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 93c24d3af22e..b93687e56f0a 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 @@ -32,6 +32,7 @@ export type LocalStorageState = { "sortSetting/TransactionsPage": SortSetting; "sortSetting/SessionsPage": SortSetting; "filters/StatementsPage": Filters; + "filters/TransactionsPage": Filters; }; type Payload = { @@ -81,6 +82,9 @@ const initialState: LocalStorageState = { "filters/StatementsPage": JSON.parse(localStorage.getItem("filters/StatementsPage")) || defaultFilters, + "filters/TransactionsPage": + JSON.parse(localStorage.getItem("filters/TransactionsPage")) || + defaultFilters, }; const localStorageSlice = createSlice({ diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts index b921eca4921a..b5489e6ca3c5 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts @@ -14,6 +14,7 @@ import Long from "long"; import moment from "moment"; import * as protos from "@cockroachlabs/crdb-protobuf-client"; import { SortSetting } from "../sortedtable"; +import { Filters } from "../queryFilter"; const history = createMemoryHistory({ initialEntries: ["/transactions"] }); @@ -40,6 +41,8 @@ export const nodeRegions: { [nodeId: string]: string } = { "4": "gcp-europe-west1", }; +export const columns: string[] = ["all"]; + export const dateRange: [moment.Moment, moment.Moment] = [ moment.utc("2021.08.08"), moment.utc("2021.08.12"), @@ -53,6 +56,14 @@ export const sortSetting: SortSetting = { columnTitle: "executionCount", }; +export const filters: Filters = { + app: "", + timeNumber: "0", + timeUnit: "seconds", + regions: "", + nodes: "", +}; + export const data: cockroach.server.serverpb.IStatementsResponse = { statements: [ { diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts index 2846fb33fe33..355eed74c84c 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts @@ -45,3 +45,8 @@ export const selectSortSetting = createSelector( localStorageSelector, localStorage => localStorage["sortSetting/TransactionsPage"], ); + +export const selectFilters = createSelector( + localStorageSelector, + localStorage => localStorage["filters/TransactionsPage"], +); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx index a879289e3829..9e8474488e68 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx @@ -15,9 +15,11 @@ import { cloneDeep, noop, extend } from "lodash"; import { data, nodeRegions, + columns, routeProps, dateRange, sortSetting, + filters, } from "./transactions.fixture"; import { TransactionsPage } from "."; @@ -37,10 +39,13 @@ storiesOf("Transactions Page", module) data={data} dateRange={dateRange} nodeRegions={nodeRegions} + columns={columns} refreshData={noop} resetSQLStats={noop} sortSetting={sortSetting} onSortingChange={noop} + filters={filters} + onFilterChange={noop} /> )) .add("without data", () => { @@ -50,10 +55,13 @@ storiesOf("Transactions Page", module) data={getEmptyData()} dateRange={dateRange} nodeRegions={nodeRegions} + columns={columns} refreshData={noop} resetSQLStats={noop} sortSetting={sortSetting} onSortingChange={noop} + filters={filters} + onFilterChange={noop} /> ); }) @@ -70,11 +78,14 @@ storiesOf("Transactions Page", module) data={getEmptyData()} dateRange={dateRange} nodeRegions={nodeRegions} + columns={columns} refreshData={noop} history={history} resetSQLStats={noop} sortSetting={sortSetting} onSortingChange={noop} + filters={filters} + onFilterChange={noop} /> ); }) @@ -85,10 +96,13 @@ storiesOf("Transactions Page", module) data={undefined} dateRange={dateRange} nodeRegions={nodeRegions} + columns={columns} refreshData={noop} resetSQLStats={noop} sortSetting={sortSetting} onSortingChange={noop} + filters={filters} + onFilterChange={noop} /> ); }) @@ -99,6 +113,7 @@ storiesOf("Transactions Page", module) data={undefined} dateRange={dateRange} nodeRegions={nodeRegions} + columns={columns} error={ new RequestError( "Forbidden", @@ -110,6 +125,8 @@ storiesOf("Transactions Page", module) resetSQLStats={noop} sortSetting={sortSetting} onSortingChange={noop} + filters={filters} + onFilterChange={noop} /> ); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx index 563a219026d2..20663ff79810 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -23,8 +23,10 @@ import { DateRange } from "src/dateRange"; import { TransactionDetails } from "../transactionDetails"; import { ColumnDescriptor, + handleSortSettingFromQueryString, ISortedTablePagination, SortSetting, + updateSortSettingQueryParamsOnTab, } from "../sortedtable"; import { Pagination } from "../pagination"; import { TableStatistics } from "../tableStatistics"; @@ -39,7 +41,8 @@ import { getStatementsByFingerprintIdAndTime, } from "./utils"; import Long from "long"; -import { getSearchParams, unique, syncHistory } from "src/util"; +import { merge } from "lodash"; +import { unique, syncHistory } from "src/util"; import { EmptyTransactionsPlaceholder } from "./emptyTransactionsPlaceholder"; import { Loading } from "../loading"; import { PageConfig, PageConfigItem } from "../pageConfig"; @@ -48,7 +51,8 @@ import { Filter, Filters, defaultFilters, - getFiltersFromQueryString, + handleFiltersFromQueryString, + updateFiltersQueryParamsOnTab, } from "../queryFilter"; import { UIConfigState } from "../store"; import { StatementsRequest } from "src/api/statementsApi"; @@ -87,6 +91,7 @@ export interface TransactionsPageStateProps { isTenant?: UIConfigState["isTenant"]; columns: string[]; sortSetting: SortSetting; + filters: Filters; } export interface TransactionsPageDispatchProps { @@ -94,6 +99,7 @@ export interface TransactionsPageDispatchProps { resetSQLStats: () => void; onDateRangeChange?: (start: Moment, end: Moment) => void; onColumnsChange?: (selectedColumns: string[]) => void; + onFilterChange?: (value: Filters) => void; onSortingChange?: ( name: string, columnTitle: string, @@ -120,42 +126,47 @@ export class TransactionsPage extends React.Component< > { constructor(props: TransactionsPageProps) { super(props); - const filters = getFiltersFromQueryString( - this.props.history.location.search, - ); - - const trxSearchParams = getSearchParams(this.props.history.location.search); this.state = { pagination: { pageSize: this.props.pageSize || 20, current: 1, }, - search: trxSearchParams("q", "").toString(), - filters: filters, + search: "", aggregatedTs: null, statementFingerprintIds: null, transactionStats: null, transactionFingerprintId: null, }; - - const ascending = trxSearchParams("ascending", false).toString() === "true"; - const columnTitle = trxSearchParams("columnTitle", undefined); - if ( - this.props.onSortingChange && - columnTitle && - (this.props.sortSetting.columnTitle != columnTitle || - this.props.sortSetting.ascending != ascending) - ) { - this.props.onSortingChange( - "Transactions", - columnTitle.toString(), - ascending, - ); - } + const stateFromHistory = this.getStateFromHistory(); + this.state = merge(this.state, stateFromHistory); } - static defaultProps: Partial = { - isTenant: false, + getStateFromHistory = (): Partial => { + const { history, sortSetting, filters } = this.props; + const searchParams = new URLSearchParams(history.location.search); + + // Search query. + const searchQuery = searchParams.get("q") || undefined; + + // Sort Settings. + handleSortSettingFromQueryString( + "Transactions", + history.location.search, + sortSetting, + this.props.onSortingChange, + ); + + // Filters. + const latestFilter = handleFiltersFromQueryString( + history, + filters, + this.props.onFilterChange, + ); + + return { + search: searchQuery, + filters: latestFilter, + }; }; refreshData = (): void => { @@ -166,7 +177,27 @@ export class TransactionsPage extends React.Component< componentDidMount(): void { this.refreshData(); } + + updateQueryParams(): void { + updateFiltersQueryParamsOnTab( + "Transactions", + this.state.filters, + this.props.history, + ); + + updateSortSettingQueryParamsOnTab( + "Transactions", + this.props.sortSetting, + { + ascending: false, + columnTitle: "executionCount", + }, + this.props.history, + ); + } + componentDidUpdate(): void { + this.updateQueryParams(); this.refreshData(); } @@ -221,11 +252,12 @@ export class TransactionsPage extends React.Component< }; onSubmitFilters = (filters: Filters): void => { + if (this.props.onFilterChange) { + this.props.onFilterChange(filters); + } + this.setState({ - filters: { - ...this.state.filters, - ...filters, - }, + filters: filters, }); this.resetPagination(); syncHistory( @@ -241,6 +273,10 @@ export class TransactionsPage extends React.Component< }; onClearFilters = (): void => { + if (this.props.onFilterChange) { + this.props.onFilterChange(defaultFilters); + } + this.setState({ filters: { ...defaultFilters, diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx index 1f4c70bce0ae..bfea562de986 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx @@ -30,10 +30,13 @@ import { } from "./transactionsPage.selectors"; import { selectIsTenant } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { selectDateRange } from "src/statementsPage/statementsPage.selectors"; +import { + selectDateRange, + selectFilters, +} from "src/statementsPage/statementsPage.selectors"; import { StatementsRequest } from "src/api/statementsApi"; import { actions as localStorageActions } from "../store/localStorage"; -import { actions as analyticsActions } from "../store/analytics"; +import { Filters } from "../queryFilter"; export const TransactionsPageConnected = withRouter( connect< @@ -49,6 +52,7 @@ export const TransactionsPageConnected = withRouter( dateRange: selectDateRange(state), columns: selectTxnColumns(state), sortSetting: selectSortSetting(state), + filters: selectFilters(state), }), (dispatch: Dispatch) => ({ refreshData: (req?: StatementsRequest) => @@ -86,6 +90,14 @@ export const TransactionsPageConnected = withRouter( }), ); }, + onFilterChange: (value: Filters) => { + dispatch( + localStorageActions.update({ + key: "filters/TransactionsPage", + value: value, + }), + ); + }, }), )(TransactionsPage), ); diff --git a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx index e82bddd2a8f9..a0447c6ed5f3 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -21,7 +21,11 @@ import { StatementsResponseMessage } from "src/util/api"; import { TimestampToMoment } from "src/util/convert"; import { PrintTime } from "src/views/reports/containers/range/print"; -import { TransactionsPage } from "@cockroachlabs/cluster-ui"; +import { + TransactionsPage, + Filters, + defaultFilters, +} from "@cockroachlabs/cluster-ui"; import { nodeRegionsByIDSelector } from "src/redux/nodes"; import { statementsDateRangeLocalSetting } from "src/redux/statementsDateRange"; import { setCombinedStatementsDateRangeAction } from "src/redux/statements"; @@ -68,6 +72,12 @@ export const sortSettingLocalSetting = new LocalSetting( { ascending: false, columnTitle: "executionCount" }, ); +export const filtersLocalSetting = new LocalSetting( + "filters/TransactionsPage", + (state: AdminUIState) => state.localSettings, + defaultFilters, +); + export const transactionColumnsLocalSetting = new LocalSetting( "showColumns/TransactionPage", (state: AdminUIState) => state.localSettings, @@ -85,6 +95,7 @@ const TransactionsPageConnected = withRouter( nodeRegions: nodeRegionsByIDSelector(state), columns: transactionColumnsLocalSetting.selectorToArray(state), sortSetting: sortSettingLocalSetting.selector(state), + filters: filtersLocalSetting.selector(state), }), { refreshData: refreshStatements, @@ -107,6 +118,7 @@ const TransactionsPageConnected = withRouter( ascending: ascending, columnTitle: columnName, }), + onFilterChange: (filters: Filters) => filtersLocalSetting.set(filters), }, )(TransactionsPage), );