Skip to content

Commit

Permalink
[dashboard-builder] add top-level tabs + undo-redo (apache#4626)
Browse files Browse the repository at this point in the history
* [top-level-tabs] initial working version of top-level tabs

* [top-level-tabs] simplify redux and disable ability to displace top-level tabs with other tabs

* [top-level-tabs] improve tab drag and drop css

* [undo-redo] add redux undo redo

* [dnd] clean up dropResult shape, add new component source id + type, use css for drop indicator instead of styles and fix tab indicators.

* [top-level-tabs] add 'Collapse tab content' to delete tabs button

* [dnd] add depth validation to drag and drop logic

* [dashboard-builder] add resize action, enforce minimum width of columns, column children inherit column size when necessary, meta.rowStyle => meta.background, add background to columns

* [dashboard-builder] make sure getChildWidth returns a number
  • Loading branch information
williaster committed Jun 22, 2018
1 parent c62af8c commit abc3ec0
Show file tree
Hide file tree
Showing 64 changed files with 1,322 additions and 690 deletions.
113 changes: 97 additions & 16 deletions superset/assets/javascripts/dashboard/v2/actions/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
import findParentId from '../util/findParentId';
import {
CHART_TYPE,
MARKDOWN_TYPE,
TABS_TYPE,
} from '../util/componentTypes';

// Component CRUD -------------------------------------------------------------
export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
export function updateComponents(nextComponents) {
return {
Expand Down Expand Up @@ -29,6 +38,67 @@ export function createComponent(dropResult) {
};
}

// Tabs -----------------------------------------------------------------------
export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
export function createTopLevelTabs(dropResult) {
return {
type: CREATE_TOP_LEVEL_TABS,
payload: {
dropResult,
},
};
}

export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
export function deleteTopLevelTabs() {
return {
type: DELETE_TOP_LEVEL_TABS,
payload: {},
};
}

// Resize ---------------------------------------------------------------------
export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
export function resizeComponent({ id, width, height }) {
return (dispatch, getState) => {
const { dashboard: undoableDashboard } = getState();
const { present: dashboard } = undoableDashboard;
const component = dashboard[id];

if (
component &&
(component.meta.width !== width || component.meta.height !== height)
) {
// update the size of this component + any resizable children
const updatedComponents = {
[id]: {
...component,
meta: {
...component.meta,
width: width || component.meta.width,
height: height || component.meta.height,
},
},
};

component.children.forEach((childId) => {
const child = dashboard[childId];
if ([CHART_TYPE, MARKDOWN_TYPE].includes(child.type)) {
updatedComponents[childId] = {
...child,
meta: {
...child.meta,
width: width || component.meta.width,
height: height || component.meta.height,
},
};
}
});

dispatch(updateComponents(updatedComponents));
}
};
}

// Drag and drop --------------------------------------------------------------
export const MOVE_COMPONENT = 'MOVE_COMPONENT';
Expand All @@ -43,27 +113,38 @@ export function moveComponent(dropResult) {

export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
export function handleComponentDrop(dropResult) {
return (dispatch) => {
if (
dropResult.destination
&& dropResult.source
return (dispatch, getState) => {
const { source, destination } = dropResult;
const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;

if (droppedOnRoot) {
dispatch(createTopLevelTabs(dropResult));
} else if (destination && isNewComponent) {
dispatch(createComponent(dropResult));
} else if (
destination
&& source
&& !( // ensure it has moved
dropResult.destination.droppableId === dropResult.source.droppableId
&& dropResult.destination.index === dropResult.source.index
destination.id === source.id
&& destination.index === source.index
)
) {
return dispatch(moveComponent(dropResult));
dispatch(moveComponent(dropResult));
}

// new components don't have a source
} else if (dropResult.destination && !dropResult.source) {
return dispatch(createComponent(dropResult));
// 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];

if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
const parentId = findParentId({ childId: source.id, components: dashboard });
dispatch(deleteComponent(source.id, parentId));
}
}

return null;
};
}

// Resize ---------------------------------------------------------------------

// export function dashboardComponentResizeStart() {}
// export function dashboardComponentResize() {}
// export function dashboardComponentResizeStop() {}
21 changes: 3 additions & 18 deletions superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';

import DashboardBuilder from './DashboardBuilder';
import StaticDashboard from './StaticDashboard';
import DashboardHeader from './DashboardHeader';
import DashboardBuilder from '../containers/DashboardBuilder';

import '../../../../stylesheets/dashboard-v2.css';
import '../stylesheets/index.less';

const propTypes = {
Expand All @@ -22,20 +19,8 @@ const defaultProps = {

class Dashboard extends React.Component {
render() {
const { editMode, actions } = this.props;
const { setEditMode, updateDashboardTitle } = actions;
return (
<div className="dashboard-v2">
<DashboardHeader
editMode={true}
setEditMode={setEditMode}
updateDashboardTitle={updateDashboardTitle}
/>

{true ?
<DashboardBuilder /> : <StaticDashboard />}
</div>
);
// @TODO delete this component?
return <DashboardBuilder />;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,116 @@ import React from 'react';
import PropTypes from 'prop-types';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
import cx from 'classnames';

import BuilderComponentPane from './BuilderComponentPane';
import DashboardHeader from '../containers/DashboardHeader';
import DashboardGrid from '../containers/DashboardGrid';
import IconButton from './IconButton';
import DragDroppable from './dnd/DragDroppable';
import DashboardComponent from '../containers/DashboardComponent';
import WithPopoverMenu from './menu/WithPopoverMenu';

import {
DASHBOARD_GRID_ID,
DASHBOARD_ROOT_ID,
DASHBOARD_ROOT_DEPTH,
} from '../util/constants';

const propTypes = {
editMode: PropTypes.bool,

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

const defaultProps = {
editMode: true,
};

class DashboardBuilder extends React.Component {
static shouldFocusTabs(event, container) {
// don't focus the tabs when we click on a tab
return event.target.tagName === 'UL' || (
/icon-button/.test(event.target.className) && container.contains(event.target)
);
}

constructor(props) {
super(props);
// this component might control the state of the side pane etc. in the future
this.state = {};
this.state = {
tabIndex: 0, // top-level tabs
};
this.handleChangeTab = this.handleChangeTab.bind(this);
}

handleChangeTab({ tabIndex }) {
this.setState(() => ({ tabIndex }));
}

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

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

const gridComponent = dashboard[gridComponentId];

return (
<div className={cx('dashboard-builder')}>
<DashboardGrid />
<BuilderComponentPane />
<div className="dashboard-v2">
{topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
<DashboardHeader />
) : (
<DragDroppable
component={dashboardRoot}
parentComponent={null}
depth={DASHBOARD_ROOT_DEPTH}
index={0}
orientation="column"
onDrop={topLevelTabs ? null : handleComponentDrop}
>
{({ dropIndicatorProps }) => (
<div>
<DashboardHeader />
{dropIndicatorProps && <div {...dropIndicatorProps} />}
</div>
)}
</DragDroppable>)}

{topLevelTabs &&
<WithPopoverMenu
shouldFocus={DashboardBuilder.shouldFocusTabs}
menuItems={[
<IconButton
className="fa fa-level-down"
label="Collapse tab content"
onClick={deleteTopLevelTabs}
/>,
]}
>
<DashboardComponent
id={topLevelTabs.id}
parentId={DASHBOARD_ROOT_ID}
depth={DASHBOARD_ROOT_DEPTH + 1}
index={0}
renderTabContent={false}
onChangeTab={this.handleChangeTab}
/>
</WithPopoverMenu>}

<div className="dashboard-builder">
<DashboardGrid
gridComponent={gridComponent}
depth={DASHBOARD_ROOT_DEPTH + 1}
/>
<BuilderComponentPane />
</div>
</div>
);
}
Expand Down
Loading

0 comments on commit abc3ec0

Please sign in to comment.