diff --git a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts index 38afe8694e1c..f6cfa71c4ac7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts @@ -34,6 +34,8 @@ export type StatementDetailsResponseWithKey = { }; export type SqlStatsResponse = cockroach.server.serverpb.StatementsResponse; +export const SqlStatsSortOptions = cockroach.server.serverpb.StatsSortOptions; +export type SqlStatsSortType = cockroach.server.serverpb.StatsSortOptions; const FetchStatsMode = cockroach.server.serverpb.CombinedStatementsStatsRequest.StatsType; @@ -43,9 +45,6 @@ export type ErrorWithKey = { key: string; }; -export const SqlStatsSortOptions = cockroach.server.serverpb.StatsSortOptions; -export type SqlStatsSortType = cockroach.server.serverpb.StatsSortOptions; - export const DEFAULT_STATS_REQ_OPTIONS = { limit: 100, sort: SqlStatsSortOptions.SERVICE_LAT, diff --git a/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.module.scss b/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.module.scss index 38a7574c7ea1..f249709fd869 100644 --- a/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.module.scss @@ -6,7 +6,7 @@ } &__btn { - height: $line-height--large; + height: $line-height--larger; width: 67px; font-size: $font-size--small; } @@ -36,9 +36,10 @@ } } -.float { +.btn-area { float: left; margin-right: 7px; + font-size: $font-size--medium; } .label { diff --git a/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.tsx b/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.tsx index a731fdc76de5..f7a631778c90 100644 --- a/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/columnsSelector/columnsSelector.tsx @@ -36,6 +36,7 @@ export interface ColumnsSelectorProps { // options provides the list of available columns and their initial selection state options: SelectOption[]; onSubmitColumns: (selectedColumns: string[]) => void; + size?: "default" | "small"; } export interface ColumnsSelectorState { @@ -222,6 +223,7 @@ export default class ColumnsSelector extends React.Component< render(): React.ReactElement { const { hide } = this.state; + const { size = "default" } = this.props; const dropdownArea = hide ? hidden : dropdown; const options = this.getOptions(); const columnsSelected = options.filter(o => o.isSelected); @@ -230,9 +232,9 @@ export default class ColumnsSelector extends React.Component<
- diff --git a/pkg/ui/workspaces/cluster-ui/src/common/index.tsx b/pkg/ui/workspaces/cluster-ui/src/common/index.tsx index 96ff5b297bb8..b4c1f2eddd0d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/common/index.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/common/index.tsx @@ -12,3 +12,31 @@ import classNames from "classnames/bind"; import styles from "./styles.module.scss"; export const commonStyles = classNames.bind(styles); + +export const selectCustomStyles = { + container: (provided: any) => ({ + ...provided, + border: "none", + }), + option: (provided: any, state: any) => ({ + ...provided, + backgroundColor: state.isSelected ? "#DEEBFF" : provided.backgroundColor, + color: "#394455", + }), + control: (provided: any) => ({ + ...provided, + width: "100%", + borderColor: "#C0C6D9", + }), + dropdownIndicator: (provided: any) => ({ + ...provided, + color: "#C0C6D9", + }), + singleValue: (provided: any) => ({ + ...provided, + color: "#475872", + }), + indicatorSeparator: (provided: any) => ({ + ...provided, + }), +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/common/styles.module.scss b/pkg/ui/workspaces/cluster-ui/src/common/styles.module.scss index 87455380c909..db5965ad5de1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/common/styles.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/common/styles.module.scss @@ -45,7 +45,16 @@ h3.base-heading { font-family: $font-family--semi-bold; font-style: normal; font-stretch: normal; - font-size: 20px; + font-size: $font-size--large; + padding-bottom: 12px; +} + +h5.base-heading { + color: $colors--neutral-7; + font-family: $font-family--semi-bold; + font-style: normal; + font-stretch: normal; + font-size: $font-size--tall; padding-bottom: 12px; } diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx index e886b361408f..872ecfbde489 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/insights/workloadInsights/statementInsights/statementInsightsView.tsx @@ -292,6 +292,7 @@ export const StatementInsightsView: React.FC = ({ {

diff --git a/pkg/ui/workspaces/cluster-ui/src/pageConfig/pageConfig.tsx b/pkg/ui/workspaces/cluster-ui/src/pageConfig/pageConfig.tsx index 031c72d14a95..5d039aa6dbd6 100644 --- a/pkg/ui/workspaces/cluster-ui/src/pageConfig/pageConfig.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/pageConfig/pageConfig.tsx @@ -17,6 +17,7 @@ export interface PageConfigProps { layout?: "list" | "spread"; children?: React.ReactNode; whiteBkg?: boolean; + className?: string; } const cx = classnames.bind(styles); @@ -31,9 +32,9 @@ export function PageConfig(props: PageConfigProps): React.ReactElement { return (
    {props.children}
diff --git a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.module.scss b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.module.scss index 783f972f1d5d..4214ac610584 100644 --- a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.module.scss @@ -6,13 +6,13 @@ $dropdown-hover-color: darken($colors--background, 2.5%); font-family: $font-family--semi-bold; padding: 8px 0 8px 17px; vertical-align: middle; - border: 1px solid $colors--neutral-5; + border: 1px solid $colors--neutral-4; border-radius: 3px; white-space: nowrap; color: $colors--neutral-7; cursor: pointer; position: relative; - background: $colors--neutral-1; + background: white; display: flex; align-items: center; height: 40px; diff --git a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx index 985e5fc3b62f..9dbea2a143f4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/queryFilter/filter.tsx @@ -29,6 +29,7 @@ import { } from "./filterClasses"; import { MultiSelectCheckbox } from "../multiSelectCheckbox/multiSelectCheckbox"; import { syncHistory } from "../util"; +import { selectCustomStyles } from "../common"; interface QueryFilter { onSubmitFilters: (filters: Filters) => void; @@ -417,33 +418,7 @@ export class Filter extends React.Component { showWorkloadInsightTypes, } = this.props; const dropdownArea = hide ? hidden : dropdown; - const customStyles = { - container: (provided: any) => ({ - ...provided, - border: "none", - }), - option: (provided: any, state: any) => ({ - ...provided, - backgroundColor: state.isSelected - ? "#DEEBFF" - : provided.backgroundColor, - color: "#394455", - }), - control: (provided: any) => ({ - ...provided, - width: "100%", - borderColor: "#C0C6D9", - }), - dropdownIndicator: (provided: any) => ({ - ...provided, - color: "#C0C6D9", - }), - singleValue: (provided: any) => ({ - ...provided, - color: "#475872", - }), - }; - const customStylesSmall = { ...customStyles }; + const customStylesSmall = { ...selectCustomStyles }; customStylesSmall.container = (provided: any) => ({ ...provided, width: "141px", diff --git a/pkg/ui/workspaces/cluster-ui/src/recentExecutions/recentStatementsSection.tsx b/pkg/ui/workspaces/cluster-ui/src/recentExecutions/recentStatementsSection.tsx index d5d6938cbfc8..f69988949eff 100644 --- a/pkg/ui/workspaces/cluster-ui/src/recentExecutions/recentStatementsSection.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/recentExecutions/recentStatementsSection.tsx @@ -83,6 +83,7 @@ export const RecentStatementsSection: React.FC< void; + onChangeTop: (top: number) => void; + onChangeBy: (by: SqlStatsSortType) => void; + onApply: () => void; +} + +export function SearchCriteria(props: SearchCriteriaProps): React.ReactElement { + const { + topValue, + byValue, + currentScale, + onChangeTop, + onChangeBy, + onChangeTimeScale, + sortOptions, + } = props; + const customStyles = { ...selectCustomStyles }; + customStyles.indicatorSeparator = (provided: any) => ({ + ...provided, + display: "none", + }); + + const customStylesTop = { ...customStyles }; + customStylesTop.container = (provided: any) => ({ + ...provided, + width: "80px", + border: "none", + }); + + const customStylesBy = { ...customStyles }; + customStylesBy.container = (provided: any) => ({ + ...provided, + width: "170px", + border: "none", + }); + + return ( +
+
Search Criteria
+ + + + + + + + + + + +
+ ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx index daa11a6245db..600ecf18335e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/sessions/sessionsPage.tsx @@ -403,6 +403,7 @@ export class SessionsPage extends React.Component< { return ( <> - reset SQL stats + Reset SQL Stats ({ ...prevState, reqSortSetting: newSort })); }; - renderStatements = (regions: string[]): React.ReactElement => { + renderStatements = (): React.ReactElement => { const { pagination, filters, activeFilters } = this.state; const { onSelectDiagnosticsReportDropdownOption, @@ -490,6 +489,9 @@ export class StatementsPage extends React.Component< hasViewActivityRedactedRole, sortSetting, search, + apps, + databases, + hasAdminRole, } = this.props; const statements = this.props.statements ?? []; const data = filteredStatementsData( @@ -499,8 +501,16 @@ export class StatementsPage extends React.Component< nodeRegions, isTenant, ); - const totalCount = data.length; const isEmptySearchResults = statements?.length > 0 && search?.length > 0; + const nodes = Object.keys(nodeRegions) + .map(n => Number(n)) + .sort(); + const regions = unique( + isTenant + ? flatMap(statements, statement => statement.stats.regions) + : nodes.map(node => nodeRegions[node.toString()]), + ).sort(); + // If the cluster is a tenant cluster we don't show info // about nodes/regions. populateRegionNodeForStatements(statements, nodeRegions); @@ -543,25 +553,78 @@ export class StatementsPage extends React.Component< ); const period = timeScaleToString(this.props.timeScale); + const clearFilter = activeFilters ? ( + + + + ) : ( + <> + ); + + const sortSettingLabel = getSortLabel(this.props.reqSortSetting); return ( -
+ <> +
+ {`Results - Top ${this.props.limit} Statement Fingerprints by ${sortSettingLabel}`} +
+
+ + + + + + "n" + n)} + activeFilters={activeFilters} + filters={filters} + showDB={true} + showSqlType={true} + showScan={true} + showRegions={regions.length > 1} + showNodes={!isTenant && nodes.length > 1} + /> + + + + + {clearFilter} + + + +

+ Showing aggregated stats from{" "} + {period} +

+
+ {hasAdminRole && ( + + + + )} +
+
-
- - -
-
+ ); }; @@ -592,27 +655,7 @@ export class StatementsPage extends React.Component< refreshStatementDiagnosticsRequests, onActivateStatementDiagnostics, onDiagnosticsModalOpen, - apps, - databases, - statements, - search, - isTenant, - nodeRegions, - hasAdminRole, } = this.props; - - const nodes = Object.keys(nodeRegions) - .map(n => Number(n)) - .sort(); - - const regions = unique( - isTenant - ? flatMap(statements, statement => statement.stats.regions) - : nodes.map(node => nodeRegions[node.toString()]), - ).sort(); - - const { filters, activeFilters } = this.state; - const longLoadingMessage = ( - - - - - - "n" + n)} - activeFilters={activeFilters} - filters={filters} - showDB={true} - showSqlType={true} - showScan={true} - showRegions={regions.length > 1} - showNodes={!isTenant && nodes.length > 1} - /> - - - - Limit: {this.state.limit ?? "N/A"} - - - - - Sort By: {getSortLabel(this.state.reqSortSetting)} - - - - - - - - - {hasAdminRole && ( - - - - )} - +
this.renderStatements(regions)} + render={() => this.renderStatements()} renderError={() => LoadingError({ statsType: "statements", diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index de992ed57230..2373eed7ab79 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -53,7 +53,7 @@ import { selectHasAdminRole, } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { SqlStatsSortType, StatementsRequest } from "src/api/statementsApi"; +import { StatementsRequest } from "src/api/statementsApi"; import { TimeScale } from "../timeScaleDropdown"; import { StatementsPageRoot, @@ -70,6 +70,7 @@ import { import { InsertStmtDiagnosticRequest, StatementDiagnosticsReport, + SqlStatsSortType, } from "../api"; type StateProps = { diff --git a/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/rangeSelector.module.scss b/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/rangeSelector.module.scss index 68995838ab96..bdd17bc20200 100644 --- a/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/rangeSelector.module.scss +++ b/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/rangeSelector.module.scss @@ -31,7 +31,7 @@ width: 100%; .trigger-button { - width: 423px; + min-width: 285px; height: fit-content; } } diff --git a/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/timeScaleDropdown.tsx b/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/timeScaleDropdown.tsx index 6ae0a4031a24..46be340fd099 100644 --- a/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/timeScaleDropdown.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/timeScaleDropdown/timeScaleDropdown.tsx @@ -40,6 +40,7 @@ export interface TimeScaleDropdownProps { timeWindow: TimeWindow, ) => TimeScale; hasCustomOption?: boolean; + className?: string; } export const getTimeLabel = ( @@ -129,6 +130,7 @@ export const TimeScaleDropdown: React.FC = ({ setTimeScale, adjustTimeScaleOnChange, hasCustomOption = true, + className, }): React.ReactElement => { const end = currentScale.fixedWindowEnd ? moment.utc(currentScale.fixedWindowEnd) @@ -253,7 +255,7 @@ export const TimeScaleDropdown: React.FC = ({ }; return ( -
+
Number(n)) - .sort(); - - const regions = unique( - isTenant - ? flatMap(statements, statement => statement.stats.regions) - : nodes.map(node => nodeRegions[node.toString()]), - ).sort(); // We apply the search filters and app name filters prior to aggregating across Node IDs // in order to match what's done on the Statements Page. @@ -443,6 +431,154 @@ export class TransactionsPage extends React.Component< internal_app_name_prefix, ); + const transactionsToDisplay: TransactionInfo[] = aggregateAcrossNodeIDs( + filteredTransactions, + statements, + ).map(t => ({ + stats_data: t.stats_data, + node_id: t.node_id, + regions: generateRegion(t, statements), + regionNodes: generateRegionNode(t, statements, nodeRegions), + })); + const { current, pageSize } = pagination; + const hasData = data?.transactions?.length > 0; + const isUsedFilter = search?.length > 0; + + const nodes = Object.keys(nodeRegions) + .map(n => Number(n)) + .sort(); + + const regions = unique( + isTenant + ? flatMap(statements, statement => statement.stats.regions) + : nodes.map(node => nodeRegions[node.toString()]), + ).sort(); + + // Creates a list of all possible columns, + // hiding nodeRegions if is not multi-region and + // hiding columns that won't be displayed for tenants. + const columns = makeTransactionsColumns( + transactionsToDisplay, + statements, + isTenant, + search, + ) + .filter(c => !(c.name === "regions" && regions.length < 2)) + .filter(c => !(c.name === "regionNodes" && regions.length < 2)) + .filter(c => !(isTenant && c.hideIfTenant)); + + // Iterate over all available columns and create list of SelectOptions with initial selection + // values based on stored user selections in local storage and default column configs. + // Columns that are set to alwaysShow are filtered from the list. + const tableColumns = columns + .filter(c => !c.alwaysShow) + .map( + (c): SelectOption => ({ + label: getLabel(c.name as StatisticTableColumnKeys, "transaction"), + value: c.name, + isSelected: isSelectedColumn(userSelectedColumnsToShow, c), + }), + ); + + // List of all columns that will be displayed based on the column selection. + const displayColumns = columns.filter(c => + isSelectedColumn(userSelectedColumnsToShow, c), + ); + + const period = timeScaleToString(this.props.timeScale); + const clearFilter = activeFilters ? ( + + + + ) : ( + <> + ); + + const sortSettingLabel = getSortLabel(this.props.reqSortSetting); + return ( + <> +
+ {`Results - Top ${this.props.limit} Transaction Fingerprints by ${sortSettingLabel}`} +
+
+ + + + + + "n" + n)} + activeFilters={activeFilters} + filters={filters} + showRegions={regions.length > 1} + showNodes={!isTenant && nodes.length > 1} + /> + + + + + {clearFilter} + + + +

+ Showing aggregated stats from{" "} + {period} +

+
+ {hasAdminRole && ( + + + + )} +
+
+
+ + } + /> +
+ + + ); + } + + render(): React.ReactElement { const longLoadingMessage = ( - - - - - - "n" + n)} - activeFilters={activeFilters} - filters={filters} - showRegions={regions.length > 1} - showNodes={!isTenant && nodes.length > 1} - /> - - - - Limit: {this.state.limit ?? "N/A"} - - - - - Sort By: {getSortLabel(this.state.reqSortSetting)} - - - - - - - - - {hasAdminRole && ( - - - - )} - +
{ - const { pagination } = this.state; - const transactionsToDisplay: TransactionInfo[] = - aggregateAcrossNodeIDs(filteredTransactions, statements).map( - t => ({ - stats_data: t.stats_data, - node_id: t.node_id, - regions: generateRegion(t, statements), - regionNodes: generateRegionNode(t, statements, nodeRegions), - }), - ); - const { current, pageSize } = pagination; - const hasData = data?.transactions?.length > 0; - const isUsedFilter = search?.length > 0; - - // Creates a list of all possible columns, - // hiding nodeRegions if is not multi-region and - // hiding columns that won't be displayed for tenants. - const columns = makeTransactionsColumns( - transactionsToDisplay, - statements, - isTenant, - search, - ) - .filter(c => !(c.name === "regions" && regions.length < 2)) - .filter(c => !(c.name === "regionNodes" && regions.length < 2)) - .filter(c => !(isTenant && c.hideIfTenant)); - - // Iterate over all available columns and create list of SelectOptions with initial selection - // values based on stored user selections in local storage and default column configs. - // Columns that are set to alwaysShow are filtered from the list. - const tableColumns = columns - .filter(c => !c.alwaysShow) - .map( - (c): SelectOption => ({ - label: getLabel( - c.name as StatisticTableColumnKeys, - "transaction", - ), - value: c.name, - isSelected: isSelectedColumn(userSelectedColumnsToShow, c), - }), - ); - - // List of all columns that will be displayed based on the column selection. - const displayColumns = columns.filter(c => - isSelectedColumn(userSelectedColumnsToShow, c), - ); - - const period = timeScaleToString(this.props.timeScale); - - return ( - <> -
- - - - } - /> -
- - - ); - }} + render={() => this.renderTransactions()} renderError={() => LoadingError({ statsType: "transactions", diff --git a/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts b/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts index 326892ba6c9c..9f9a3d002230 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/sqlActivityConstants.ts @@ -13,10 +13,10 @@ import { TimeScale } from "../timeScaleDropdown"; import { SqlStatsSortOptions, SqlStatsSortType } from "../api/statementsApi"; export const limitOptions = [ - { value: 100, name: "100" }, - { value: 200, name: "200" }, - { value: 500, name: "500" }, - { value: 1000, name: "1000" }, + { value: 100, label: "100" }, + { value: 200, label: "200" }, + { value: 500, label: "500" }, + { value: 1000, label: "1000" }, ]; export function getSortLabel(sort: SqlStatsSortType): string { @@ -40,8 +40,8 @@ export function getSortLabel(sort: SqlStatsSortType): string { export const stmtRequestSortOptions = Object.values(SqlStatsSortOptions).map( sortVal => ({ - value: sortVal, - name: getSortLabel(sortVal as SqlStatsSortType), + value: sortVal as SqlStatsSortType, + label: getSortLabel(sortVal as SqlStatsSortType), }), );