From 54511c86b22c5470600474bd8da5851e53a6f0ca Mon Sep 17 00:00:00 2001 From: Marylia Gutierrez Date: Wed, 14 Apr 2021 18:38:38 -0400 Subject: [PATCH] cluster-ui: show database info and ability to choose statement columns Information about database is now displayed on Statements page and Statements Details page. By default the column database is not displayed, but with the new column selector option, it can be selected. New filter allows to select statements based on database name. Closes: #33316 Release note (ui change): Display database name information on Statements page (hidden by default) and Statements Details page. New filter option for statements based on databases name. New option to select which columns to display on statements table. Release note (bug fix): Transaction page showing correct value for implicit txn. --- packages/cluster-ui/package.json | 2 +- .../columnsSelector.module.scss | 57 ++++ .../src/columnsSelector/columnsSelector.tsx | 253 ++++++++++++++++++ .../multiSelectCheckbox.tsx | 6 - .../src/queryFilter/filter.module.scss | 1 + .../cluster-ui/src/queryFilter/filter.tsx | 62 +++-- .../src/sortedtable/sortedtable.tsx | 4 + .../statementDetails.fixture.ts | 12 +- .../statementDetails.selectors.ts | 39 +-- .../src/statementDetails/statementDetails.tsx | 6 + .../statementsPage/statementsPage.fixture.ts | 45 ++++ .../statementsPage/statementsPage.module.scss | 1 + .../statementsPage.selectors.ts | 53 +++- .../src/statementsPage/statementsPage.tsx | 94 +++++-- .../statementsPageConnected.tsx | 16 ++ .../src/statementsTable/statementsTable.tsx | 41 ++- .../statementsTableContent.tsx | 51 +++- .../localStorage/localStorage.reducer.ts | 8 +- .../transactionDetails/transactionDetails.tsx | 1 - .../cluster-ui/src/transactionsPage/utils.ts | 3 +- .../cluster-ui/src/util/appStats/appStats.ts | 9 + packages/cluster-ui/src/util/constants.ts | 1 + yarn.lock | 8 +- 23 files changed, 674 insertions(+), 99 deletions(-) create mode 100644 packages/cluster-ui/src/columnsSelector/columnsSelector.module.scss create mode 100644 packages/cluster-ui/src/columnsSelector/columnsSelector.tsx diff --git a/packages/cluster-ui/package.json b/packages/cluster-ui/package.json index 65f833c76..0b4c9feb4 100644 --- a/packages/cluster-ui/package.json +++ b/packages/cluster-ui/package.json @@ -27,7 +27,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.13", - "@cockroachlabs/crdb-protobuf-client": "^0.0.10-beta.0", + "@cockroachlabs/crdb-protobuf-client": "^0.0.11", "@cockroachlabs/icons": "0.3.0", "@cockroachlabs/ui-components": "0.2.18", "@popperjs/core": "^2.4.0", diff --git a/packages/cluster-ui/src/columnsSelector/columnsSelector.module.scss b/packages/cluster-ui/src/columnsSelector/columnsSelector.module.scss new file mode 100644 index 000000000..9d624cd84 --- /dev/null +++ b/packages/cluster-ui/src/columnsSelector/columnsSelector.module.scss @@ -0,0 +1,57 @@ +@import "../core/index.module"; + +.apply-btn { + &__wrapper { + text-align: end; + } + + &__btn { + height: $line-height--large; + width: 67px; + font-size: $font-size--small; + } +} + +.checkbox { + &__input { + margin-right: 5px; + } + &__label { + cursor: pointer; + } +} + +.dropdown { + position: relative; + &__indicator { + color: $colors--neutral-11; + height: $line-height--medium; + width: 32px; + } +} + +.float { + float: left; + margin-right: 7px; +} + +.label { + height: 16px; + font-family: $font-family--base; + font-weight: $font-weight--bold; + font-size: $font-size--small; + line-height: $line-height--small; + letter-spacing: 0.3px; + font-style: normal; + margin-bottom: 8px; + margin-left: 10px; +} + +.menu { + background-color: white; + border-radius: 4px; + box-shadow: 0px 0px 4px rgba(154, 161, 171, 33%); + margin-top: 8px; + position: absolute; + z-index: 2; +} \ No newline at end of file diff --git a/packages/cluster-ui/src/columnsSelector/columnsSelector.tsx b/packages/cluster-ui/src/columnsSelector/columnsSelector.tsx new file mode 100644 index 000000000..394801e70 --- /dev/null +++ b/packages/cluster-ui/src/columnsSelector/columnsSelector.tsx @@ -0,0 +1,253 @@ +import Select, { components, OptionsType } from "react-select"; +import React from "react"; +import classNames from "classnames/bind"; +import styles from "./columnsSelector.module.scss"; +import { Button } from "../button"; +import { + dropdown, + dropdownContentWrapper, + hidden, +} from "../queryFilter/filterClasses"; +import { List } from "@cockroachlabs/icons"; +import { + DeselectOptionActionMeta, + SelectOptionActionMeta, +} from "react-select/src/types"; + +const cx = classNames.bind(styles); + +export interface SelectOption { + label: string; + value: string; + isSelected: boolean; +} + +export interface ColumnsSelectorProps { + // options provides the list of available columns and their initial selection state + options: SelectOption[]; + onSubmitColumns: (selectedColumns: string[]) => void; +} + +export interface ColumnsSelectorState { + hide: boolean; + selectionState: Map; +} + +/** + * Create all options items using the values from options + * on ColumnsSelector() + * The options must have the parameters label and isSelected + * @param props + * @constructor + */ +const CheckboxOption = (props: any) => { + return ( + + null} + /> + + + ); +}; + +// customStyles uses the default styles provided from the +// react-select component and add changes +const customStyles = { + container: (provided: any) => ({ + ...provided, + border: "none", + height: "fit-content", + }), + control: (provided: any) => ({ + ...provided, + display: "none", + }), + menu: (provided: any) => ({ + ...provided, + position: "relative", + boxShadow: "none", + }), + option: (provided: any, state: any) => ({ + ...provided, + backgroundColor: "white", + color: "#394455", + cursor: "pointer", + padding: "4px 10px", + }), + multiValue: (provided: any) => ({ + ...provided, + backgroundColor: "#E7ECF3", + borderRadius: "3px", + }), +}; + +/** + * Creates the ColumnsSelector from the props + * @param props: + * options (SelectOption[]): a list of options. Each option object must contain a + * label, value and isSelected parameters + * onSubmitColumns (callback function): receives the selected string + * @constructor + */ +export default class ColumnsSelector extends React.Component< + ColumnsSelectorProps, + ColumnsSelectorState +> { + constructor(props: ColumnsSelectorProps) { + super(props); + const allSelected = props.options.every(o => o.isSelected); + // set initial state of selections based on props + const selectionState = new Map( + props.options.map(o => [o.value, allSelected || o.isSelected]), + ); + selectionState.set("all", allSelected); + this.state = { + hide: true, + selectionState, + }; + } + dropdownRef: React.RefObject = React.createRef(); + + componentDidMount() { + window.addEventListener("click", this.outsideClick, false); + } + componentWillUnmount() { + window.removeEventListener("click", this.outsideClick, false); + } + + toggleOpen = () => { + this.setState({ + hide: !this.state.hide, + }); + }; + outsideClick = () => { + this.setState({ hide: true }); + }; + insideClick = (event: any) => { + event.stopPropagation(); + }; + + handleChange = ( + _selectedOptions: OptionsType, + // get actual selection of specific option and action type from "actionMeta" + actionMeta: + | SelectOptionActionMeta + | DeselectOptionActionMeta, + ) => { + const { option, action } = actionMeta; + const selectionState = new Map(this.state.selectionState); + // true - if option was selected, false - otherwise + const isSelectedOption = action === "select-option"; + + // if "all" option was toggled - update all other options + if (option.value === "all") { + selectionState.forEach((_v, k) => + selectionState.set(k, isSelectedOption), + ); + } else { + // check if all other options (except current changed and "all" options) are selected as well to select "all" option + const allOtherOptionsSelected = [...selectionState.entries()] + .filter(([k, _v]) => ![option.value, "all"].includes(k)) // filter all options except currently changed and "all" option + .every(([_k, v]) => v); + + // update "all" option if other options are selected + if (allOtherOptionsSelected) { + selectionState.set("all", isSelectedOption); + } + + selectionState.set(option.value, isSelectedOption); + } + this.setState({ + selectionState, + }); + }; + + handleSubmit = () => { + const { selectionState } = this.state; + const selectedValues = this.props.options + .filter(o => selectionState.get(o.value)) + .filter(o => o.value !== "all") // do not include artificial option "all". It should live only inside this component. + .map(o => o.value); + this.props.onSubmitColumns(selectedValues); + this.setState({ hide: true }); + }; + + isAllSelected = (): boolean => { + return this.state.selectionState.get("all"); + }; + + // getOptions returns list of all options with updated selection states + // and prepends "all" option as an artificial option + getOptions = (): SelectOption[] => { + const { options } = this.props; + const { selectionState } = this.state; + const isAllSelected = this.isAllSelected(); + const allOption: SelectOption = { + label: "All", + value: "all", + isSelected: isAllSelected, + }; + return [allOption, ...options].map(o => { + let isSelected = o.isSelected; // default value; + if (isAllSelected) { + isSelected = true; + } else if (selectionState.has(o.value)) { + isSelected = selectionState.get(o.value); + } + return { + ...o, + // if "all" is selected then every item in the list selected as well + isSelected, + }; + }); + }; + + render() { + const { hide } = this.state; + const dropdownArea = hide ? hidden : dropdown; + const options = this.getOptions(); + const columnsSelected = options.filter(o => o.isSelected); + + return ( +
+ +
+
+
Hide/show columns
+