Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Query Source to React: resizable areas (v2) #4503

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions client/app/assets/less/redash/query.less
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,20 @@ a.label-tag {
display: flex;
width: 100vw;

.resizable-component.react-resizable {
.react-resizable-handle-horizontal {
border-right: 1px solid #efefef;
}

.react-resizable-handle-vertical {
border-bottom: 1px solid #efefef;
}
}

.query-metadata-new.query-metadata-horizontal {
border-bottom: 1px solid #efefef;
}

.tile,
.tiled {
box-shadow: none;
Expand Down Expand Up @@ -330,10 +344,6 @@ a.label-tag {
}
}
}
main {
display: flex;
height: 100%;
}
.content {
background: #fff;
flex-grow: 1;
Expand All @@ -344,10 +354,6 @@ a.label-tag {
padding: 0;
overflow-x: hidden;

.editor {
border-bottom: 1px solid #efefef;
}

.pivot-table-visualization-container > table,
.visualization-renderer > .visualization-renderer-wrapper {
overflow: visible;
Expand All @@ -359,7 +365,6 @@ a.label-tag {
}
.row {
background: #fff;
z-index: 9;
min-height: 50px;

&.resizable {
Expand All @@ -372,6 +377,10 @@ a.label-tag {
justify-content: space-around;
align-content: space-around;
overflow: hidden;

min-height: 10px;
max-height: 70vh;
flex: 0 0 300px;
}

.row {
Expand Down
155 changes: 155 additions & 0 deletions client/app/components/Resizable/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import d3 from "d3";
import React, { useRef, useMemo, useCallback, useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Resizable as ReactResizable } from "react-resizable";
import { KeyboardShortcuts } from "@/services/keyboard-shortcuts";

import "./index.less";

export default function Resizable({ toggleShortcut, direction, sizeAttribute, children }) {
const [size, setSize] = useState(0);
const elementRef = useRef();
const wasUsingTouchEvents = useRef(false);
const wasResizedRef = useRef(false);

const sizeProp = direction === "horizontal" ? "width" : "height";
sizeAttribute = sizeAttribute || sizeProp;

const getElementSize = useCallback(() => {
if (!elementRef.current) {
return 0;
}
return Math.floor(elementRef.current.getBoundingClientRect()[sizeProp]);
}, [sizeProp]);

const savedSize = useRef(null);
const toggle = useCallback(() => {
if (!elementRef.current) {
return;
}

const element = d3.select(elementRef.current);
let targetSize;
if (savedSize.current === null) {
targetSize = "0px";
savedSize.current = `${getElementSize()}px`;
} else {
targetSize = savedSize.current;
savedSize.current = null;
}

element
.style(sizeAttribute, savedSize.current || "0px")
.transition()
.duration(200)
.ease("swing")
.style(sizeAttribute, targetSize);
}, [getElementSize, sizeAttribute]);

const resizeHandle = useMemo(
() => (
<span
className={`react-resizable-handle react-resizable-handle-${direction}`}
onClick={() => {
// On desktops resize uses `mousedown`/`mousemove`/`mouseup` events, and there is a conflict
// with this `click` handler: after user releases mouse - this handler will be executed.
// So we use `wasResized` flag to check if there was actual resize or user just pressed and released
// left mouse button (see also resize event handlers where ths flag is set).
// On mobile devices `touchstart`/`touchend` events wll be used, so it's safe to just execute this handler.
// To detect which set of events was actually used during particular resize operation, we pass
// `onMouseDown` handler to draggable core and check event type there (see also that handler's code).
if (wasUsingTouchEvents.current || !wasResizedRef.current) {
toggle();
}
wasUsingTouchEvents.current = false;
wasResizedRef.current = false;
}}
/>
),
[direction, toggle]
);

useEffect(() => {
if (toggleShortcut) {
const shortcuts = {
[toggleShortcut]: toggle,
};

KeyboardShortcuts.bind(shortcuts);
return () => {
KeyboardShortcuts.unbind(shortcuts);
};
}
}, [toggleShortcut, toggle]);

const resizeEventHandlers = useMemo(
() => ({
onResizeStart: () => {
// use element's size as initial value (it will also check constraints set in CSS)
setSize(getElementSize());
},
onResize: (unused, data) => {
// update element directly for better UI responsiveness
d3.select(elementRef.current).style(sizeAttribute, `${data.size[sizeProp]}px`);
setSize(data.size[sizeProp]);
wasResizedRef.current = true;
},
onResizeStop: () => {
if (wasResizedRef.current) {
savedSize.current = null;
}
},
}),
[sizeProp, getElementSize, sizeAttribute]
);

const draggableCoreOptions = useMemo(
() => ({
onMouseDown: e => {
// In some cases this handler is executed twice during the same resize operation - first time
// with `touchstart` event and second time with `mousedown` (probably emulated by browser).
// Therefore we set the flag only when we receive `touchstart` because in ths case it's definitely
// mobile browser (desktop browsers will also send `mousedown` but never `touchstart`).
if (e.type === "touchstart") {
wasUsingTouchEvents.current = true;
}
},
}),
[]
);

if (!children) {
return null;
}

children = React.createElement(children.type, { ...children.props, ref: elementRef });

return (
<ReactResizable
className="resizable-component"
axis={direction === "horizontal" ? "x" : "y"}
resizeHandles={[direction === "horizontal" ? "e" : "s"]}
handle={resizeHandle}
width={direction === "horizontal" ? size : 0}
height={direction === "vertical" ? size : 0}
minConstraints={[0, 0]}
{...resizeEventHandlers}
draggableOpts={draggableCoreOptions}>
{children}
</ReactResizable>
);
}

Resizable.propTypes = {
direction: PropTypes.oneOf(["horizontal", "vertical"]),
sizeAttribute: PropTypes.string,
toggleShortcut: PropTypes.string,
children: PropTypes.element,
};

Resizable.defaultProps = {
direction: "horizontal",
sizeAttribute: null, // "width"/"height" - depending on `direction`
toggleShortcut: null,
children: null,
};
57 changes: 57 additions & 0 deletions client/app/components/Resizable/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@import (reference, less) "~@/assets/less/inc/variables.less";

.resizable-component.react-resizable {
position: relative;

.react-resizable-handle {
position: absolute;
background: #fff;
margin: 0;
padding: 0;

display: flex;
align-items: center;
justify-content: center;

&:hover,
&:active {
background: mix(@redash-gray, #fff, 6%);
}

&.react-resizable-handle-horizontal {
cursor: col-resize;
width: 10px;
height: auto;
right: 0;
top: 0;
bottom: 0;

&:before {
content: "";
display: inline-block;
width: 3px;
height: 25px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
}

&.react-resizable-handle-vertical {
cursor: row-resize;
width: auto;
height: 10px;
left: 0;
right: 0;
bottom: 0;

&:before {
content: "";
display: inline-block;
width: 25px;
height: 3px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
}
}
}
1 change: 1 addition & 0 deletions client/app/components/dashboards/DashboardGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import cfg from "@/config/dashboard-grid-options";
import AutoHeightController from "./AutoHeightController";
import { WidgetTypeEnum } from "@/services/widget";

import "react-resizable/css/styles.css";
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
import "react-grid-layout/css/styles.css";
import "./dashboard-grid.less";

Expand Down
22 changes: 13 additions & 9 deletions client/app/components/dashboards/dashboard-grid.less
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@

&.editing-mode {
/* Y axis lines */
background: linear-gradient(to right, transparent, transparent 1px, #F6F8F9 1px, #F6F8F9), linear-gradient(to bottom, #B3BABF, #B3BABF 1px, transparent 1px, transparent);
background: linear-gradient(to right, transparent, transparent 1px, #f6f8f9 1px, #f6f8f9),
linear-gradient(to bottom, #b3babf, #b3babf 1px, transparent 1px, transparent);
background-size: 5px 50px;
background-position-y: -8px;

Expand All @@ -48,7 +49,8 @@
left: 0;
bottom: 85px;
right: 15px;
background: linear-gradient(to bottom, transparent, transparent 2px, #F6F8F9 2px, #F6F8F9 5px), linear-gradient(to left, #B3BABF, #B3BABF 1px, transparent 1px, transparent);
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
background-size: calc((100vw - 15px) / 6) 5px;
background-position: -7px 1px;
}
Expand Down Expand Up @@ -123,11 +125,10 @@

// react-grid-layout overrides
.react-grid-item {

// placeholder color
&.react-grid-placeholder {
border-radius: 3px;
background-color: #E0E6EB;
background-color: #e0e6eb;
opacity: 0.5;
}

Expand All @@ -142,10 +143,13 @@
}

// resize handle size
& > .react-resizable-handle::after {
width: 11px;
height: 11px;
right: 5px;
bottom: 5px;
& > .react-resizable-handle {
background: none;
&:after {
width: 11px;
height: 11px;
right: 5px;
bottom: 5px;
}
}
}
Loading