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 all 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
163 changes: 163 additions & 0 deletions client/app/components/Resizable/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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 wasUsingTouchEventsRef = 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);

// update state to new element's size
setSize(parseInt(targetSize) || 0);
}, [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 (wasUsingTouchEventsRef.current || !wasResizedRef.current) {
toggle();
}
wasUsingTouchEventsRef.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)
// updated here and in `draggableCore::onMouseDown` handler to ensure that right value will be used
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") {
wasUsingTouchEventsRef.current = true;
}

// use element's size as initial value (it will also check constraints set in CSS)
// updated here and in `onResizeStart` handler to ensure that right value will be used
setSize(getElementSize());
},
}),
[getElementSize]
);

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;
}
}
}
}
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