diff --git a/packages/cluster-ui/package.json b/packages/cluster-ui/package.json index b413bda56..67a4a3110 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.19-alpha.2", "@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
+