Skip to content

Commit

Permalink
[dashboard builder] static layout + toasts (#4763)
Browse files Browse the repository at this point in the history
* [dashboard-builder] remove spacer component

* [dashboard-builder] better transparent indicator, better grid gutter logic, no dragging top-level tabs, headers are multiples of grid unit, fix row height granularity, update redux state key dashboard => dashboardLayout

* [dashboard-builder] don't blast column child dimensions on resize

* [dashboard-builder] ResizableContainer min size can't be smaller than size, fix row style, role=none on WithPopoverMenu container

* [edit mode] add edit mode to redux and propogate to all <DashboardComponent />s

* [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers

* [dashboard-builder] add info toast when dropResult overflows parent
  • Loading branch information
williaster authored Apr 5, 2018
1 parent 1aebfd6 commit a19a260
Show file tree
Hide file tree
Showing 62 changed files with 715 additions and 630 deletions.
16 changes: 11 additions & 5 deletions superset/assets/javascripts/components/EditableTitle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class EditableTitle extends React.PureComponent {
}

render() {
let input = (
let content = (
<input
required
type={this.state.isEditing ? 'text' : 'button'}
Expand All @@ -129,19 +129,25 @@ class EditableTitle extends React.PureComponent {
/>
);
if (this.props.showTooltip) {
input = (
content = (
<TooltipWrapper
label="title"
tooltip={this.props.canEdit ? t('click to edit title') :
this.props.noPermitTooltip || t('You don\'t have the rights to alter this title.')}
>
{input}
{content}
</TooltipWrapper>
);
}
return (
<span className={cx('editable-title', this.props.canEdit && 'editable-title--editable')}>
{input}
<span
className={cx(
'editable-title',
this.props.canEdit && 'editable-title--editable',
this.state.isEditing && 'editable-title--editing',
)}
>
{content}
</span>
);
}
Expand Down
5 changes: 4 additions & 1 deletion superset/assets/javascripts/dashboard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ initJQueryAjax();
const appContainer = document.getElementById('app');
// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
// const initState = Object.assign({}, getInitialState(bootstrapData));

const initState = {
dashboard: {
dashboardLayout: {
past: [],
present: emptyDashboardLayout,
future: [],
},
editMode: true,
messageToasts: [],
};

const store = createStore(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { addInfoToast } from './messageToasts';
import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
import dropOverflowsParent from '../util/dropOverflowsParent';
import findParentId from '../util/findParentId';
import {
CHART_TYPE,
MARKDOWN_TYPE,
TABS_TYPE,
} from '../util/componentTypes';

// Component CRUD -------------------------------------------------------------
export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
Expand Down Expand Up @@ -61,8 +59,8 @@ export function deleteTopLevelTabs() {
export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
export function resizeComponent({ id, width, height }) {
return (dispatch, getState) => {
const { dashboard: undoableDashboard } = getState();
const { present: dashboard } = undoableDashboard;
const { dashboardLayout: undoableLayout } = getState();
const { present: dashboard } = undoableLayout;
const component = dashboard[id];

if (
Expand All @@ -88,8 +86,8 @@ export function resizeComponent({ id, width, height }) {
...child,
meta: {
...child.meta,
width: width || component.meta.width,
height: height || component.meta.height,
width: width || child.meta.width,
height: height || child.meta.height,
},
};
}
Expand All @@ -114,6 +112,15 @@ export function moveComponent(dropResult) {
export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
export function handleComponentDrop(dropResult) {
return (dispatch, getState) => {
const overflowsParent = dropOverflowsParent(dropResult, getState().dashboardLayout.present);

if (overflowsParent) {
return dispatch(addInfoToast(
`Parent does not have enough space for this component.
Try decreasing its width or add it to a new row.`,
));
}

const { source, destination } = dropResult;
const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
Expand All @@ -133,14 +140,14 @@ export function handleComponentDrop(dropResult) {
dispatch(moveComponent(dropResult));
}

// if we moved a tab and the parent tabs no longer has children, delete it.
// if we moved a Tab and the parent Tabs no longer has children, delete it.
if (!isNewComponent) {
const { dashboard: undoableDashboard } = getState();
const { present: dashboard } = undoableDashboard;
const sourceComponent = dashboard[source.id];
const { dashboardLayout: undoableLayout } = getState();
const { present: layout } = undoableLayout;
const sourceComponent = layout[source.id];

if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
const parentId = findParentId({ childId: source.id, components: dashboard });
const parentId = findParentId({ childId: source.id, components: layout });
dispatch(deleteComponent(source.id, parentId));
}
}
Expand Down
9 changes: 9 additions & 0 deletions superset/assets/javascripts/dashboard/v2/actions/editMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export function setEditMode(editMode) {
return {
type: SET_EDIT_MODE,
payload: {
editMode,
},
};
}
49 changes: 49 additions & 0 deletions superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';

function getToastUuid(type) {
return `${Math.random().toString(16).slice(2)}-${type}-${Math.random().toString(16).slice(2)}`;
}

export const ADD_TOAST = 'ADD_TOAST';
export function addToast({ toastType, text }) {
debugger;
return {
type: ADD_TOAST,
payload: {
id: getToastUuid(toastType),
toastType,
text,
},
};
}

export const REMOVE_TOAST = 'REMOVE_TOAST';
export function removeToast(id) {
return {
type: REMOVE_TOAST,
payload: {
id,
},
};
}

// Different types of toasts
export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
export function addInfoToast(text) {
return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST }));
}

export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
export function addSuccessToast(text) {
return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST }));
}

export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
export function addWarningToast(text) {
return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST }));
}

export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
export function addDangerToast(text) {
return dispatch => dispatch(addToast({ text, toastType: DANGER_TOAST }));
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import NewColumn from './gridComponents/new/NewColumn';
import NewDivider from './gridComponents/new/NewDivider';
import NewHeader from './gridComponents/new/NewHeader';
import NewRow from './gridComponents/new/NewRow';
import NewSpacer from './gridComponents/new/NewSpacer';
import NewTabs from './gridComponents/new/NewTabs';

const propTypes = {
Expand All @@ -24,7 +23,6 @@ class BuilderComponentPane extends React.PureComponent {
<NewHeader />

<NewDivider />
<NewSpacer />

<NewTabs />
<NewRow />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import cx from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
import HTML5Backend from 'react-dnd-html5-backend';
Expand All @@ -9,6 +10,7 @@ import DashboardGrid from '../containers/DashboardGrid';
import IconButton from './IconButton';
import DragDroppable from './dnd/DragDroppable';
import DashboardComponent from '../containers/DashboardComponent';
import ToastPresenter from '../containers/ToastPresenter';
import WithPopoverMenu from './menu/WithPopoverMenu';

import {
Expand All @@ -18,11 +20,10 @@ import {
} from '../util/constants';

const propTypes = {
editMode: PropTypes.bool,

// redux
dashboard: PropTypes.object.isRequired,
dashboardLayout: PropTypes.object.isRequired,
deleteTopLevelTabs: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
};

Expand Down Expand Up @@ -52,20 +53,20 @@ class DashboardBuilder extends React.Component {

render() {
const { tabIndex } = this.state;
const { handleComponentDrop, dashboard, deleteTopLevelTabs } = this.props;
const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
const { handleComponentDrop, dashboardLayout, deleteTopLevelTabs, editMode } = this.props;
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
const rootChildId = dashboardRoot.children[0];
const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];

const gridComponentId = topLevelTabs
? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
: DASHBOARD_GRID_ID;

const gridComponent = dashboard[gridComponentId];
const gridComponent = dashboardLayout[gridComponentId];

return (
<div className="dashboard-v2">
{topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
<div className={cx('dashboard-v2', editMode && 'dashboard-v2--editing')}>
{topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
<DashboardHeader />
) : (
<DragDroppable
Expand All @@ -74,7 +75,8 @@ class DashboardBuilder extends React.Component {
depth={DASHBOARD_ROOT_DEPTH}
index={0}
orientation="column"
onDrop={topLevelTabs ? null : handleComponentDrop}
onDrop={handleComponentDrop}
editMode
>
{({ dropIndicatorProps }) => (
<div>
Expand All @@ -94,6 +96,7 @@ class DashboardBuilder extends React.Component {
onClick={deleteTopLevelTabs}
/>,
]}
editMode={editMode}
>
<DashboardComponent
id={topLevelTabs.id}
Expand All @@ -105,13 +108,14 @@ class DashboardBuilder extends React.Component {
/>
</WithPopoverMenu>}

<div className="dashboard-builder">
<div className="dashboard-content">
<DashboardGrid
gridComponent={gridComponent}
depth={DASHBOARD_ROOT_DEPTH + 1}
/>
<BuilderComponentPane />
{editMode && <BuilderComponentPane />}
</div>
<ToastPresenter />
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {

const propTypes = {
depth: PropTypes.number.isRequired,
editMode: PropTypes.bool.isRequired,
gridComponent: componentShape.isRequired,
handleComponentDrop: PropTypes.func.isRequired,
resizeComponent: PropTypes.func.isRequired,
Expand Down Expand Up @@ -70,7 +71,7 @@ class DashboardGrid extends React.PureComponent {
}

render() {
const { gridComponent, handleComponentDrop, depth } = this.props;
const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
const { isResizing, rowGuideTop } = this.state;

return (
Expand Down Expand Up @@ -99,18 +100,19 @@ class DashboardGrid extends React.PureComponent {
))}

{/* render an empty drop target */}
{gridComponent.children.length === 0 &&
{editMode &&
<DragDroppable
component={gridComponent}
depth={depth}
parentComponent={null}
index={0}
index={gridComponent.children.length}
orientation="column"
onDrop={handleComponentDrop}
className="empty-grid-droptarget"
editMode
>
{({ dropIndicatorProps }) => dropIndicatorProps &&
<div {...dropIndicatorProps} />}
<div className="drop-indicator drop-indicator--top" />}
</DragDroppable>}

{isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { componentShape } from '../util/propShapes';
import EditableTitle from '../../../components/EditableTitle';

const propTypes = {
// editMode: PropTypes.bool.isRequired,
// setEditMode: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
component: componentShape.isRequired,

// redux
Expand All @@ -17,6 +16,7 @@ const propTypes = {
onRedo: PropTypes.func.isRequired,
canUndo: PropTypes.bool.isRequired,
canRedo: PropTypes.bool.isRequired,
setEditMode: PropTypes.func.isRequired,
};

class DashboardHeader extends React.Component {
Expand All @@ -27,8 +27,7 @@ class DashboardHeader extends React.Component {
}

toggleEditMode() {
console.log('@TODO toggleEditMode');
// this.props.setEditMode(!this.props.editMode);
this.props.setEditMode(!this.props.editMode);
}

handleChangeText(nextText) {
Expand All @@ -47,19 +46,18 @@ class DashboardHeader extends React.Component {
}

render() {
const { component, onUndo, onRedo, canUndo, canRedo } = this.props;
const editMode = true;
const { component, onUndo, onRedo, canUndo, canRedo, editMode } = this.props;

return (
<div className="dashboard-header">
<h1>
<div className="dashboard-component-header header-large">
<EditableTitle
title={component.meta.text}
onSaveTitle={this.handleChangeText}
showTooltip={false}
canEdit={editMode}
/>
</h1>
</div>
<ButtonToolbar>
<ButtonGroup>
<Button
Expand Down
Loading

0 comments on commit a19a260

Please sign in to comment.