From 78443e6e05521396508ea8b840c405250598a3b5 Mon Sep 17 00:00:00 2001 From: Gerardo Torres Date: Thu, 13 Jan 2022 16:47:44 -0500 Subject: [PATCH 1/2] ui: allow stmts page to search across explain plan page Closes #71615. Previously, the search functionality for the stmts page only allowed a search through the statement query. This change adds the statement's Plan as part of that search. Release note (ui change): logical plan text included in searchable text for stmts page. --- .../planView/planView.spec.tsx | 92 +++++++++++++++++++ .../statementDetails/planView/planView.tsx | 18 ++++ .../statementsPage/statementsPage.spec.tsx | 64 +++++++++++++ .../src/statementsPage/statementsPage.tsx | 32 +++++-- 4 files changed, 197 insertions(+), 9 deletions(-) diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx index dad1c408fbd8..a303ee048d2e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.spec.tsx @@ -17,6 +17,8 @@ import { flattenTreeAttributes, flattenAttributes, standardizeKey, + planNodeToString, + planNodeAttrsToString, } from "./planView"; import IAttr = cockroach.sql.ExplainTreePlanNode.IAttr; @@ -260,4 +262,94 @@ describe("planView", () => { assert.equal(standardizeKey("(anti) hello world"), "helloWorld"); }); }); + + describe("planNodeAttrsToString", () => { + it("should convert an array of FlatPlanNodeAttribute[] into a string", () => { + const testNodeAttrs: FlatPlanNodeAttribute[] = [ + { + key: "Into", + values: ["users(id, city, name, address, credit_card)"], + warn: false, + }, + { + key: "Size", + values: ["5 columns, 3 rows"], + warn: false, + }, + ]; + + const expectedString = + "Into users(id, city, name, address, credit_card) Size 5 columns, 3 rows"; + + assert.equal(planNodeAttrsToString(testNodeAttrs), expectedString); + }); + }); + + describe("planNodeToString", () => { + it("should recursively convert a FlatPlanNode into a string.", () => { + const testPlanNode: FlatPlanNode = { + name: "insert fast path", + attrs: [ + { + key: "Into", + values: ["users(id, city, name, address, credit_card)"], + warn: false, + }, + { + key: "Size", + values: ["5 columns, 3 rows"], + warn: false, + }, + ], + children: [], + }; + + const expectedString = + "insert fast path Into users(id, city, name, address, credit_card) Size 5 columns, 3 rows"; + + assert.equal(planNodeToString(testPlanNode), expectedString); + }); + + it("should recursively convert a FlatPlanNode (with children) into a string.", () => { + const testPlanNode: FlatPlanNode = { + name: "render", + attrs: [], + children: [ + { + name: "group (scalar)", + attrs: [], + children: [ + { + name: "filter", + attrs: [ + { + key: "filter", + values: ["variable = _"], + warn: false, + }, + ], + children: [ + { + name: "virtual table", + attrs: [ + { + key: "table", + values: ["cluster_settings@primary"], + warn: false, + }, + ], + children: [], + }, + ], + }, + ], + }, + ], + }; + + const expectedString = + "render group (scalar) filter filter variable = _ virtual table table cluster_settings@primary"; + assert.equal(planNodeToString(testPlanNode), expectedString); + }); + }); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx index 529ac44f286a..c0ee8191a9f7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementDetails/planView/planView.tsx @@ -56,6 +56,24 @@ function warnForAttribute(attr: IAttr): boolean { return false; } +// planNodeAttrsToString converts an array of FlatPlanNodeAttribute[] into a string. +export function planNodeAttrsToString(attrs: FlatPlanNodeAttribute[]): string { + return attrs.map(attr => `${attr.key} ${attr.values.join(" ")}`).join(" "); +} + +// planNodeToString recursively converts a FlatPlanNode into a string. +export function planNodeToString(plan: FlatPlanNode): string { + const str = `${plan.name} ${planNodeAttrsToString(plan.attrs)}`; + + if (plan.children.length > 0) { + return `${str} ${plan.children + .map(child => planNodeToString(child)) + .join(" ")}`; + } + + return str; +} + // flattenAttributes takes a list of attrs (IAttr[]) and collapses // all the values for the same key (FlatPlanNodeAttribute). For example, // if attrs was: diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.spec.tsx index e70548843e98..de5b4c5cd4f2 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.spec.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.spec.tsx @@ -14,11 +14,14 @@ import { ReactWrapper, mount } from "enzyme"; import { MemoryRouter } from "react-router-dom"; import { + filterBySearchQuery, StatementsPage, StatementsPageProps, StatementsPageState, } from "src/statementsPage"; import statementsPagePropsFixture from "./statementsPage.fixture"; +import { AggregateStatistics } from "../statementsTable"; +import { FlatPlanNode } from "../statementDetails"; describe("StatementsPage", () => { describe("Statements table", () => { @@ -43,4 +46,65 @@ describe("StatementsPage", () => { assert.equal(statementsPageInstance.props.sortSetting.ascending, false); }); }); + + describe("filterBySearchQuery", () => { + const testPlanNode: FlatPlanNode = { + name: "render", + attrs: [], + children: [ + { + name: "group (scalar)", + attrs: [], + children: [ + { + name: "filter", + attrs: [ + { + key: "filter", + values: ["variable = _"], + warn: false, + }, + ], + children: [ + { + name: "virtual table", + attrs: [ + { + key: "table", + values: ["cluster_settings@primary"], + warn: false, + }, + ], + children: [], + }, + ], + }, + ], + }, + ], + }; + + const statement: AggregateStatistics = { + aggregatedFingerprintID: "", + aggregatedTs: 0, + aggregationInterval: 0, + database: "", + fullScan: false, + implicitTxn: false, + summary: "", + label: + "SELECT count(*) > _ FROM [SHOW ALL CLUSTER SETTINGS] AS _ (v) WHERE v = '_'", + stats: { + sensitive_info: { + most_recent_plan_description: testPlanNode, + }, + }, + }; + + assert.equal(filterBySearchQuery(statement, "select"), true); + assert.equal(filterBySearchQuery(statement, "virtual table"), true); + assert.equal(filterBySearchQuery(statement, "group (scalar)"), true); + assert.equal(filterBySearchQuery(statement, "node_build_info"), false); + assert.equal(filterBySearchQuery(statement, "crdb_internal"), false); + }); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index 67ee37c7058b..518e280f4388 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -76,6 +76,7 @@ import { } from "../timeScaleDropdown"; import { commonStyles } from "../common"; +import { flattenTreeAttributes, planNodeToString } from "../statementDetails"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -146,6 +147,25 @@ function statementsRequestFromProps( }); } +// filterBySearchQuery returns true if a search query matches the statement. +export function filterBySearchQuery( + statement: AggregateStatistics, + search: string, +): boolean { + const label = statement.label; + const plan = planNodeToString( + flattenTreeAttributes( + statement.stats.sensitive_info && + statement.stats.sensitive_info.most_recent_plan_description, + ), + ); + const matchString = `${label} ${plan}`.toLowerCase(); + return search + .toLowerCase() + .split(" ") + .every(val => matchString.includes(val)); +} + export class StatementsPage extends React.Component< StatementsPageProps, StatementsPageState @@ -408,15 +428,6 @@ export class StatementsPage extends React.Component< databases.length == 0 || databases.includes(statement.database), ) .filter(statement => (filters.fullScan ? statement.fullScan : true)) - .filter(statement => - search - ? search - .split(" ") - .every(val => - statement.label.toLowerCase().includes(val.toLowerCase()), - ) - : true, - ) .filter( statement => statement.stats.service_lat.mean >= timeValue || @@ -456,6 +467,9 @@ export class StatementsPage extends React.Component< statement.stats.nodes.map(node => "n" + node), nodes, )), + ) + .filter(statement => + search ? filterBySearchQuery(statement, search) : true, ); }; From b4908da3cf393787a985c3490defeb90f66855b4 Mon Sep 17 00:00:00 2001 From: Marylia Gutierrez Date: Tue, 25 Jan 2022 08:40:38 -0500 Subject: [PATCH 2/2] ui: switch order of tabs on SQL Activity Previously, the tabs under SQL Activity where Sessions, Transactions and Statements. This commit changes the order to be Statements, Transactions and Sessions. Fixes #74644 Release note (ui change): Change the order of tabs under SQL Activity page to be Statements, Transactions and Sessions. --- .../src/views/sqlActivity/sqlActivityPage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/ui/workspaces/db-console/src/views/sqlActivity/sqlActivityPage.tsx b/pkg/ui/workspaces/db-console/src/views/sqlActivity/sqlActivityPage.tsx index fb8d5374ce02..075077a640c0 100644 --- a/pkg/ui/workspaces/db-console/src/views/sqlActivity/sqlActivityPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/sqlActivity/sqlActivityPage.tsx @@ -23,7 +23,7 @@ import { RouteComponentProps } from "react-router-dom"; const { TabPane } = Tabs; const SQLActivityPage = (props: RouteComponentProps) => { - const defaultTab = util.queryByName(props.location, "tab") || "Sessions"; + const defaultTab = util.queryByName(props.location, "tab") || "Statements"; const [currentTab, setCurrentTab] = useState(defaultTab); const onTabChange = (tabId: string): void => { @@ -33,7 +33,7 @@ const SQLActivityPage = (props: RouteComponentProps) => { }; useEffect(() => { - const queryTab = util.queryByName(props.location, "tab") || "Sessions"; + const queryTab = util.queryByName(props.location, "tab") || "Statements"; if (queryTab !== currentTab) { setCurrentTab(queryTab); } @@ -49,14 +49,14 @@ const SQLActivityPage = (props: RouteComponentProps) => { onChange={onTabChange} activeKey={currentTab} > - - + + - - + +