Skip to content

Commit

Permalink
Grid 1.0 (#804)
Browse files Browse the repository at this point in the history
* Add VisualizationControls and use it in Grid component.

* Parametrize DataProvider with query factory function (#802)

* Parametrize DataProvider with query factory function

* SplitMenuBase

* Split controls for Grid

* Use real limits in grid queries

* Fix key on React.Fragment for efficient array diff
Rename commonSort prop to sort.

* Remove "sort by each dimension" feature on table

* Flatten props on split header components.

* Add 10 000 limit for grid

* Show sort indicator on split columns

* mainSplit util for grid - it selects split where are defined sort and limit

* Split grid into visual component and interaction controller.
  • Loading branch information
adrianmroz-allegro authored Oct 14, 2021
1 parent a3fd142 commit e02ade2
Show file tree
Hide file tree
Showing 32 changed files with 974 additions and 304 deletions.
106 changes: 106 additions & 0 deletions src/client/components/split-menu/split-menu-base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2017-2021 Allegro.pl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as React from "react";
import { Dimension } from "../../../common/models/dimension/dimension";
import { coerceGranularity, isGranularityValid } from "../../../common/models/granularity/granularity";
import { Sort } from "../../../common/models/sort/sort";
import { Split } from "../../../common/models/split/split";
import { Stage } from "../../../common/models/stage/stage";
import { Fn } from "../../../common/utils/general/general";
import { STRINGS } from "../../config/constants";
import { enterKey } from "../../utils/dom/dom";
import { BubbleMenu } from "../bubble-menu/bubble-menu";
import { Button } from "../button/button";

interface SplitAssembly {
dimension: Dimension;
split: Split;
granularity?: string;
limit?: number;
sort?: Sort;
}

export function validateSplit(splitAssembly: SplitAssembly): boolean {
const { granularity, split, dimension: { kind } } = splitAssembly;
if (!isGranularityValid(kind, granularity)) {
return false;
}
const newSplit = createSplit(splitAssembly);
return !split.equals(newSplit);
}

export function createSplit({
split: { type, reference },
limit,
granularity,
sort,
dimension: { kind }
}: SplitAssembly): Split {
const bucket = coerceGranularity(granularity, kind);
return new Split({ type, reference, limit, sort, bucket });
}

interface SplitMenuBaseProps {
openOn: Element;
containerStage: Stage;
onClose: Fn;
onSave: Fn;
dimension: Dimension;
isValid: boolean;
}

export class SplitMenuBase extends React.Component<SplitMenuBaseProps> {

componentDidMount() {
window.addEventListener("keydown", this.globalKeyDownListener);
}

componentWillUnmount() {
window.removeEventListener("keydown", this.globalKeyDownListener);
}

globalKeyDownListener = (e: KeyboardEvent) => enterKey(e) && this.onOkClick();

onCancelClick = () => this.props.onClose();

onOkClick = () => {
const { isValid, onSave, onClose } = this.props;
if (!isValid) return;
onSave();
onClose();
};

render() {
const { containerStage, openOn, dimension, onClose, children, isValid } = this.props;
if (!dimension) return null;

return <BubbleMenu
className="split-menu"
direction="down"
containerStage={containerStage}
stage={Stage.fromSize(250, 240)}
openOn={openOn}
onClose={onClose}
>
{children}
<div className="button-bar">
<Button className="ok" type="primary" disabled={!isValid} onClick={this.onOkClick} title={STRINGS.ok}/>
<Button type="secondary" onClick={this.onCancelClick} title={STRINGS.cancel}/>
</div>
</BubbleMenu>;
}
}
110 changes: 31 additions & 79 deletions src/client/components/split-menu/split-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,20 @@
* limitations under the License.
*/

import { Duration } from "chronoshift";
import * as React from "react";
import { Dimension, isContinuous } from "../../../common/models/dimension/dimension";
import { Essence } from "../../../common/models/essence/essence";
import { granularityToString, isGranularityValid } from "../../../common/models/granularity/granularity";
import { granularityToString } from "../../../common/models/granularity/granularity";
import { DimensionSortOn, SortOn } from "../../../common/models/sort-on/sort-on";
import { Sort } from "../../../common/models/sort/sort";
import { Bucket, Split } from "../../../common/models/split/split";
import { Split } from "../../../common/models/split/split";
import { Stage } from "../../../common/models/stage/stage";
import { Binary } from "../../../common/utils/functional/functional";
import { Fn } from "../../../common/utils/general/general";
import { STRINGS } from "../../config/constants";
import { enterKey } from "../../utils/dom/dom";
import { BubbleMenu } from "../bubble-menu/bubble-menu";
import { Button } from "../button/button";
import { GranularityPicker } from "./granularity-picker";
import { LimitDropdown } from "./limit-dropdown";
import { SortDropdown } from "./sort-dropdown";
import { createSplit, SplitMenuBase, validateSplit } from "./split-menu-base";
import "./split-menu.scss";

export interface SplitMenuProps {
Expand All @@ -46,7 +42,6 @@ export interface SplitMenuProps {
}

export interface SplitMenuState {
reference?: string;
granularity?: string;
sort?: Sort;
limit?: number;
Expand All @@ -58,112 +53,69 @@ export class SplitMenu extends React.Component<SplitMenuProps, SplitMenuState> {

componentWillMount() {
const { split } = this.props;
const { bucket, reference, sort, limit } = split;
const { bucket, sort, limit } = split;

this.setState({
reference,
sort,
limit,
granularity: bucket && granularityToString(bucket)
});
}

componentDidMount() {
window.addEventListener("keydown", this.globalKeyDownListener);
}

componentWillUnmount() {
window.removeEventListener("keydown", this.globalKeyDownListener);
}

globalKeyDownListener = (e: KeyboardEvent) => enterKey(e) && this.onOkClick();

saveGranularity = (granularity: string) => this.setState({ granularity });

saveSort = (sort: Sort) => this.setState({ sort });

saveLimit = (limit: number) => this.setState({ limit });

onCancelClick = () => this.props.onClose();

onOkClick = () => {
if (!this.validate()) return;
const { split, saveSplit, onClose } = this.props;
const newSplit = this.constructSplitCombine();
saveSplit(split, newSplit);
onClose();
saveSplit = () => {
const { split, saveSplit } = this.props;
saveSplit(split, this.createSplit());
};

private constructGranularity(): Bucket {
const { dimension: { kind } } = this.props;
const { granularity } = this.state;
if (kind === "time") {
return Duration.fromJS(granularity);
}
if (kind === "number") {
return parseInt(granularity, 10);
}
return null;
}

private constructSplitCombine(): Split {
const { split: { type } } = this.props;
const { limit, sort, reference } = this.state;
const bucket = this.constructGranularity();
return new Split({ type, reference, limit, sort, bucket });
private createSplit(): Split {
const { split, dimension } = this.props;
const { limit, sort, granularity } = this.state;
return createSplit({ split, dimension, limit, sort, granularity });
}

validate() {
const { dimension: { kind }, split: originalSplit } = this.props;
if (!isGranularityValid(kind, this.state.granularity)) {
return false;
}
const newSplit: Split = this.constructSplitCombine();
return !originalSplit.equals(newSplit);
const { dimension, split } = this.props;
const { limit, sort, granularity } = this.state;
return validateSplit({ split, dimension, limit, sort, granularity });
}

renderSortDropdown() {
const { essence, dimension } = this.props;
const { sort } = this.state;
render() {
const { essence, containerStage, openOn, dimension, onClose } = this.props;
const { granularity, sort, limit } = this.state;

const seriesSortOns = essence.seriesSortOns(true).toArray();
const options = [new DimensionSortOn(dimension), ...seriesSortOns];
const selected = SortOn.fromSort(sort, essence);
return <SortDropdown
direction={sort.direction}
selected={selected}
options={options}
onChange={this.saveSort}
/>;
}

render() {
const { containerStage, openOn, dimension, onClose } = this.props;
const { granularity, limit } = this.state;
if (!dimension) return null;

return <BubbleMenu
className="split-menu"
direction="down"
containerStage={containerStage}
stage={Stage.fromSize(250, 240)}
return <SplitMenuBase
openOn={openOn}
containerStage={containerStage}
onClose={onClose}
>
onSave={this.saveSplit}
dimension={dimension}
isValid={this.validate()}>
<GranularityPicker
dimension={dimension}
granularityChange={this.saveGranularity}
granularity={granularity}
/>
{this.renderSortDropdown()}
<SortDropdown
direction={sort.direction}
selected={selected}
options={options}
onChange={this.saveSort}
/>
<LimitDropdown
onLimitSelect={this.saveLimit}
selectedLimit={limit}
selectedLimit={limit}
includeNone={isContinuous(dimension)}
limits={dimension.limits}/>
<div className="button-bar">
<Button className="ok" type="primary" disabled={!this.validate()} onClick={this.onOkClick} title={STRINGS.ok} />
<Button type="secondary" onClick={this.onCancelClick} title={STRINGS.cancel} />
</div>
</BubbleMenu>;
</SplitMenuBase>;
}
}
16 changes: 12 additions & 4 deletions src/client/components/split-tile/split-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import { Stage } from "../../../common/models/stage/stage";
import { Binary, Ternary, Unary } from "../../../common/utils/functional/functional";
import { Fn } from "../../../common/utils/general/general";
import { classNames } from "../../utils/dom/dom";
import { SplitMenu } from "../split-menu/split-menu";
import { SplitMenu, SplitMenuProps } from "../split-menu/split-menu";
import { SvgIcon } from "../svg-icon/svg-icon";
import { WithRef } from "../with-ref/with-ref";

interface SplitTileProps {
export interface SplitTileBaseProps {
essence: Essence;
split: Split;
dimension: Dimension;
Expand All @@ -41,10 +41,18 @@ interface SplitTileProps {
containerStage: Stage;
}

const SPLIT_CLASS_NAME = "split";
interface SplitTileProps extends SplitTileBaseProps {
splitMenuComponent: React.ComponentType<SplitMenuProps>;
}

export const SPLIT_CLASS_NAME = "split";

export const DefaultSplitTile: React.SFC<SplitTileBaseProps> = props => {
return <SplitTile {...props} splitMenuComponent={SplitMenu} />;
};

export const SplitTile: React.SFC<SplitTileProps> = props => {
const { essence, open, split, dimension, style, removeSplit, updateSplit, openMenu, closeMenu, dragStart, containerStage } = props;
const { splitMenuComponent: SplitMenu, essence, open, split, dimension, style, removeSplit, updateSplit, openMenu, closeMenu, dragStart, containerStage } = props;

const title = split.getTitle(dimension);

Expand Down
13 changes: 11 additions & 2 deletions src/client/components/split-tile/split-tiles-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,29 @@ import { DraggedElementType, DragManager } from "../../utils/drag-manager/drag-m
import { getMaxItems } from "../../utils/pill-tile/pill-tile";
import { DragIndicator } from "../drag-indicator/drag-indicator";
import { AddSplit } from "./add-split";
import { DefaultSplitTile, SplitTileBaseProps } from "./split-tile";
import "./split-tile.scss";
import { SplitTiles } from "./split-tiles";

interface SplitTilesRowProps {
export interface SplitTilesRowBaseProps {
clicker: Clicker;
essence: Essence;
menuStage: Stage;
}

interface SplitTilesRowProps extends SplitTilesRowBaseProps {
splitTileComponent: React.ComponentType<SplitTileBaseProps>;
}

interface SplitTilesRowState {
dragPosition?: DragPosition;
openedSplit?: Split;
overflowOpen?: boolean;
}

export const DefaultSplitTilesRow: React.SFC<SplitTilesRowBaseProps> = props =>
<SplitTilesRow {...props} splitTileComponent={DefaultSplitTile} />;

export class SplitTilesRow extends React.Component<SplitTilesRowProps, SplitTilesRowState> {
private items = React.createRef<HTMLDivElement>();

Expand Down Expand Up @@ -189,13 +197,14 @@ export class SplitTilesRow extends React.Component<SplitTilesRowProps, SplitTile
};

render() {
const { essence, menuStage } = this.props;
const { essence, menuStage, splitTileComponent } = this.props;
const { dragPosition, overflowOpen, openedSplit } = this.state;
return <div className="split-tile" onDragEnter={this.dragEnter}>
<div className="title">{STRINGS.split}</div>
<div className="items" ref={this.items}>
<SplitTiles
essence={essence}
splitTileComponent={splitTileComponent}
openedSplit={openedSplit}
removeSplit={this.removeSplit}
updateSplit={this.updateSplit}
Expand Down
Loading

0 comments on commit e02ade2

Please sign in to comment.