Skip to content

Commit

Permalink
Merge pull request #294 from maryliag/column-selector
Browse files Browse the repository at this point in the history
cluster-ui: show database info and ability to choose statement columns
  • Loading branch information
maryliag committed May 3, 2021
2 parents ed3848b + 54511c8 commit 698ad2b
Show file tree
Hide file tree
Showing 23 changed files with 673 additions and 98 deletions.
2 changes: 1 addition & 1 deletion packages/cluster-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
253 changes: 253 additions & 0 deletions packages/cluster-ui/src/columnsSelector/columnsSelector.tsx
Original file line number Diff line number Diff line change
@@ -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<string, boolean>;
}

/**
* 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 (
<components.Option {...props}>
<input
type="checkbox"
className={cx("checkbox__input")}
checked={props.isSelected}
onChange={() => null}
/>
<label className={cx("checkbox__label")}>{props.label}</label>
</components.Option>
);
};

// 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<HTMLDivElement> = 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<SelectOption>,
// get actual selection of specific option and action type from "actionMeta"
actionMeta:
| SelectOptionActionMeta<SelectOption>
| DeselectOptionActionMeta<SelectOption>,
) => {
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 (
<div
onClick={this.insideClick}
ref={this.dropdownRef}
className={cx("float")}
>
<Button type="secondary" size="small" onClick={this.toggleOpen}>
<List />
</Button>
<div className={dropdownArea}>
<div className={dropdownContentWrapper}>
<div className={cx("label")}>Hide/show columns</div>
<Select
isMulti
menuIsOpen={true}
options={options}
value={columnsSelected}
onChange={this.handleChange}
hideSelectedOptions={false}
closeMenuOnSelect={false}
components={{ Option: CheckboxOption }}
styles={customStyles}
controlShouldRenderValue={false}
/>
<div className={cx("apply-btn__wrapper")}>
<Button
className={cx("apply-btn__btn")}
textAlign="center"
onClick={this.handleSubmit}
>
Apply
</Button>
</div>
</div>
</div>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ import classNames from "classnames/bind";

const cx = classNames.bind(styles);

const defaultSelectProps = {
searchable: false,
clearable: false,
};

export interface SelectOption {
label: string;
value: string;
Expand Down Expand Up @@ -112,7 +107,6 @@ export const MultiSelectCheckbox = (props: MultiSelectCheckboxProps) => {
closeMenuOnSelect={false}
components={{ Option: CheckboxOption }}
styles={customStyles}
{...defaultSelectProps}
/>
);
};
1 change: 1 addition & 0 deletions packages/cluster-ui/src/queryFilter/filter.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ $dropdown-hover-color: darken($colors--background, 2.5%);
border: 0.5px solid #E5E5E5;
box-shadow: 0px 0px 4px rgba(154, 161, 171, 0.33);
border-radius: 4px;
z-index: 2;

.dropdown-content-wrapper {
padding: 18px 20px 22px 16px;
Expand Down
Loading

0 comments on commit 698ad2b

Please sign in to comment.